本篇来学习《Android开发艺术探索》中的最后一章性能优化部分。安卓作为移动设备,内存和CPU资源都有限,应用程序不可能无限制的使用内存和CPU资源,过多的使用内存资源会导致OOM,而过多的使用CPU资源则可能会造成ANR。因此性能优化显得十分重要。

1.布局优化

核心思想:

  • 减少布局文件的层级,这样安卓绘制时的工作量就会减少,程序的性能也会提高。

具体方法包括:

  • 多嵌套情况下可使用RelativeLayout减少嵌套。
  • 布局层级相同的情况下使用LinearLayout,它比RelativeLayout更高效。
  • 使用<include>标签重用布局、<merge>标签减少层级、<ViewStub>标签懒加载,实现布局重用。
  • <include>标签可以将一个指定的布局文件加载到当前的布局文件中,例如<include layout="@layout/xxx">
  • <merge>一般和<include>一起使用从而减少布局的层级,例如如果当前布局和被包含的布局都采用了线性布局,那么显然被包含的布局中的线性布局是多余的,可以使用<merge>去掉多余的那一层。
  • ViewStub继承自View,宽/高都为0,本身并不参与任何布局的绘制过程,ViewStub 的意义在于按需加载所需的布局文件,在实际开发中,有很多布局文件在正常情况下不会显示,比如网络异常时的界面,这时没有必要在整个界面初始化的时候将其加载进来,通过ViewStub就可以做到在使用的时候再加载,提高了程序初始化时的性能。

2.绘制优化

核心思想:

  • 避免在View.onDraw()中执行大量的操作

具体方法:

  • onDraw()中避免创建新的局部对象,因为onDraw()可能被多次调用而产生大量的临时对象,导致占用过多内存、系统频繁gc,降低了执行效率。
  • onDraw()避免做耗时任务,以及大量循环操作。

3.内存泄漏优化

内存泄漏原因:内存空间使用完毕后没有被回收,就会导致内存泄漏。可能原因包括:

  • 1)静态变量导致的内存泄漏,例如如下代码:
public class SecondActivity extends Activity{
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            SecondActivity.this.finish();
            this.removeMessages(0);
        }
    };
 
    private static Haha haha;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        haha = new Haha();
        mHandler.sendEmptyMessageDelayed(0,2000);
    }
 
    class Haha{
 
    }
}

在dalvik虚拟机中,static变量所指向的内存引用,如果不把它设置为null,GC是永远不会回收这个对象的,上面代码中,SecondActivity实例持有了haha的引用,但这里haha是用static修饰的,虚拟机不会回收haha这个对象,从而导致SecondActivity实例也得不到回收,造成内存泄漏。解决方法可以如下,在Activity销毁时置空static变量:

protected void onDestroy() {
        super.onDestroy();
        if(haha!=null){
            haha = null;
        }
    }
  • 2)单例导致的内存泄漏:单例的静态特性使得单例的生命周期和应用的生命周期一样长, 这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。例如如下代码:
public class UserManager {
    private static UserManager instance;
    private Context context;
    private UserManager(Context context) {
        this.context = context;
    }
    public static UserManager getInstance(Context context) {
        if (instance != null) {
            instance = new UserManager(context);
        }
        return instance;
    }
}

当创建这个单例的时候,由于需要传入一个Context,传入的是Application的Context时没有任何问题,因为单例的生命周期和Application的一样长 ,但是传入的如果是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用,最终导致内存泄漏。所以可以修改单例模式如下:

public class UserManager {
    private static UserManager instance;
    private Context context;
    private UserManager(Context context) {
        this.context = context.getApplicationContext();
    }
    public static UserManager getInstance(Context context) {
        if (instance != null) {
            instance = new UserManager(context);
        }
        return instance;
    }
}

这样不管外面传入什么Context,最终都会使用Applicaton的Context,而我们单例的生命周期和应用的一样长,这样就防止了内存泄漏。内存泄漏最终会导致内存内存溢出(OOM),OOM指的是指程序在申请内存时,没有足够的内存空间供其使用。

  • 3)属性动画导致的内存泄漏:由于没有在onDestroy()中停止无限循环的属性动画,使得View持有了Activity。解决办法:在Activity.onDestroy()中调用Animator.cancel()停止动画。
  • 4)Handler导致的内存泄漏:当Activity销毁后,Handler依然处理延时消息,导致Activity无法被GC释放。解决方法可以采用静态内部类+弱引用。
  • 5)线程导致的内存泄漏:
public class ThreadActivity extends Activity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new MyThread().start();
    }

    private class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            dosomthing();
        }
    }
    private void dosomthing(){

    }
}

这是我们平常使用的写法,假设MyThread的run函数是一个很费时的操作,当我们开启该线程后,将设备的横屏变为了竖屏, 一般情况下当屏幕转换时会重新创建Activity,按照我们的想法,老的Activity应该会被销毁才对,然而事实上并非如此。 由于我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThread的run函数没有结束时, MyThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。解决办法可以将将线程的内部类,改为静态内部类;在线程内部采用弱引用保存Context引用。
此外,AsyncTask/Runnable以匿名内部类的方式存在,会隐式持有对所在Activity的引用。解决办法相同:将AsyncTask和Runnable设为静态内部类或独立出来;在线程内部采用弱引用保存Context引用。

  • 6)资源未关闭导致的内存泄漏:未及时注销资源导致内存泄漏,如BraodcastReceiver、File、Cursor、Stream、Bitmap等。解决方法就是及时关闭即可。

分析内存泄漏可以使用MAT工具,具体参考《Android开发艺术探索》。

4.响应速度优化和ANR日志分析

核心思想:

  • 避免在主线程中做耗时操作。因此需要耗时的操作应放在子线程中去执行。

ANR原因及场景:

  • 当操作在一段时间内系统无法处理时,会在系统层面会弹出ANR对话框。产生ANR可能是因为5s内无响应用户输入事件、10s内未结束BroadcastReceiver、20s内未结束Service。此外,死锁也会导致ANR问题发生。

ANR定位:如果app出现ANR问题时,系统会生成一个traces.txt的文件放在/data/anr下,最新的ANR信息在最开始部分,或者可以使用用DDMS的Traceview定位。

5.ListView和Bitmap优化

  • ListView优化:例如复用ViewHolder并避免在getView中执行耗时操作,还可以尝试开启硬件加速使ListView滑动更加流畅。
  • Bitmap优化:利用BitmapFactory.Options的inSampleSize属性,修改采样率。

6.线程优化

采用线程池,避免存在大量的Thread。

参考: