饿汉式Handler 初始化引发的惨案

0. 背景

关于Handler 的使用,在Android开发相关的领域里基本上每一个做Android研发的小伙伴都非常熟悉的了。Handler 是 Android 平台上一个线程间可以方便进行通信的工具,是基于消息队列模型来实现的。通常被用于子线程向主线程发出更新ui的通知。

由于Handler的对象通常都会被子线程持有,所以它的生命周期一般来说都更长,并且使用的时候经常会作为匿名内部类的形式实例化。为了避免由Handler对象持有其他的组件导致的内存泄露问题,在使用Handler 的时候官方推荐把Handler 的成员对象声明为 static 的,这样它就不会直接对外部的其他组件有任何引用了。

1
2
3
4
public class SomeActivity extends Activity {
// 定义一个用于像主线程发送消息的Handler
private static Handler mainHandler = new Handler();
}

官方问了大家能够按照推荐的规范使用静态的 Handler 对象,甚至在Android Studio 的lint代码分析工具中专门有一个规则是来分析Handler是否被“正确”使用了,这个lint 规则就是Handler reference leaks

ab72c185c521a66821ba0de2dc7f7ff5.png
handler-reference-leak.png

1. 问题出现

就是上面的简简单单的一段代码,我们发现也有出问题的可能性。

在上面的代码段中,声明了一个静态的Handler成员并使用了饿汉式的初始化,即:

1
private static Handler mainHandler = new Handler();

但看这段代码的话是没什么问题,并且在大部分的使用中都会表现正常,但是在一些特殊的场景中,则会出现NoClassDefFoundError的错误。

我们来分析下这段代码,由于采用的是静态成员的定义方式,那么这段代码会在相应的类加载的时候执行。再看下 new Handler(); 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 无参构造方法调用的其他构造方法
public Handler() {
this(null, false);
}
// 无参构造方法会调用到这里
public Handler(@Nullable Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                klass.getCanonicalName());
        }
    }
// 注意看这里,mLooper 对象的赋值会使用当前调用线程的Looper对象
// 如果当前调用线程的Looper 为空的话,会直接触发一个Exception的。
    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread " + Thread.currentThread()
                    + " that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

由Handler 的构造方法代码可以看出来,在使用无参的构造方法创建Handler对象的时候,会使用当前调用线程的Looper对象来为Handler做初始化,如果当前调用线程没有Looper对象的话(比如一个普通的子线程是没有Looper对象的),那么这里的对象创建就会失败。

那么,这个地方的Looper会是空的吗?静态代码是在哪个线程中执行的呢?

我们分析一下,静态代码是属于类本身所有的,静态代码的运行时机实在类加载的时候,那么我们猜测静态代码的运行线程,也多半是触发类加载的地方所在的线程

这个结论是经过我验证的,由于验证较为简单这个也不再赘述。

所以,我们可以确定:如果这个类的加载是在一个子线程中触发的(比如在子线程中第一次对该类进行实例化),那么它的静态代码的执行,也会在这个子线程中执行。并且这段代码在执行到Handler的创建时会引发异常。

如果线上直接出现这个错误的话,倒也还好排查,但是现在互联网的公司里面,线上的项目基本都会用UncaughtExceptionHandler来把所有的异常给拦截下来。那么这个错误可能就不会直接暴漏了。Handler的初始化异常,会直接导致它所在的类的加载异常,这样的话,就会触发一个NoClassDefFoundError错误,而这个错误的排查,则要比一个RuntimeException 要麻烦很多了。

2. 问题解决

那么,在确定了问题之后,修改起来也就比较简单了,无非就是以下两种方法:

  1. 创建Handler的时候指定明确的Looper对象,比如Looper.mainLooper();
  2. 把饿汉式的Handler初始化改成懒汉式的,同时在初始化的时候对异常进行捕获处理;

下面使用第二种修改方式,把以上代码改成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SomeActivity extends Activity {
// 定义一个用于像主线程发送消息的Handler
private static Handler mainHandler;
// 在使用 mainHandler 前调用
    private static void checkHandler() {
        if(mainHandler != null) {
            return;
        }
        try{
            mainHandler = new Handler(Looper.mainLooper());
        } catch (RuntimeException e) {
            // handle exception
        }
    }
}

3. 事后分析(马后炮~)

在问题解决之后,我们稍微回顾下这个问题,回过头来总结这个问题就是:

  1. 创建Handler 对象的时候没有指定Looper参数,这样的话在子线程里面就会出现异常;
  2. 由于Handler 对象创建的位置是在类的静态代码中,这会导致类的加载失败;
  3. 同时,线上项目一般会使用全局的 UncaughedExceptionHandler 把所有的线上异常拦截掉,进而导致抛出NoClassDefFoundError的错误。

那么,这里就需要注意的是,创建Handler对象的时候,请务必指定它的Looper参数,除非你很明确它是在单线程环境中执行的。

坚持原创技术分享,您的支持将鼓励我继续创作!