作为一个Android程序员,代码优化是最基本的能力。根据Android官方的建议,编写高效的代码的两个基本准则是:

  • 不要做冗余的工作
  • 尽量避免次数过多的内存分配操作

这里还有还有第三个准则:深入的理解所用语言特性和系统平台的API,具体到Android开发,就是要熟练掌握Java语言,并对Android SDK所提供的API了如指掌。

1. 数据结构的选择

正确的选择合适的数据结构是很重要的,对Java中常见的数据结构例如ArrayList和LinkedList、HashMap和HashSet等,需要做到对它们的联系与区别有较深的理解。这样在编写代码中面临选择时才能作出正确的选择。

在Android开发中使用SparseArray代替HashMap为例说明。SparseArray是Android平台特有的稀疏数组的实现,它是Integer到Object的一个映射,在特定场合可用于代替HashMap<Integer,<E>>,提高性能。主要是因为它避免了对key的自动装箱(int转为Integer类型),它内部则是通过两个数组来进行数据存储的,一个存储key,另外一个存储value,为了优化性能,它内部对数据还采取了压缩的方式来表示稀疏数组的数据,从而节约内存空间,我们从源码中可以看到key和value分别是用数组表示:

private int[] mKeys;
    private Object[] mValues;

SparseArray只能存储key为int类型的数据,同时,SparseArray在存储和读取数据时候,使用的是二分查找法:

// This is Array.binarySearch(),but doesn't do any argument validation.
static int binarySearch(int[] array,int size,int value){
    int lo = 0;
    int hi = size - 1;
    while (lo <= hi) {
        final int mid = (lo + hi) >>> 1;
        final int midVal = array(mid);

        if (midVal < value) {
            lo = mid + 1;
        } else if (midVal > value) {
            hi = mid -1;
        } else {
            return mid; // value found
        }
    }
    return ~lo; // value not present
}

SparseArray 家族目前有以下四类

// 用于替换HashMap<Integer, Boolean> booleanMap = new HashMap<Integer, Boolean>();
SparseBooleanArray booleanArray = new SparseBooleanArray();

// 用于替换HashMap<Integer, Integer> booleanMap = new HashMap<Integer, Integer>();
SparseIntArray intArray = new SparseIntArray();

// 用于替换HashMap<Integer, Long> booleanMap = new HashMap<Integer, Long>();
SparseLongArray longArray = new SparseLongArray();

// 用于替换HashMap<Integer, String> booleanMap = new HashMap<Integer, String>();
SparseArray<String> stringArray = new SparseArray()<String>;

需要注意的几点:

  • SparseArray不是线程安全的
  • 由于要进行二分查找,因此,SparseArray会对插入的数据按照Key值大小顺序插入。
  • SparseArray对删除操作做了优化,它并不会立即删除这个元素,而是通过设置标识位(DELETED)的方式,后面尝试重用。

2. Handler和内部类的正确用法

Android代码中涉及线程间通信的地方经常会使用Handler,典型的代码结构如下:

public class HandlerActivity extends Activity {

    // 可能引入内存泄露的方法
    private final Handler mLeakyHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    };
}

产生内存的原因是什么呢?我们知道,Handler是和Looper以及MessageQueue一起工作的,在Android中,一个应用的启动后,系统默认会创建一个为主线程服务的Looper对象,该Looper对象用于处理主线程的所有Message对象,它的生命周期贯穿于整个应用的生命周期。在主线程中使用的Handler都会默认绑定到这个Looper对象。在主线程中创建Handler对象时,它会立即关联主线程Looper对象的MessageQueue,这时发送到MessageQueue中的Message对象都会持有这个Handler对象的引用,这样在Looper处理消息时才能回调到Handler的handlerMessage方法。因此,如果Message还没有被处理完成,那么Handler对象也就不会被垃圾回收。

在上面代码中,将Handler的实例声明为HandlerActivity类的内部类。而在Java语言中,非静态内部匿名类会持有外部类的一个隐式的引用,这样就可能会导致外部类无法被垃圾回收。因此,最终由于MessageQueue中的Message还没有处理完成,就会持有Handler对象的引用,而非静态的Handler对象会持有外部类HandlerActivity的引用,这个Activity无法被垃圾回收,从而导致内存泄露。

