0. 背景
关于Handler 的使用,在Android开发相关的领域里基本上每一个做Android研发的小伙伴都非常熟悉的了。Handler 是 Android 平台上一个线程间可以方便进行通信的工具,是基于消息队列模型来实现的。通常被用于子线程向主线程发出更新ui的通知。
由于Handler的对象通常都会被子线程持有,所以它的生命周期一般来说都更长,并且使用的时候经常会作为匿名内部类的形式实例化。为了避免由Handler对象持有其他的组件导致的内存泄露问题,在使用Handler 的时候官方推荐把Handler 的成员对象声明为 static 的,这样它就不会直接对外部的其他组件有任何引用了。
|
|
官方问了大家能够按照推荐的规范使用静态的 Handler 对象,甚至在Android Studio 的lint代码分析工具中专门有一个规则是来分析Handler是否被“正确”使用了,这个lint 规则就是Handler reference leaks。
1. 问题出现
就是上面的简简单单的一段代码,我们发现也有出问题的可能性。
在上面的代码段中,声明了一个静态的Handler成员并使用了饿汉式的初始化,即:
但看这段代码的话是没什么问题,并且在大部分的使用中都会表现正常,但是在一些特殊的场景中,则会出现NoClassDefFoundError的错误。
我们来分析下这段代码,由于采用的是静态成员的定义方式,那么这段代码会在相应的类加载的时候执行。再看下 new Handler(); 的实现:
|
|
由Handler 的构造方法代码可以看出来,在使用无参的构造方法创建Handler对象的时候,会使用当前调用线程的Looper对象来为Handler做初始化,如果当前调用线程没有Looper对象的话(比如一个普通的子线程是没有Looper对象的),那么这里的对象创建就会失败。
那么,这个地方的Looper会是空的吗?静态代码是在哪个线程中执行的呢?
我们分析一下,静态代码是属于类本身所有的,静态代码的运行时机实在类加载的时候,那么我们猜测静态代码的运行线程,也多半是触发类加载的地方所在的线程。
这个结论是经过我验证的,由于验证较为简单这个也不再赘述。
所以,我们可以确定:如果这个类的加载是在一个子线程中触发的(比如在子线程中第一次对该类进行实例化),那么它的静态代码的执行,也会在这个子线程中执行。并且这段代码在执行到Handler的创建时会引发异常。
如果线上直接出现这个错误的话,倒也还好排查,但是现在互联网的公司里面,线上的项目基本都会用UncaughtExceptionHandler来把所有的异常给拦截下来。那么这个错误可能就不会直接暴漏了。Handler的初始化异常,会直接导致它所在的类的加载异常,这样的话,就会触发一个NoClassDefFoundError错误,而这个错误的排查,则要比一个RuntimeException 要麻烦很多了。
2. 问题解决
那么,在确定了问题之后,修改起来也就比较简单了,无非就是以下两种方法:
- 创建Handler的时候指定明确的Looper对象,比如Looper.mainLooper();
- 把饿汉式的Handler初始化改成懒汉式的,同时在初始化的时候对异常进行捕获处理;
下面使用第二种修改方式,把以上代码改成:
3. 事后分析(马后炮~)
在问题解决之后,我们稍微回顾下这个问题,回过头来总结这个问题就是:
- 创建Handler 对象的时候没有指定Looper参数,这样的话在子线程里面就会出现异常;
- 由于Handler 对象创建的位置是在类的静态代码中,这会导致类的加载失败;
- 同时,线上项目一般会使用全局的 UncaughedExceptionHandler 把所有的线上异常拦截掉,进而导致抛出NoClassDefFoundError的错误。
那么,这里就需要注意的是,创建Handler对象的时候,请务必指定它的Looper参数,除非你很明确它是在单线程环境中执行的。