Handler泄漏场景
一般情况下使用Handler不会出现内存泄漏,但是页面退出后Handler需要即时清理消息列表,特别是通过postDelay发布的延时任务,这种任务特别容易造成页面(Aty、Fgm)对象泄漏。
看看下面代码
class HandlerTestActivity : AppCompatActivity() {
private val mHandler = Handler(Looper.getMainLooper()) // 主线程Handler
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mHandler.postDelayed({ // 发布一个延时任务,延时10s
val a = "匿名内部类,持有外部类Activity的对象"
Log.d("xiaoma", "a = $a")
}, 10000)
}
}
可以尝试下,打开页面后在10s内关闭Aty,就会出现内存泄漏。咦?这是为什么呢?先不解释,先抛一个大家都认同的:
如上,Handler在postDelay任务前,会先构建一个匿名内部类的任务对象,这种对象会持有外部类的对象。酱紫一来,是不是说明如果有个长生命周期的对象(例如static、单例)引用了这个任务对象,那就相当于持有了Activity的对象?是的话,结果会是什么,会造成什么问题?
很明显,Activity、Fragment这种短生命周期的对象被长生命周期对象持有,是危险的,容易出现内存泄漏,内存泄漏多了,容易频繁触发JVM GC,如果刚好泄漏的Activity很占用内存,甚至容易触发卡顿、OOM。
如何解决这种场景下的内存泄漏呢?本章只提出一种解决办法,其他方案不在讨论范围内哈哈。
解决Handler内存泄漏
在Activity#onDestroy()中清除Handler所有待执行的任务,如下:
class HandlerTestActivity : AppCompatActivity() {
private val mHandler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mHandler.postDelayed({
val a = "匿名内部类,持有外部类Activity的对象"
Log.d("xiaoma", "a = $a")
}, 10000)
}
override fun onDestroy() {
super.onDestroy()
mHandler.removeCallbacksAndMessages(null) // 移除所有的回调和消息
}
}
为什么移除了就可以呢?如何结合JVM和Handler源码进行解释?
JVM GC
GC
(Garbage Collection,Java虚拟机垃圾回收器)
手机内存是有限的,供我们应用可使用的对象更是少之又少,那问题就来了,我们平时打开的Activity、Fragment、以及其他new出来的对象,都是需要占用内存的,当这些对象不需要用到时,这些对象占用的内存是怎么回收以便重新分配给新new的对象的?
这些对象的清理工作都是JVM GC在负责的,而这些需要被清理的对象称为垃圾对象。
这里抛个问题,如果一个对象在GC时没有被识别成垃圾对象,是不是一般就不会被回收?是的话,什么对象不会被识别成垃圾对象?这个问题对我们很重要,关系着我们是否能自己找出一个对象是否可能出现内存泄漏,以及如何处理、规避内存泄漏问题。
这里抛个知识点:
JVM 在GC时是基于“可达性算法”分析对象是否垃圾对象的
即JVM根据一系列称为“GC Roots”的跟对象作为起始节点集,以这些节点为开始,根据引用关系向下搜索,搜索走过的路径称为“引用链”,不在引用链内的对象是不可达的,允许被GC回收(此时并不是非死不可,有兴趣的可以度娘查一下GC对象的自救)
基于此我们可以理解为,当Activity对象给所谓称为"GC Roots"的对象引用了,就会出现在搜索的引用链上,被GC认为是非垃圾对象,从而不被释放。咦?这不就是内存泄漏?
那,能被GC认为是"GC Roots"的对象有哪些?这里提供一下。
GC Root
认清楚GC Root,避免我们的Activity、Fragment被其引用
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 跨代引用:老年代引用新生代
- 所有的同步锁(synchronized关键字)持有的对象
- JVM的内部引用(class对象、异常对象NullPointerException、OutOfMemoryError、系统类加载器)
- JVM内部的JMXBean、JVMTI中注册的回调、本地代码缓存等
- JVM实现中的“临时性”对象
以上佐证了,static修饰的对象是GC 认为的 GC Root,被这种对象引用的对象,不会被GC识别为垃圾对象而回收。
那,这跟上面场景的Handler内存泄漏有什么关系?
为了解答这个问题,我们先进入Handler大致看看,一个延时任务是怎么被保存起来的。
Handler 源码
这里只通过源码,分析延时任务保存后,对象的引用链
延时任务保存
先通过AS下载SDK代码,查看Handler#postDelay()源码
进过上面流程发现,Handler通过构建一个Message对象,包裹Activity中构建的Runnable匿名内部类对象,然后Message对象会放到Handler#mQueue(MessageQueue对象)#mMessages这个队列的相应位置,这里我们称这个队列为MessageQueue,看到这里,我们发现mQueue是非static的,到此并没发现一个GC Roots引用了mActivity。
不着急,我们看看mQueue赋值
这个Looper我们给的是Looper.getMainLooper,也就是说这个MessageQueue就是MainLooper中的,那这个MainLooper在哪?
哈哈,这个MainLooper就是Looper类中的静态对象sMainLooper,在ActivityThread#main(应用在Java层的程序入口)初始化的。这下明了了,GC在触发时基于可达性算法,分析发现引用链:
Looper#sMainLooper -> sMainLooper#mQueue -> mQueue#mMessages -> 包裹目标Runnable对象的Message对象 -> 目标Runnable对象 -> Activity对象
最后发现Activity对象虽然退出了,但GC时通过可达性算法分析后发现不是垃圾对象,不予回收,最终出现内存泄漏。
感谢。