进程内通讯:Handler:Handler使用不当带来内存泄露

1:什么是Handler使用不当 ?

表象就是 Handler 采用匿名内部类或者内部类扩展,这两种情况默认持有外部类 Activity引用

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

 1.1:Activity退出时仍有 Thread 在处理,Thrad 引用着 Handler

1.2:  Activity退出的时候 虽然 Thread结束了,但 Message 还在 队列中排队处理或者正在处理,造成间接持有 Handler

代码如下:

// 匿名内部类

override fun onCreate(savedInstanceState: Bundle ?) {

    ....

    val innerHandler : handler = object : Handler(Looper.getMainLooper()) {

        override fun handleMessage(msg : Message) {

            log.d("MainActivity" , "Anonymous inner handler message occured & waht:$
                   
                  {msg.what}
                  ")
        }
    }

}
// 内部类

override  fun onCreate (svaeInstancedState : 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 进入后台,后续又因为内存不足的原因触发了 Destroy ,虚拟机在标记 GC 对象时,发现 Actvitiy 被引用,就是导致 Activity 无法被回收,从而引起内存泄漏。

    2.1  Thread 尚未结束,仍然处于活跃状态

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

          

2.2 Thread虽然已经结束,但是发送的Message还没处理完毕 

       这种情况虽然 Thread已经结束了,但还是有Message 可能还在队列中等待,又正好处于 handleMessage()的回调当中,那么这种情况 Looper 通过MesageQueue持有该 Message, Handler 又作为 target属性被 Message持有 ,Handler又持有 Activity ,那么最终会导致 Looper间接持有 Activity 。

      可以看到 MainLooper 作为 GC Root 对象,引用着 Activity实例,导致引用链仍可到达,那么 MainLooper 为啥能作为 GC Root对象了,因为 MainLooper是 Looper.java 中的静态变量。

      

3:子线程 Looper 会导致内存泄漏嘛?

         针对2.2结论,我们可能会说 子线程的Looper 是非静态的变量,Activity 没有引用到 GC Root对象,所以子线程 Looper不会导致 内存泄漏。

        答案当然是错误的,因为子线程的 Looper是存储在 静态变量 sThreadLocal中的。所以sThreadLocal作为 GC Root对象,从这个角度看也会间接导致 Message无法回收。

        但是,我们阅读源码知道,本质不是因为 ThreadLocal 而是因为 Thread

        

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

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

// TheadLocal.java

public class ThreadLocal<T> {
    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;
    }
}
// TheadLocal.java

public class ThreadLocal<T> {
    ...
    static class ThreadLocalMap {
        private Entry[] table;

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

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

简言之,Looper 的 ThreadLocal 不会导致内存泄漏。

回到 Thread,因其强引用 Looper,所以 Thread 仍然会因为这个因素导致 Message 发生内存泄漏。 比如:向子线程 Handler 发送了内部类写法的 Runnable,当 Activity 结束的时候该 Runnable 尚未抵达或正在执行过程中,那么 Activity 会因为如下的引用链一直可达,即发生内存泄漏的可能。

 3.1 解决方案

       1:  Activity 结束的时候移除子线程 Handler 中所有未处理 Message,比如 Handler#removeCallbacksAndMessages()。这将会切断 MessageQueue 到 Message,以及 Message 到 Runnable 的引用关系
      2:更好的办法是调用 Looper#quit() 或 quitSafely(),它将清空所有的 Message 或未来的 Message,并促使 loop() 轮询的结束。子线程的结束则意味着引用起点 GC Root 不复存在

3.2 达成共识

     

1:管理 Looper 实例的静态属性 sThreadLocal,并不持有实际存放 Looper 的 ThreadLocalMap,而是通过 Thread 去读写 。这使得 sThreadLocal 虽贵为 GC Root,但无法达成到 Looper 的引用链,进而从这条路径上并不能构成内存泄漏!

2:另外ThreadLocal,因其是静态属性,也不会发生 key 被回收 value 被孤立的内存泄漏风险~

3:   最后,由于 Thread 持有 Looper value,从这条路径上来说是存在内存泄漏的可能的
 

4:非内部类的 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 持有。

 

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

5:CallBack接口能解决嘛?

        网上有种说法:创建 Handler 时不覆写 handleMessage(),而是指定 Callback 接口实例,这样子可以避免内存泄漏。理由是这种写法之后 AndroidStudio 就不会再弹出如下的警告:

This Handler class should be static or leaks might occur.

事实上,Callback 实例如果仍然是匿名内部类或内部类的写法,仍然会造成内存泄漏,只是 AS 没弹出这层警告而已。 

private Handler mHandler = new Handler(new Handler.Callback() {
    @Override  
    public boolean handleMessage(Message msg) {  
        return false;  
    }  
});  

比如上面的这种写法,Handler 会持有传递进去的 Callback 实例,而 Callback 作为内部类写法,默认持有外部类 Activity 的引用。 

public class Handler {
    final Callback mCallback;

	public Handler(@NonNull Looper looper, @Nullable Callback callback) {
        this(looper, callback, false);
    }

    public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
        ...
        mCallback = callback;
    }
}

无论是从 Thread 活跃的角度,还是从 Thread 结束但 Message 仍然未执行完的角度来说,都将导致 Activity 仍然可被 GC Root 间接引用而发生内存泄漏的风险。

本质来说和上面的 Runnable 例子是一样的问题:

 

6:如何正确使用 Handler ?

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

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

       6.1  : 一则将强引用 Activity 改为弱引用

      6.2 : 二则及时地切断两大 GC Root 的引用链关系:Main Looper 到 Message,以及结束子线程

     

      6.3  代码示例

        1:将Handler 或 Callback 或 Runnable 定义为静态内部类

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

※:`Kotlin` 里 `class` 中定义 `class` 即为静态内部类,内部类则需要 `inner` 的关键字描述

        2:弱引用 外部实例

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

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

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

      3.1 :Activity 销毁时候,如果子线程任务尚未结束,及时中断 Thread

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

     3.2  : 如果子线程中创建了 Looper 并成为 Looper线程的话,需手动 quit  比如 HandlerThread

override fun onDestroy() {
    ...
    handlerThread.quitSafely()
}

    3.3 : 主线程的 Looper 无法手动 quit 所以 还需要清空主线程中的 Handler 未处理的 Message

override fun onDestroy() {
    ...
    mainHandler.removeCallbacksAndMessages(null)
}

7: 扩展 :哪些对象可以作为 GC Root对象 ? 参考我的另一篇博客  JVM虚拟机系列:GC Root对象是什么 ?哪些对象可作为GC Root对象_hongwen_yul的博客-CSDN博客

总结

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

2:同时在 Activity生命周期结束的时候,及时情况Mesage 终止 Thread 或者 退出 Looper ,以便系统回收 Thread 或 Message

3:  参考 一次性讲清楚 Handler 使用不当导致的内存泄露?_TechMerger的博客-CSDN博客_handler导致内存泄露

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值