您当前的位置: 首页 > 

代码与思维

暂无认证

  • 0浏览

    0关注

    163博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

一次性讲清楚 Handler 可能导致的内存泄漏与解决办法

代码与思维 发布时间:2022-06-24 15:11:23 ,浏览量:0

本文重制和补充了多个示意图和章节,期望能为您一次性讲清楚 Handler 可能导致的内存泄漏和解决办法!

  1. Handler 使用不当?

  2. 为什么会内存泄露?

  3. 子线程 Looper 会导致内存泄露吗?

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

  5. 网传的 Callback 接口真得能解决吗?

  6. 正确使用 Handler?

  7. 结语

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}"
        )
    }
}

2. 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 的实例,这层引用链仍可达:

  • 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 仍然可达。

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

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

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

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

子线程 Looper 会导致内存泄露吗?

为了便于每个线程方便拿到独有的 Looper 实例,Looper 类采用静态的 sThreadLocal 属性管理着各实例。

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

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

会,但本质上不是因为 ThreadLocal,而是因为 Thread。

翻看 ThreadLocal 的源码,您会发现: 目标对象并不存放在 ThreadLocal 中,而是其静态内部类 ThreadLocalMap 中。加上为了线程独有,该 Map 又被 Thread 持有,两者的生命周期等同。

// TheadLocal.java
public class ThreadLocal {
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    // 创建 Map 并放到了 Thread 中
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
}
// Thread.java
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

更进一步的细节是: ThreadLocalMap 中的元素 Entry 采用弱引用持有作为 key 的 ThreadLocal 对象,但作为 value 的目标对象则被强引用着。

这就导致 Thread 间接持有着目标对象,比如本次的 Looper 实例。这样可以确保 Looper 的生命周期和 Thread 保持一致,但 Looper 生命周期过长会有内存泄漏的风险 (当然这不是 Looper 设计者的锅)。

// TheadLocal.java
public class ThreadLocal {
    ...
    static class ThreadLocalMap {
        private Entry[] table;

        static class Entry extends WeakReference(looper, referencedObject) {
        override fun handleMessage(msg: Message) {
            val activity: MainActivity? = referencedObject
            if (activity != null) {
                // ...
            }
        }
    }
}

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

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

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

3. onDestroy 的时候切断引用链关系,纠正生命周期:

  • Activity 销毁的时候,如果子线程任务尚未结束,及时中断 Thread:
override fun onDestroy() {
    ...
    thread.interrupt()
}
  • 如果子线程中创建了 Looper 并成为了 Looper 线程的话,须手动 quit。比如 HandlerThread:
override fun onDestroy() {
    ...
    handlerThread.quitSafely()
}
  • 主线程的 Looper 无法手动 quit,所以还需手动清空主线程中 Handler 未处理的 Message:
override fun onDestroy() {
    ...
    mainHandler.removeCallbacksAndMessages(null)
}

*※1: Message 在执行 recycle() 后会清除其与 Main Handler 的引用关系; **※2: Looper 子线程调用 quit 时会清空 Message,所以无需针对子线程的 Handler 再作 Message 的清空处理了。

结语

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

  • 持有 Activity 实例的 Handler 处理,其生命周期应当和 Activity 保持一致;

  • 如果 Activity 本该销毁了,但异步 Thread 仍然活跃或发送的 Message 尚未处理完毕,将导致 Activity 实例的生命周期被错误地延长;

  • 造成本该回收的 Activity 实例被子线程或 Main Looper 占据而无法及时回收。

简单来讲的正确做法:

  • 使用 Handler 机制的时候,无论是覆写 Handler 的 handleMessage() 方式,还是指定回调的 Callback 方式,以及发送任务的 Runnable 方式,尽量采用静态内部类 + 弱引用,避免其强引用持有 Activity 的实例。

    确保即便错误地延长了生命周期,Activity 也能及时被 GC 回收。

  • 同时在 Activity 结束的时候,及时地清空 Message、终止 Thread 或退出 Looper,以便回收 Thread 或 Message。

    确保能彻底切断 GC Root 抵达 Activity 的引用链。

本文来源于TechMerger ,作者小虾米君 原文链接:https://mp.weixin.qq.com/s/kzExhgdm4fIEk882w8cEkQ

关注
打赏
1665387627
查看更多评论
立即登录/注册

微信扫码登录

0.0399s