Handler 泄漏场景、解决方案、深入理解

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被其引用

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 本地方法栈中JNI(即一般说的Native方法)引用的对象
  3. 方法区中类静态属性引用的对象
  4. 方法区中常量引用的对象
  5. 跨代引用:老年代引用新生代
  6. 所有的同步锁(synchronized关键字)持有的对象
  7. JVM的内部引用(class对象、异常对象NullPointerException、OutOfMemoryError、系统类加载器)
  8. JVM内部的JMXBean、JVMTI中注册的回调、本地代码缓存等
  9. JVM实现中的“临时性”对象

以上佐证了,static修饰的对象是GC 认为的 GC Root,被这种对象引用的对象,不会被GC识别为垃圾对象而回收。

那,这跟上面场景的Handler内存泄漏有什么关系?

为了解答这个问题,我们先进入Handler大致看看,一个延时任务是怎么被保存起来的。

Handler 源码

这里只通过源码,分析延时任务保存后,对象的引用链

延时任务保存

先通过AS下载SDK代码,查看Handler#postDelay()源码

SDK 源码下载查看
SDK源码下载查看
Handler#postDelayed(@NonNull Runnable r, long delayMillis)
Handler#postDelayed(@NonNull Runnable r, long delayMillis)
Handler#getPostMessage(Runnable r)
Handler#sendMessageDelayed(@NonNull Message msg, long delayMillis)
Handler#sendMessageDelayed(@NonNull Message msg, long delayMillis)
Handler#enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis)

进过上面流程发现,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时通过可达性算法分析后发现不是垃圾对象,不予回收,最终出现内存泄漏。

感谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值