前面肥柴从浅入深,以Handler的基本工作机制为导入,进一步解析Handler机制的内部底层原理、Android触摸事件原理以及Android Framework层对消息机制的应用。这一篇章作为Handler的最后一个篇章,我们依旧从Handler入手,来谈谈内存泄漏的那些事。
内存泄漏是一个老生常谈的问题,也是面试容易问到的问题,那到底什么是内存泄漏呢?
内存泄漏是指动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元,直到程序结束。
java的优势之一就是内置了垃圾回收器GC,它帮助我们实现了自动化内存管理。但是GC再好,也有老马失前蹄的时候,它不能保证提供一个解决内存泄漏的万无一失的解决方案。而内存泄漏通俗的讲就是一部分内存空间明明已经使用了,却没有引用指向这部分空间,造成这片已经使用的空间无法处理的情况。
当我们尝试写一个Handler的匿名对象的时候,Android Studio会有如下报错,提示我们此写法存在内存泄漏。
那么为何Android Studio会有这种提示?
答案很简单:因为匿名内部类默认会持有外部类Activity的引用,这样当Activity被销毁时,由于被匿名handler对象所持有而不能被释放,Activity所占用的内存就会泄露。
这是我们都知道的答案,那么问题又来了,为啥匿名内部类会持有外部类引用?
这就得从匿名内部类的java编译后的.class文件说起,下面是肥柴把上面的匿名内部Handler类编译后的kotlin字节码代码。我们重点看注释1的地方,可以发现编译器为匿名内部类也单独生成了一份.class文件,而且其类名为Outer$1,并为其构造函数添加了一个参数,这个参数就是Outer类的实例,这就是为什么说匿名内部类默认会持有外部类的引用。
// ================com/happyfatdoge/learningdemo/MainActivity$handler$1.class =================
// class version 52.0 (52)
// access flags 0x31
public final class com/happyfatdoge/learningdemo/MainActivity$handler$1 extends android/os/Handler {
/** 注释1 外部Activity引用变量 */
OUTERCLASS com/happyfatdoge/learningdemo/MainActivity <init> ()V
// access flags 0x1
public handleMessage(Landroid/os/Message;)V
// annotable parameter count: 1 (visible)
// annotable parameter count: 1 (invisible)
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 1
LDC "msg"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 20 L1
RETURN
L2
LOCALVARIABLE this Lcom/happyfatdoge/learningdemo/MainActivity$handler$1; L0 L2 0
LOCALVARIABLE msg Landroid/os/Message; L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x0
<init>()V
L0
LINENUMBER 17 L0
ALOAD 0
L1
LINENUMBER 17 L1
INVOKESPECIAL android/os/Handler.<init> ()V
RETURN
L2
LOCALVARIABLE this Lcom/happyfatdoge/learningdemo/MainActivity$handler$1; L0 L2 0
MAXSTACK = 1
MAXLOCALS = 1
@Lkotlin/Metadata;(mv={1, 5, 1}, k=1, d1={"\u0000\u0017\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000*\u0001\u0000\u0008\n\u0018\u00002\u00020\u0001J\u0010\u0010\u0002\u001a\u00020\u00032\u0006\u0010\u0004\u001a\u00020\u0005H\u0016\u00a8\u0006\u0006"}, d2={"com/happyfatdoge/learningdemo/MainActivity$handler$1", "Landroid/os/Handler;", "handleMessage", "", "msg", "Landroid/os/Message;", "LearningDemo.app.main"})
// access flags 0x19
public final static INNERCLASS com/happyfatdoge/learningdemo/MainActivity$handler$1 null null
// compiled from: MainActivity.kt
}
同理,非静态内部类也是默认持有了外部类引用,而容易造成内存泄漏。
现在我们回到正题,因为Handler这个匿名内部类持有外部Activity的引用,导致Activity销毁时无法释放其内存,那为何Activity被引用了就无法释放Activity的内存呢?
这里就涉及到了JVM的垃圾回收机制:如果GC Root到这个对象是引用链可达的话,那么此时就不能被GC垃圾回收掉此对象的内存。
而JVM内能充当GC Root对象的可以分为几种:1). 虚拟机栈/本地方法栈中JNI中的引用的对象。2). System Class Loader/Boot Class Loader加载的类对象。3). 激活状态的Thread 线程。4). 方法区中的常量引用的对象。5). 方法区里的类静态属性所引用的对象,等等。
肥柴利用内存泄漏工具分析Handler泄漏的真实引用链:Activity -> handler -> message -> queue -> UI线程。用最容易理解的一句话来回答为何会导致无法释放Activity的内存:由于激活状态的Thread线程是GC Root对象,此处对应了UI线程,那么由于UI线程一直处于激活状态,导致了这条引用链上的所有对象都不能被GC内存回收掉。
那根据这个理论,如果我们现在的handler关联的looper是子线程而非UI线程的话,因为随着子线程运行完毕,子线程的Looper和MessageQueue、Handler对象也随之消亡,这条引用链也就断裂了,Activity销毁后就可以被GC回收掉!
既然我们知道了内存泄漏的真实原因,如何处理Handler引发的内存泄漏呢?
最常见的答案便是:将Handler声明为静态内部类,同时为了能够使用到Activity的引用,可以使用弱引用处理Activity引用,避免GC无法释放Activity。
那有更简单更好的方法吗?
其实是有的,这是肥柴也是很多读者最容易忽视的方法,同时也是最简单的方法:就是在Activity onDestroy()时调用handler.removeCallbacksAndMessages(null),这样就把Queue里所有的Message都remove掉了,之前说过Message被Message pool回收掉会reset,因此不会再引用Handler,这条引用链就断掉了。
- - - - - Handler的内存泄露完 - - - - -
至此关于Handler的相关内容也就结束了,后续如有补充在继续扩展本篇章。
一、一切从Android的Handler讲起(一):Handler工作机制
二、一切从Android的Handler讲起(二):Message
三、一切从Android的Handler讲起(三):Looper的唯一性——ThreadLocal
四、一切从Android的Handler讲起(四):Looper消息获取
五、一切从Android的Handler讲起(五):延迟消息实现原理与消息机制的基本原理
六、一切从Android的Handler讲起(六):Android触摸事件基本原理
七、一切从Android的Handler讲起(七):Handler在Android系统框架层的应用
八、一切从Android的Handler讲起(八):Handler的内存泄露
篇章系列参考资料:https://www.zhihu.com/people/jing-shen-ling-xiu-68-79/posts
- - - - - 一切从Android的Handler讲起篇章完 - - - - -