明显引起泄露的例子:

public class HandlerActivity extends Activity {

    // 可能引入内存泄露的方法
    private final Handler mLeakyHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 延时5分钟发送消息
        mLeakyHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                // ...
            }
        },1000 * 60 * 5);
    }
}

当用户打开应用并退出后,在消息发送并处理完成之前,这个Activity是不能被系统回收的。如何解决:

  • 在子线程中使用Handler,这时需要开发者自己创建一个Looper对象,这个Looper对象的生命周期同一般的Java对象,因此这种用法没有问题。
  • 将Handler声明为静态的内部类,前面说过,静态内部类不会持有外部类的引用,因此,也不会引起内存泄露,经典的用法代码如下:
public class HandlerActivity extends Activity {

    // 声明一个静态的Handler内部类,并持有外部类的弱引用
    private static class InnerHandler extends Handler {
        private final WeakReference<HandlerActivity> mActivity;

        public InnerHandler(HandlerActivity activity) {
            mActivity = new WeakReference<HandlerActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            HandlerActivity activity = mActivity.get();
            if (activity != null) {
                // ...
            }
        }
    }

    private final InnerHandler mHandler = new InnerHandler(this);

    // 静态的匿名内部类不会持有外部类的引用
    private static final Runnable sRunnable = new Runnable() {
        @Override
        public void run() {
            // ...
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 延时5分钟发送消息
        mHandler.postDelayed(sRunnable, 1000 * 60 * 5);
    }
}

3. 正确地使用Context

Context是Android开发的程序员第一个接触到的概念,它代表上下文的环境,可以用来实现很多功能的调用。例如获取资源管理器,启动指定应用,获取各种服务等等。虽然应用开发中随处可见Context的使用,但是并不是所有的Context实例都具备相同的功能,在使用上需区别对待,否则可能引入问题。

3.1 Context的种类

根据Context依托的组件以及用途不同,我们可以将Context分为如下几种

  • Application : Android应用中的默认单例类,在Activity或者Service中通过getApplication()可以获取到这个单例,通过context.getApplicationContext()可以获取到应用全局唯一的Context实例
  • Activity/Service:这两个类都是ContextWrapper的子类,在这两个类中可以通过getBaseContext()获取到它们的Context实例,不同的Activity或者Service实例,它们的Context都是独立的,不会复用。
  • BroadcastReceiver:和Activity以及Service不同,BroadcaReceiver本身并不是Context的子类,而是在回调函数onReceive()中由Android框架传入一个Content的实例。系统传入的这个Context实例是经过功能裁剪的,它不能调用registerReceiver()以及bindService()这两个函数。
  • ContentProvider:同样的,ContentProvider也不是Context的子类,但在创建时系统会传入一个Context实例,这样在ContentProvider中可以调用getContext()函数获取。如果ContentProvider和调用者处于相同的应用进程中,那么getContext()将返回应用全局唯一的Context实例。如果是其他进程调用的ContentProvider,那么ContentProvider将持有自身所在进程的Context实例。

3.2 错误使用Context导致的内存泄露

错误的使用Context可能会导致内存泄露,典型的例子是在实现单例模式时使用Context,如下所示:

public class SingleInstance {
    private Context mContext;
    private static SingleInstance sInstance;
    private SingleInstance(Context context){
        mContext = context;
    }
    public static SingleInstance getInstance(Context context){
        if(sInstance == null) {
            sInstance = new SingleInstance(context);
        }
        return sInstance;
    }
}

使用者调用getInstance时传入的Context是一个Activity或者Service的实例,那么在应用退出之前,由于单例一直存在,会导致对应的Activity或者Service被单例引用,从而不会被垃圾回收,Activity或者Service中关联的其他View或者数据结构对象也不会被释放,从而导致内存泄露,正确的做法是使用Application Context,因为它是应用唯一的,而且生命周期跟应用一致的,代码如下:

public class SingleInstance {

    private Context mContext;
    private static SingleInstance sInstance;

