Android Handler 必知必会

244 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

在 Android 中有一个非常重要的知识点,那就是 Handler。简单来说,Handler 可以用来做线程切换(使用 handler 可以将消息 post 到主线程。),可以做消息排队(使用 handler 的 sendMessage)。

一、Handler 的原理

提到 Handler 的原理,不得不提到与之相关的 Message、MessageQueue 和 Looper。它的工作机制是:Handler 将 Message 发送到 MessageQueue 中,Looper 不断地从 MessageQueue 中取出 Message 并执行。这在设计模式中属于命令模式。

命令模式:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。

二、Handler 同步屏障

Handler 不仅可以发送同步消息,还可以发送异步消息。通过构造函数中的 async 参数可以设置此 Handler 用于发送同步或异步消息。

public Handler(boolean async) {
    this(null, async);
}

不论同步还是异步消息,最终都会发到 MessageQueue 中,按顺序执行。

顾名思义,同步屏障就是挡住同步消息的屏障。它也是消息的一种。

当页面刷新时,屏幕会发出一条 VSync 信号,通知 CPU 进行绘制计算。View 绘制的起点是 ViewRootImpl 的requestLayout()。这个方法并不会马上开启绘制任务,而是先发送一条同步屏障消息,再添加一个 VSync 信号的监听器。使得 MessageQueue 中,同步屏障之后的同步消息暂停执行,从而使页面刷新的异步消息优先执行,以防止页面卡顿。

原理是当 Looper 从 MessageQueue 中取出消息时,如果发现取出的消息时同步屏障消息(target 为空),则查找这条消息之后的异步消息优先执行。执行完成后,移除同步屏障。

所以同步屏障的作用可总结为:一种给消息增加优先级的机制。目的是减少 Looper 的压力,防止页面掉帧卡顿。

三、子线程中使用 Handler

子线程中使用 Handler 需要先调用 Looper.prepare(),最后调用 Looper.loop()。

thread {
    Looper.prepare()
    Log.e("~~~", "looper: ${Looper.myLooper()}")
    val handler = object : Handler(Looper.myLooper()!!) {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            Log.e("~~~", "get message: ${msg.what}")
        }
    }
    Log.e("~~~", "send message")
    handler.sendEmptyMessage(666)
    Looper.loop()
}

需要注意的是,调用 Looper.loop 之后,程序会在这里挂起。所以如果先调用 Looper.loop,再调用 sendEmptyMessage,则 sendEmptyMessage 永远得不到执行。

或者直接使用主线程的 Looper,将消息发到主线程中。

thread {
    Log.e("~~~", "looper: ${Looper.getMainLooper()}")
    val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            Log.e("~~~", "get message: ${msg.what}")
        }
    }
    Log.e("~~~", "send message")
    handler.sendEmptyMessage(666)
}

四、Handler 可能引起的内存泄漏

使用 Handler 时,Android Studio 常常提示我们需要定义成静态的,以防止内存泄漏。

private val handler = object : Handler(Looper.getMainLooper()) {
    override fun handleMessage(msg: Message) {
        super.handleMessage(msg)
    }
}

原因是这样定义的 Handler 属于匿名内部类,内部类会持有外部类的引用。在 JVM 将内部类转成 class 文件时,会在其构造函数中添加一个参数,这个参数指向外部类。使得我们在内部类中可以直接调用外部类的方法。所以 Handler 会持有 Activity。

而 Message 中有个 target 字段,会指向 Handler。(有一种情况例外:同步屏障的消息 target 为空),所以 Message 持有 Handler 的引用。

Handler 发送延迟消息时,如果页面退出时,消息不被及时移除,则会导致内存泄漏。泄漏的链是:主线程 -> threadlocal -> Looper -> MessageQueue -> Message -> Handler -> Activity

解决方法时声明 Handler 为静态对象,静态对象不会持有外部类的引用。可以将 Context 传入,并声明为弱引用。弱引用会在 GC 时被回收掉。但笔者认为这种方式并不是很好,因为如果这个传入的弱引用被回收掉了,而 Handler 又需要这个 Context 怎么办呢?

2023.04.10 补充:当一个对象只有弱引用时,才会被 GC 回收掉。如果一个弱引用对象同时存在强引用,则 GC 不会回收此弱引用对象。所以这里说的,当 Context 被回收掉后,Handler 还需要此 Context 的情况实际上并不是个问题。只有页面销毁之后,Context 才会被回收,此时 Handler 中的 Context 本身就不应该继续存在。而页面未销毁前,因为 Context 存在强引用,所以Context 的弱引用是不会被回收的,而是一直可用的。

所以我认为较好的解决 handler 内存泄漏的方式是在退出时将延迟消息移除。

内存泄漏的原因是长生命周期的对象持有短生命周期的对象,导致短生命周期的对象无法被及时回收。