重新理解为什么 Handler 可能导致内存泄露?

总说 Handler 使用不当会导致内存泄露,真正的原因到底是什么?


网上千篇一律的答案貌似没有说到点子上,本文带你重新理解个中细节!


什么是 Handler 使用不当?


先搞清楚什么叫 Handler 使用不当?


一般具备这么几个特征:



  1. Handler 采用匿名内部类内部类扩展,默认持有外部类 Activity 的引用:

// 匿名内部类
override fun onCreate(savedInstanceState: Bundle?) {
...
val innerHandler: Handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
Log.d(
"MainActivity",
"Anonymous inner handler message occurred & what:${msg.what}"
)
}
}
}

// 内部类
override fun onCreate(savedInstanceState: Bundle?) {
...
val innerHandler: Handler = MyHandler(Looper.getMainLooper())
}

inner class MyHandler(looper: Looper): Handler(looper) {
override fun handleMessage(msg: Message) {
Log.d(
"MainActivity",
"Inner handler message occurred & what:\${msg.what}"
)
}
}


  1. Activity 退出的时候 Handler 仍可达,有两种情况:

    • 退出的时候仍有 Thread 在处理中,其引用着 Handler
    • 退出的时候虽然 Thread 结束了,但 Message 尚在队列中排队处理正在处理中,间接持有 Handler



override fun onCreate(savedInstanceState: Bundle?) {
...
val elseThread: Thread = object : Thread() {
override fun run() {
Log.d(
"MainActivity",
"Thread run"
)

sleep(2000L)
innerHandler.sendEmptyMessage(1)
}
}.apply { start() }
}

为什么会内存泄露?


上述的 Thread 在执行的过程中,如果 Activity 进入了后台,后续因为内存不足触发了 destroy。虚拟机在标记 GC 对象的时候,会发生如下两种情形:




  • Thread 尚未结束,处于活跃状态


    活跃的 Thread 作为 GC Root 对象,其持有 Handler 实例,Handler 又默认持有外部类 Activity 的实例,这层引用链仍可达:


    6e9819e69542419287acc82f8c1c8018~tplv-k3u1fbpfcp-watermark.awebp



  • Thread 虽然已结束,但发送的 Message 还未处理完毕


    Thread 发送的 Message 可能还在队列中等待,又或者正好处于 handleMessage() 的回调当中。此刻 Looper 通过 MessagQueue 持有该 Message,Handler 又作为 target 属性被 Message 持有,Handler 又持有 Activity,最终导致 Looper 间接持有 Activity。


    大家可能没有注意到主线程的 Main Looper 是不同于其他线程的 Looper 的。


    为了能够让任意线程方便取得主线程的 Looper 实例,Looper 将其定义为了静态属性 sMainLooper


    public final class Looper {
    private static Looper sMainLooper; // guarded by Looper.class
    ...
    public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
    sMainLooper = myLooper();
    }
    }
    }

    静态属性也是 GC Root 对象,其通过上述的应用链导致 Activity 仍然可达。


    dee2014158f7488a8d812f4b44e87b8b~tplv-k3u1fbpfcp-watermark.awebp



这两种情形都将导致 Activity 实例将无法被正确地标记,直到 Thread 结束 且 Message 被处理完毕。在此之前 Activity 实例将得不到回收。


其他线程的 Looper 会导致内存泄露吗?


为了便于每个线程方便拿到自己的 Looper 实例,Looper 采用静态的 sThreadLocal 属性缓存了各 Looper 实例。


public final class Looper {
static final ThreadLocal sThreadLocal = new ThreadLocal();
...
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
}

那 sThreadLocal 作为静态属性也是 GC Root 对象,从这个角度讲会不会也间接导致 Message 无法回收呢?


答案是不会,因为 ThreadLocal 内部的 Map 采用弱引用持有 Looper 对象,不会导致 Looper 及引用链实例无法被回收。


public class ThreadLocal {
...
static class ThreadLocalMap {
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
}
}
<?>

内部类 Thread 也会导致 Activity 无法回收吧?


为了侧重阐述 Handler 导致的内存泄漏,并没有针对 Thread 直接产生的引用链作说明。


上面的代码示例中 Thread 也采用了匿名内部类形式,其当然也持有 Activity 实例。从这点上来说,尚未结束的 Thread 会直接占据 Acitvity 实例,这也是导致 Activity 内存泄露的一条引用链,需要留意!


如何正确使用 Handler?


GC 标记的时候 Thread 已结束并且 Message 已被处理的条件一旦没有满足,Activity 的生命周期就将被错误地延长,继而引发内存泄露!


那如何避免这种情况的发生呢?针对上面的特征,其实应该已经有了答案。



  1. 将 Handler 定义为静态内部类

private class MainHandler(looper: Looper?, referencedObject: MainActivity?) :
WeakReferenceHandler(looper, referencedObject) {
override fun handleMessage(msg: Message) {
val activity: MainActivity? = referencedObject
if (activity != null) {
// ...
}
}
}
?>

另外还需要弱引用外部类的实例:


open class WeakReferenceHandler(looper: Looper?, referencedObject: T) : Handler(looper!!) {
private val mReference: WeakReference = WeakReference(referencedObject)

protected val referencedObject: T?
protected get() = mReference.get()
}



  1. onDestroy 的时候纠正生命周期




    • Activity 销毁的时候,如果异步任务尚未结束停止 Thread:


      override fun onDestroy() {
      super.onDestroy()
      thread.interrupt()
      }



    • 同时还要将 Handler 未处理的 Message 及时移除,Message 执行 recycle() 时将重置其与和 Handler 的关系:


      override fun onDestroy() {
      super.onDestroy()
      thread.interrupt()
      handler.removeCallbacksAndMessages(null)
      }





非内部类的 Handler 会内存泄露吗?


上面说过匿名内部类或内部类是 Handler 造成内存泄漏的一个特征,那如果 Handler 不采用内部类的写法,会造成泄露吗?


比如这样:


override fun onCreate(...) {
Handler(Looper.getMainLooper()).apply {
object : Thread() {
override fun run() {
sleep(2000L)
post {
// Update ui
}
}
}.apply { start() }
}
}

仍然可能造成内存泄漏。


虽然 Handler 不是内部类,但 post 的 Runnable 也是内部类,其同样会持有 Activity 的实例。另外,post 到 Handler 的 Runnable 最终会作为 callback 属性被 Message 持有。


7772b7e39b3246a883f37bea38c7db00~tplv-k3u1fbpfcp-watermark.awebp

基于这两个表现,即便 Handler 不是内部类了,但因为 Runnable 是内部类,同样会发生 Activity 被 Thread 或 Main Looper 不当持有的风险。


结语


回顾一下本文的几个要点:



  • 持有 Activity 实例的内名内部类或内部类的生命周期应当和 Activity 保持一致
  • 如果 Activity 本该销毁了,但异步任务仍然活跃或通过 Handler 发送的 Message 尚未处理完毕,将使得内部类实例的生命周期被错误地延长
  • 造成本该回收的 Activity 实例被别的 ThreadMain Looper 占据而无法及时回收
  • 记得持有 Activity 尽量采用静态内部类 + 弱引用的写法,另外在 Activity 销毁的时候及时地终止 Thread 或清空 Message

作者:TechMerger
链接:https://juejin.cn/post/7017466392165220360
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册