    private SingleInstance(Context context){
        mContext = context;
    }
    public static SingleInstance getInstance(Context context){
        if(sInstance == null) {
            sInstance = new SingleInstance(context.getApplicationContext()); // 这一句是关键
        }
        return sInstance;
    }
}

3.3 不同Context的对比

不同组件中的Context能提供的功能不尽相同,总结如下

功能

Application

Activity

Service

BroadcastReceiver

ContentProvider

显示 Dialog

NO

YES

NO

NO

NO

启动 Activity

NO【1】

YES

NO【1】

NO【1】

NO【1】

实现 Layout Inflation

NO【2】

YES

NO【2】

NO 【2】

NO【2】

启动 Service

YES

YES

YES

YES

YES

绑定 Service

YES

YES

YES

YES

NO

发送 Broadcast

YES

YES

YES

YES

YES

注册 Broadcast

YES

YES

YES

YES

NO 【3】

加载资源 Resourse

YES

YES

YES

YES

YES

  • NO【1】标记表示对应的组件并不是真的不可以启动Activity,而是建议不要这么做,因为这些组件会在新的Task中创建Activity,而不是原来的Task中。
  • NO【2】标记也是表示不建议这么做,因为在非Activity中进行Layout Inflation,会使用系统默认的主题,而不是应用中设置的主题。
  • NO【3】标记表示在Android4.2及以上的系统上,如果注册的BroadcastReceiver是null时是可以的,用来获取sticky广播的当前值。

4. 掌握Java的四种引用方式

  • 强引用:Java里面最广泛使用的一种,也是对象默认的引用类型。如果一个对象具有强引用,那么垃圾回收器是不会对它进行回收操作的,当内存空间不足时,Java虚拟机将会抛出OutOfMemoryError错误,这时应用将会终止运行。
  • 软引用:一个对象如果只有软引用,那么当空间内存充足时,垃圾回收器不会对它进行回收操作,只有当内存空间不足时,这个对象才会被回收。软引用可以用来实现内存敏感的高速缓存,如果配合引用队列(ReferenceQueue)使用,当软引用指向的对象被垃圾回收器回收后,Java虚拟机将会把这个软引用加入到与之关联的引用队列中。
  • 弱引用:弱引用是比软引用更弱的一种引用类型,只有弱引用指向的对象的生命周期更短,当垃圾回收器扫描到只具有弱引用的对象时,不论当前内存空间是否不足,都会对弱引用对象进行回收。弱引用也可以和一个引用队列配合使用,当弱引用指向的对象被回收后,Java虚拟机会将这个弱引用加入到与之关联的引用队列中。
  • 虚引用:和软引用和弱引用不同,虚引用并不会对所指向的对象生命周期产生任何影响,也就是对象还是会按照它原来的方式被垃圾回收器回收,虚引用本质上只是一个标记作用,主要用来跟踪对象被垃圾回收的活动,虚引用必须和引用队列配合使用,当对象被垃圾回收时,如果存在虚引用,那么Java虚拟机会将这个虚引用加入到与之关联的引用队列中。

5. 其他代码微优化

5.1 避免创建非必要的对象

对象的创建需要分配内存,对象的销毁需要垃圾回收,这些都会在一定程度上影响到应用性能。因此一般来说,最好是重用对象而不是在每次需要的时候去创建一个功能相同的新对象,特别注意不要在循环中重复创建相同的对象

5.2 对常量使用static final修饰

对于基本数据类型和String类型的常量,建议使用static final修饰,因为final类型的常量会在进入静态dex文件的域初始化部分,这时对基本数据类型和String类型常量的调用不会涉及类的初始化,而是直接调用字面量。

5.3 避免内部的Getter/Serrers

在面向对象编程中,Getter/Serrers的作用主要是对外屏蔽具体的变量定义,从而达到更好的封装性。但如果是在类内部还使用Getter/Serrers函数访问变量的话,会降低访问的速度。根据Android官方文档,在没有JIT(Just In Time)编译器时,直接访问变量的速度是调用Getter方法的3倍;在JIT编译时,直接访问变量的速度是调用Getter方法的7倍。当然,如果你的应用中使用了ProGuard的话,那么ProGuard会对Getter/Serrers进行内联操作,从而达到直接访问的效果。

5.4 代码的重构

代码的重构时一项长期的持之以恒的工作,需要依靠团队中每一个成员来维护代码库的高质量。