Android性能优化小结

一、什么是性能?

《Android性能优化小结》 image.png

以上图片形象的描述了什么是性能。如果我们开发出来的APP没有达到以上的快、稳、省、小。那说明我们的应用是有待优化的,就是我们说的性能优化。

二、性能优化

1、布局优化

(1)避免不必要的多层级嵌套
屏幕某个像素在同一帧的时间内被绘制了多次,会浪费CPU资源。
(2)如果父控件已经有颜色,也是子控件需要的颜色,就没必要再子控件设置颜色。
(3)能用LinearLayout和FrameLayout,尽量不用RelativeLayout。因为RelativeLayout相对复杂,绘制更耗时
(4)使用ViewStub,按需加载,需要的时候才绘制。
(5)复杂的界面可以用ConstraintLayout,可以有效减少层级(本人不怎么使用,使用比较麻烦)

2、绘制的优化

(1)不要在onDraw方法中做耗时操作,会导致View绘制不流畅
(2)不要在onDraw方法中创建新的局部对象,因为onDraw方法会频繁的调用,这样就会创建很多局部对象,不仅仅暂用内存,也会导致系统频繁的GC操作。大大的降低了程序的运行效率。

3、内存的优化

3.1、内存泄露的危害

我们知道JAVA中特有的GC让JAVA程序员更加的轻松,也就是JAVA虚拟机自动识别那些没用的对象,然后进行回收,释放内存。但是往往有的时候,有的对象逻辑上已经没用了,但是还保持着引用,想当于赖在内存中不走,空耗内存。这样就导致了内存的泄露。因为有了内存的泄露,那么内存被占用就越来越多,这样就更加容易触发GC,而每一次GC操作,所有的线程都是停止的状态,如果过多频繁的GC,那么线程停止和恢复就越频繁,也就会容易造成卡顿。
简单的来说
1、内存泄露会导致内存被空耗。
2、内存泄露过多会造成频繁的GC,造成卡顿。

3.2、造成内存泄露的情况

上面我们说了内存泄露的危害,下面我们来了解下内存泄露的主要常见情况。

1、集合类泄露
static List<Object> mList = new ArrayList<>();
   for (int i = 0; i < 100; i++) {
       Object obj = new Object();
      mList.add(obj);
    }

以上的静态集合变量持有了众多个对象,当我们不用该集合又不做处理的时候,就会造成内存泄露。处理也比较简单。先把集合清理掉,然后把它的引用也释放。

 mList.clear();
 mList = null;
2、单例、静态变量造成的泄露

单例模式具有静态性,它的生命周期等于应用程序的生命周期。往往很容易造成内存泄露。我们看下面的一个例子。

public class SingleInstance {

    private static SingleInstance mInstance;
    private Context mContext;

    private SingleInstance(Context context){
        this.mContext = context;
    }

    public static SingleInstance newInstance(Context context){
        if(mInstance == null){
            mInstance = new SingleInstance(context);
        }
        return sInstance;
    }
}

我们看到构造方法SingleInstance需要一个Context对象,如果我们把Activity的Context传给它,当我们的Activity需要被销毁的时候,而静态变量SingleInstance mInstance还持有Activity的引用,会导致GC无法回收。所以就会出现了内存泄露,也就是生命周期长的持有生命周期短的引用,导致生命周期短的无法被回收。我们来看下下具体的解决办法。

public class SingleInstance {

    private static SingleInstance mInstance;
    private Context mContext;

    private SingleInstance(Context context){
        this.mContext = context.getApplicationContext();
    }

    public static SingleInstance newInstance(Context context){
        if(mInstance == null){
            mInstance = new SingleInstance(context);
        }
        return sInstance;
    }
}

3、匿名内部类和非静态内部类

首先我们说非静态内部类导致的内存泄露。
我们首先要知道,非静态的内部类会持有外部类的引用,而静态的内部类不会持有外部类的引用。下面我们先来个例子:

public class TestActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        new MyAscnyTask().execute();
    }

    class MyAscnyTask extends AsyncTask<Void, Integer, String>{
        @Override
        protected String doInBackground(Void... params) {
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
}

在TestActivity 中有一个 非静态内部类MyAscnyTask ,如果MyAscnyTask 中的doInBackground操作耗时较长,而TestActivity 早就被关闭了,而MyAscnyTask 还持有TestActivity 的引用,就回导致TestActivity 无法被GC。解决办法,我们只需要把非静态内部类改成静态的内部类即可:

public class TestActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        new MyAscnyTask().execute();
    }
