我们在编码的过程中,如果出现疏忽或错误,造成程序未能释放已经不再使用的内存,就会导致内存泄露,随着泄露内存的增长,最终一定会导致 OOM。
在 JVM 中,对对象的回收 GC 是基于可达性分析。简单来说,就是从 GC Root 出发,被引用的对象均被标记为存活,而没有被引用的对象,则被标记为垃圾,即可以被 GC 回收。
那么如果出现内存泄露,可以理解为就是一个长生命周期的对象,引用了短生命周期的对象,导致短生命周期的对象,在生命周期结束后,仍然得不到回收,最终导致内存泄露。
而 Handler 若是使用不当,就有内存泄露的可能。这种情况,通常发生在对应的 MessageQueue 中,持有了延迟 Message,而这个 Message 又间接持有了 Activity,导致 Activity 回收不即时出现内存泄露。针对这种情况,我们也有了成熟的解决方案,例如使用静态内部类 + WeakReference 解决。
今天给大家介绍另外一个 Handler 体系下,出现的内存泄露的场景。主要由于基于享元模式的 Message Pool 导致 Message 对象的重用,这个 Message 会被 Looper 在循环时,作为局部变量短暂的持有,那么如果这个 Message 又被其他短生命周期的对象持有了,就会导致它的内存泄露。
本文介绍的场景,比较特殊。就是子线程 Looper(HandlerThread)持有的 Message 对象,又被 DialogFragment 持有了,导致这个弹出的 Dialog 的 Activity,在 finish()
后仍得不到回收的情况。
文章内我会在适当的地方加一些补充,仅代表个人理解,文末也追加了一个总结。希望各位阅读愉快,接下来是原文。
某一个 HandlerThread 的 Looper#loop
方法,一直等待 queue#next
方法返回,但是它的 msg
局部变量还引用着上一个循环中已经被放到 Message Pool 中 Message,我们称之为 MessageA。
Q: 咋回事?正常使用 Dialog 和 DialogFragment 也有可能会导致内存泄漏?
A: … 是的,说来话长。
长话短说:
- 某一个 HandlerThread 的
Looper#loop
方法,一直等待queue#next
方法返回,但是它的 msg 局部变量还引用着上一个循环中已经被放到 Message Pool 中 Message,我们称之为 MessageA; - DialogFragment 的 onActivityCreated 方法中,会调用
Dialog#setOnCancelListener
方法,将自身的引用作为 listener 参数传递给该方法; - Dialog 的 setOnCancelListener 方法内部,会尝试从 Message Pool 中获取一个 Message,取出的 Message 刚好是 MessageA,然后将传入的 Listener 实例赋值给
MessageA#obj
; - 外部调用
cancel()
的时候,Dialog 内部会将 MessageA 拷贝一份,我们称它为 MessageB,然后将 MessageB 发送到消息队列中; - DialogFragment 收到 onDestory 回调之后,LeakCanary 开始监听这个 DialogFragment 是否正常被回收,发现这个实例一直存在,dump 内存,分析引用链,报告内存泄漏问题;
墨影补充:DialogFragment 实现在构造时,会从 Message Pool 中获取一个 Message,但在结束时又不会消费这个 Message 对象,会持续持有这个 Message 对象。
具体细节介绍见下文。
一、发现问题
开发的时候, LeakCanary 报告了一个诡异的内存泄漏链。
操作路径:App 显示 DialogFragment 然后点击外部使其消失,之后 LeakCanary 就报了如下问题:
从上面的截图,可以看出:GCRoot 是 HandlerThread 正在执行的方法中的一个局部变量。这个局部变量强引用了一个 Message 对象,message
的 obj
字段又强引用了 NormalDialogFragment ,导致其调用了 onDestory()
方法之后,也无法被回收。
二、分析
注:本文中的「HandlerThread」,泛指那些带有 Looper 并且开启了消息循环(调用了 Looper#loop
)的线程。
DialogFragment 为啥会被一个 Message 的 obj
字段强引用?而且那还是一个被 HandlerThread 引用着的 Message。
回顾一下我们正常显示 DialogFragment 的流程。
- 实例化 DialogFragment;
- 调用
DialogFragment#show
方法让其显示出来;
这个流程中有可能导致 Fragment 被 Message 强引用吗?
- 首先看 DialogFragment 的构造方法是一个空实现。(排除)
- 其次看 DialogFragment show 方法逻辑如下,也是正常的 Fragment 显示逻辑。(排除)
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
难道是 show()
过程的某个步骤中去获取了 Message?
在 DialogFragment#onActivityCreated
方法中,可以看到:
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
s