//改了这里 注意一下 static
   static  class MyAscnyTask extends AsyncTask<Void, Integer, String>{
        @Override
        protected String doInBackground(Void... params) {
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
}

说完非静态内部类,我们来说匿名内部类,其实匿名内部类和非静态内部类的性质是一样的,都是因为持有
外部类的引用,导致无法GC。下面我们来看个例子

public class TestActivity extends Activity {
private TextView mText;
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
//do something
mText.setText(" do someThing");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
mText = findVIewById(R.id.mText);
        //  匿名线程持有 Activity 的引用,进行耗时操作
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

    
        mHandler. sendEmptyMessageDelayed(0, 100000);
    }

你们线程持有了外部类TestActivity 的引用,当TestActivity 销毁的时候,由于匿名线程还持有TestActivity的引用,导致无法GC。以上我们第一反应我们就会想到,我们只需把这个匿名内部类改成静态的内部类,这样就不会持有外部类的引用了,这样问题不就是解决了吗?对于以上,我们只需要把Thread和Handler 用一个静态的内部类实现,这就不会持有TestActivity 的引用了。
是的,这样做只是解决问题第一步,我们观察

mText.setText(" do someThing");

我们把Handler改成了静态的,但是我们在handleMessage方法中做UI操作,mText.setText(” do someThing”);而mText肯定是持有TestActivity 的引用,由于Handler是静态的,也就说生命周期大于TestActivity,这一来又会导致生命周期长的持有生命周期短的引用,无法GC。怎么解决呢?除了我们要把匿名内部类改成静态的内部类之外,我们还需要通过弱引用的方式去解决。我们先来了解一下JAVA中的几个引用。
(1)强引用
我们平时不做特殊处理的情况下,一般都是属于强引用。当无法GC的时候,就算内存溢出OOM了也不会回收。
(2)软引用
当内存空间足够的情况下不会回收,只有内存空间不足的时候才会强制回收
(3)弱引用
GC的时候,不管内存够不够都要回收他。由于GC是一个优先级很低的线程,不会频繁的操作,所以我们使用的时候,不用担心对象被盲目的GC掉。
(4)虚引用
没用过
所以针对以上的问题,我们可以使用弱引用,要就是说,当TestActivity被关闭的时候,JVM会进行GC回收。这个时候即使其他的类还持有TestActivity的引用,由于我们使用的是弱引用,所以会进行强制的回收。具体解决办法

public class TestActivity extends Activity {
    private TextView mText;
    private MyHandler myHandler = new MyHandler(TestActivity.this);
    private MyThread myThread = new MyThread();

    private static class MyHandler extends Handler {

        WeakReference<TestActivity> weakReference;

        MyHandler(TestActivity testActivity) {
            this.weakReference = new WeakReference<TestActivity>(testActivity);

        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            weakReference.get().mText.setText("do someThing");

        }
    }

    private static class MyThread extends Thread {

        @Override
        public void run() {
            super.run();

            try {
                sleep(100000);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        mText = findViewById(R.id.mText);
        myHandler.sendEmptyMessageDelayed(0, 100000);
        myThread.start();
    }
//最后清空这些回调 
    @Override
    protected void onDestroy() {
        super.onDestroy();
        myHandler.removeCallbacksAndMessages(null);
    }
4、资源未关闭造成内存泄露

网络、文件等流忘记关闭
手动注册广播,忘记取消注册
Service执行完忘记stopSelf
EventBus 等观察者模式框架忘记手动解除注册
平时注意代码规范。
工具:leakcanary可以检测内存泄露

4、启动速度的优化

在介绍APP启动速度的优化的时候我们先来了解APP启动的三种方式:
冷启动、热启动、温启动。

冷启动

冷启动指的是APP应用从头开始启动,通常是APP的第一次启动。启动的时候系统有三个任务:
(1)加载并启动应用程序
(2)启动后立即显示窗口(也就是我们平常看到的空白窗口)
(3)创建应用进程
只有创建应用进程完之后,才会创建我们的应用程序对象,主要由以下步骤:
(1)启动主线程
(2)创建主Activity
(3)加载布局
(4)屏幕布局(在屏幕中布置布局)
(5)初始化绘制
当绘制完成之后,系统才会交换当前显示的窗口,将其替换为主活动,此时用户可以使用该应用程序,也就是我们的系统界面可以看到了。在绘制完成之前,一直都是属于空白页。

Application的创建

我们知道Application初始化完之后,才会初始化我们的主Activity,然后加载布局,绘制操作,最后将窗口交换为当前Activity的界面。在显示主Activity界面之前窗口都是属于空白页。

热启动

热启动比冷启动开销较小,在热启动中,当我们需要启动的Activity已经在内存中,则不会创建新的Activity,而是直接把Activity带到前台,避免了创建Activity加载布局,绘制等操作。

温启动

没啥要讲的

以上小结:冷启动和热启动

当应用程序的Activity退出的时候,进程没有被杀死,重新启动Acivity的时候,会在原来的进程中创建Activity
,或者如果内存中已有需要启动的Activity将把该Activity移到前台。这样的启动方式叫做热启动。
假如打开Activity的时候没有进程,则去创建一个新的进程,在该进程中创建Activity。这样的启动方式叫做任冷启动。
不管是冷启动还是热启动都会经历空白页。所以我们针对以上进程优化:

1、利用提前展示出来的Window,快速展示出一个界面来代替空白页。
2、避免在Applicaion中做大量的初始化操作,如果能异步就异步。尽快加载主Activity。

5、包体积的优化

1、lint检测无用的代码、资源。
2、包的体积主要在于图片。能自己手写shape就不要用图片
3、适当压缩图片
4、代码混淆,使用proGuard 代码混淆器工具,它包括压缩、优化、混淆等功

6、电量的优化

Battery Historian电量分析工具

1、使用JobScheduler调度任务
在合适的场景调度相应的任务。
2、懒人做法。缓存数据、推迟执行等等

7、相应速度的优化

异步任务

8、线程的优化

使用线程池,因为大量的线程的创建和销毁会给性能带来一定的开销。线程池还能有效的控制并发数,因为如果有大量的线程相互想占资源,会造成堵塞现象。

    原文作者:大熊啊啊啊
    原文地址: https://www.jianshu.com/p/991ae3fab4e3
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