Framework筑基之Handler消息机制5(应用层)

1 阅读13分钟

Framework筑基之Handler消息机制(应用层)

任务:学习Looper、MessageQueue、Handler的协作原理。

为什么需要消息机制?

  • Android 的 UI 操作不是线程安全的。若多个线程同时修改 UI,可能导致界面异常甚至崩溃。
  • 因此,系统规定只有创建 View 的原始线程(通常是主线程)才能更新 UI。
  • 但耗时操作(如网络请求、文件读写)不能在主线程执行,否则会阻塞 UI,导致 ANR(Application Not Responding)。
  • 于是,开发者通常在子线程中处理耗时任务,再将结果“传递”回主线程更新 UI——这就是消息机制的用武之地。

核心组件详解

Android 消息机制由四大核心组件构成,它们各司其职,形成闭环:

1.Message(消息)

  • 角色:线程间传递的数据载体。
  • 核心字段:包含 what、arg1、arg2、obj 以及 target(指向发送它的 Handler)。
  • 内存优化:通过 Message.obtain() 复用对象,减少内存分配。

2.MessageQueue(消息队列)

  • 角色:负责存储和管理待处理的 Message。
  • 底层结构:并非传统意义上的“队列”,而是一个单链表结构,按时间戳(when 字段)排序。
  • 核心方法:
    • enqueueMessage() 负责按延迟时间插入消息。
    • next() 负责阻塞式获取下一条可执行消息(无消息时进入休眠,节省 CPU)。

3.Looper(消息循环器)

  • 角色:负责从 MessageQueue 中不断“轮询”消息,并分发给对应的 Handler 处理。
  • 线程唯一性:每个线程最多只能有一个 Looper(通过 ThreadLocal 实现线程隔离)。
  • 核心机制:loop() 是一个死循环,不断调用 queue.next() 取消息,取出后通过 msg.target.dispatchMessage(msg) 分发给对应的 Handler
  • 创建时机:
    • 主线程的 LooperActivityThread.main() 中自动创建。
    • 子线程需手动调用 Looper.prepare()Looper.loop()

4.Handler(消息处理器)

  • 角色:消息的“生产者”与“最终处理者”,是线程间通信的接口。
  • 绑定机制:创建 Handler 时必须关联一个 Looper(默认关联当前线程的 Looper),因此 Handler 处理消息的线程由其绑定的 Looper 所在线程决定。
  • 核心作用:
    • 发送消息(sendMessage()、post())和处理消息(重写 handleMessage())。
    • 发送消息时,Handler 会将自身作为 target 绑定到 Message 上。

消息机制协同工作流程

Handler 机制的完整工作流程可以概括为以下几个步骤:

  • 准备阶段:目标线程(通常是主线程)启动时,系统自动调用 Looper.prepareMainLooper() 创建 Looper 对象,Looper 内部会初始化一个 MessageQueue
  • 绑定阶段:开发者创建 Handler 实例,该 Handler 自动关联主线程的 LooperMessageQueue
  • 发送消息:
    • 子线程执行耗时任务后,通过主线程的 Handler 调用 sendMessage()post() 发送消息。
    • Handler 会将自身作为 target 绑定到 Message 上,并将 Message 加入主线程的 MessageQueue
  • 循环取消息:主线程 Looperloop() 中不断从 MessageQueue 取出 Message(无消息时进入休眠)。
  • 处理消息:Looper 调用 Message 对应的 HandlerdispatchMessage(),最终回调到 HandlerhandleMessage(),完成 UI 更新。

常见使用方式与最佳实践

使用 Handler + Message

通过 sendMessage() 发送消息,在 handleMessage() 中根据 msg.what 标识处理不同的 UI 操作。

使用 Handler.post(Runnable)

post() 本质是将 Runnable 封装为 Messagecallback 字段,其优先级高于 handleMessage(),适合简单的 UI 更新任务。

避坑指南:避免内存泄漏

Handler 作为内部类会隐式持有外部类(如 Activity)引用。如果 Activity 被销毁但 Message 还在队列中,Activity 无法回收。

  • 修复方法 1:建议使用静态内部类 + WeakReference 弱引用持有外部类。
  • 修复方法 2:在 onDestroy() 中调用 handler.removeCallbacksAndMessages(null) 清理所有消息和回调。

现代替代方案

  • 协程本质上是 Handler 的高级抽象,底层很多实现仍然用了 Looper
  • 但在绝大多数日常业务场景中,优先使用协程(如 lifecycleScope.launch),少手写 Handler

示例

内存泄漏案例

错误示例

  • 泄漏原理分析:
    • 当用户退出 LeakActivity 时,Activity 本应被 GC 回收。但由于 handler 是它的内部类,持有 Activity 的强引用。
    • 同时,主线程的 MessageQueue 中还有一个延迟 10 秒的 Message,该 Message 的 target 又强引用着 handler。
  • 引用链:
    • 主线程 -> Looper -> MessageQueue -> Message -> Handler -> LeakActivity。
    • 只要 Message 还在队列中,Activity 就永远无法被回收,从而导致内存泄漏。
class LeakActivity : AppCompatActivity() {
    // 错误示范:非静态内部类隐式持有外部类 (LeakActivity) 的强引用
    private val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)

            // 模拟耗时操作后的 UI 更新
            findViewById<TextView>(R.id.tv_text).text = "更新UI"
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_leak)

        // 发送一个延迟 10 秒执行的消息
        handler.sendEmptyMessageDelayed(1, 10000)

        // 模拟用户在 2 秒后按返回键销毁了 Activity
        finish()
    }
}

修正

  • 静态内部类 + WeakReference
    • 使用 WeakReference(弱引用)将外部 Activity 与 Handler 解耦。
    • 弱引用的对象在 GC 回收时,无论内存是否充足,都会被回收。
class SafeActivity : AppCompatActivity() {
    // 正确示范:使用静态内部类 + WeakReference 修复内存泄漏
    private val handler = SafeHandler(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_leak)

        // 发送延迟消息
        handler.sendEmptyMessageDelayed(1, 10000)

        // 此时即使 Activity 被销毁,也不会造成内存泄漏
        finish()
    }

    // 1. 定义静态内部类(不再隐式持有外部类引用)
    private class SafeHandler(activity: SafeActivity) : Handler(Looper.getMainLooper()) {

        // 2. 使用弱引用包裹 Activity
        private val weakRef: WeakReference<SafeActivity> = WeakReference(activity)

        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)

            // 3. 取出弱引用,必须先判空,因为 Activity 可能已经被 GC 回收了
            val activity = weakRef.get()
            if (activity != null && !activity.isFinishing) {
                activity.findViewById<TextView>(R.id.tv_text).text = "安全更新UI"
            }
        }
    }

    // 4. 最佳实践:在 onDestroy 中主动清理消息队列
    override fun onDestroy() {
        super.onDestroy()

        // 移除所有未处理的消息和回调,彻底斩断引用链
        handler.removeCallbacksAndMessages(null)
    }
}

手戳简易版Handler消息循环机制代码

抛弃 Android SDK 的 android.os 包,仅使用 Java 基础语法(链表、锁、线程)来模拟 Message、MessageQueue、Looper 和 Handler 的核心机制。

  • 线程隔离的基石:ThreadLocal

    • 为什么在主线程创建的 Handler,就能把消息发送到主线程?
      • 因为 Looper 内部使用了 ThreadLocal
      • ThreadLocal 为每个线程维护了一个独立的变量副本。
      • 当你在某个线程调用 Looper.prepare() 时,实际上是把这个 Looper 存进了当前线程的局部变量里。
      • 后续 new Handler() 时,它去拿的也是当前线程的 Looper
  • 为什么不会把 CPU 跑满?(阻塞机制)

    • loop() 是一个 while(true) 死循环,为什么它不会让手机发烫、电量耗尽?
      • 秘密在于 MessageQueue.next() 中的 lock.wait()
      • 当队列里没有消息时,线程会主动释放 CPU 资源并进入休眠(阻塞)状态。
      • 只有当子线程调用 enqueueMessage 并触发 lock.notify() 时,主线程才会被操作系统唤醒。
      • 这就是 Android 消息机制极其省电、高效的底层原因。
  • 消息是如何精准找到处理者的?

    • 注意看 sendMessage 方法中的 msg.target = this
    • 发送消息时,Handler 会把自己作为 target 塞进 Message 里。
    • Looper 从队列里取出消息时,它根本不需要知道这个消息是谁发的,只需要调用 msg.target.dispatchMessage(msg),消息就会自动回到当初发送它的那个 Handler 手中。
/**
 * 模拟Message消息载体
 *
 * 使用 data class 自动生成 equals, hashCode, toString 等方法
 */
data class MyMessage(
    var what: Int = 0,
    var obj: Any? = null,
    var target: MyHandler? = null // 核心:持有发送它的 Handler 的引用
)

/**
 * 2.模拟MessageQueue消息队列(使用链表,支持线程安全)
 */
class MyMessageQueue {
    // 使用 LinkedList 作为底层链表,支持高效的头部移除
    private val queue = LinkedList<MyMessage>()

    // 锁对象,用于线程间同步
    // Any为了保持跨平台(Kotlin/Native, Kotlin/JS等)的通用性,并没有声明这些 JVM 特有的底层并发方法(wait()、notify() 和 notifyAll())。
    private val lock = Object() // 使用 Object() 替代 Any(),就能调用底层的 wait() 和 notify()

    // 入队(生产者调用)
    fun enqueueMessage(msg: MyMessage) {
        synchronized(lock) {
            queue.add(msg)
            lock.notify() // 唤醒正在阻塞等待的 Looper
        }
    }

    // 出队(消费者 Looper 调用):没有消息时进入休眠(阻塞)
    fun next(): MyMessage {
        synchronized(lock) {
            // 当队列为空时,释放锁并进入休眠,不消耗 CPU
            while (queue.isEmpty()) {
                lock.wait()
            }
            return queue.removeFirst() // 取出队列头部的消息
        }
    }
}

/**
 * 3. 模拟 Looper(消息循环器)
 */
class MyLooper private constructor() {
    val queue = MyMessageQueue()

    // 使用 ThreadLocal 保证每个线程只能有一个 Looper
    companion object {
        private val sThreadLocal = ThreadLocal<MyLooper>()

        // 准备 Looper(类似 Looper.prepare())
        fun prepare() {
            if (sThreadLocal.get() != null) {
                throw RuntimeException("Only one Looper may be created per thread")
            }
            sThreadLocal.set(MyLooper())
        }

        // 获取当前线程的 Looper
        fun myLooper(): MyLooper? {
            return sThreadLocal.get()
        }

        // 开启死循环,不断从队列取消息(类似 Looper.loop())
        fun loop() {
            val me = myLooper() ?: throw RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.")
            val queue = me.queue

            // 死循环,模拟主线程永不退出
            while (true) {
                // 阻塞式取消息,没消息时线程会在这里休眠
                val msg = queue.next()
                // 取出消息后,交给对应的 Handler 处理
                msg.target?.dispatchMessage(msg)
            }
        }
    }
}

/**
 * 4.模拟 Handler(消息处理器)
 *
 * 使用 open 关键字,允许子类重写 handleMessage
 */
open class MyHandler {
    private val looper: MyLooper
    private val queue: MyMessageQueue

    init {
        // 获取当前线程的 Looper 和队列
        looper = MyLooper.myLooper() ?: throw RuntimeException("Can't create handler inside thread that has not called Looper.prepare()")
        queue = looper.queue
    }

    // 发送消息
    fun sendMessage(msg: MyMessage) {
        msg.target = this // 核心:将 Handler 自身绑定到消息上
        queue.enqueueMessage(msg) // 将消息放入队列
    }

    // 处理消息(子类重写此方法)
    open fun handleMessage(msg: MyMessage) {
        // 默认空实现
    }

    // 分发逻辑(Looper 调用此方法)
    fun dispatchMessage(msg: MyMessage) {
        handleMessage(msg)
    }
}
/**
 * 5.测试运行
 */
fun main() {
    // 1. 准备当前线程的 Looper
    MyLooper.prepare()

    // 2. 创建 Handler(绑定当前线程的 Looper)
    val handler = object : MyHandler() {
        override fun handleMessage(msg: MyMessage) {
            println("[主线程] 收到消息并处理: $msg")
        }
    }

    // 3. 开启消息循环(死循环,会阻塞主线程)
    // 在实际 Android 中,主线程的 loop() 是在 ActivityThread.main() 中开启的
    Thread {
        Thread.sleep(1000)

        // 4. 模拟子线程发送消息
        val msg = MyMessage(
            what = 1,
            obj = "Hello from SubThread"
        )
        handler.sendMessage(msg)

        println("[子线程] 消息已发送,当前线程继续执行其他任务...")
    }.start()

    // 启动循环
    MyLooper.loop()
}

结论

  • 一个线程可以有几个 Looper?几个 Handler?(答:1个 Looper,N个 Handler)

  • 子线程能不能直接更新 UI?为什么?(答:不能,因为子线程没有为 UI 渲染准备 Looper 和 Surface 绘制通道,且 UI 控件不是线程安全的)

  • Looper.loop() 是个死循环,为什么不会导致应用卡死?(答:基于 Linux 的 epoll/IO 多路复用机制,无消息时线程处于休眠状态,不占用 CPU 时间片)

  • sendMessageDelayed 的底层是怎么保证消息按时执行的?(答:计算时间戳,按时间排序插入链表,next() 时根据剩余时间精确休眠)

  • 为什么 MessageQueue 要用链表而不是数组?(答:链表在中间插入和删除的时间复杂度是 O(1),而数组需要移动大量元素。消息队列频繁地在中间插入和移除头部消息,链表效率更高)

小结

Handler 核心机制与底层原理

1.请简述 Handler、Looper、MessageQueue 三者是如何协同工作的?

  • Looper 初始化:
    • 通过 Looper.prepare() 为当前线程创建唯一的 Looper,并在其构造函数中初始化 MessageQueue
    • 主线程的 Looper 是在 ActivityThread.main() 中由系统自动创建的。
  • Handler 绑定:创建 Handler 时,它会通过 ThreadLocal 获取当前线程的 Looper,从而绑定该线程的 MessageQueue
  • 消息发送:子线程通过 Handler 发送消息,Handler 将自身作为 target 绑定到 Message 上,并调用 MessageQueue.enqueueMessage() 按时间戳(when)将消息插入单链表。
  • 消息循环与分发:主线程 Looper 调用 loop() 开启无限循环,不断从 MessageQueue 中取出消息,并调用 msg.target.dispatchMessage(msg),最终回调到 HandlerhandleMessage(),实现线程切换。

2.Looper 的 loop() 是死循环,为什么不会导致主线程卡死或 CPU 满载?

  • 事件驱动模型:主线程的 loop() 虽然是死循环,但它依赖于底层的 Linux epoll 机制(IO 多路复用)。
  • 阻塞与唤醒:当 MessageQueue.next() 发现队列为空或没有到期的消息时,会调用 Native 层的 nativePollOnce(),使主线程进入休眠(阻塞)状态,此时不占用 CPU 时间片。
  • 精准唤醒:
    • 当有新消息入队或延迟时间到达时,Native 层会通过管道(Pipe FD)向 epoll 写入数据,触发内核级事件,精准唤醒主线程继续处理消息。
    • 这种“等待-唤醒”机制保证了低功耗和高响应。

3.sendMessageDelayed 的底层是如何保证消息按时执行的?

  • 时间戳排序:
    • 消息入队时,enqueueMessage 会根据 msg.when(当前时间 + 延迟时间)在单链表中找到合适的位置插入,保证链表按时间递增有序。
  • 精确休眠:
    • MessageQueue.next() 在取消息时,如果发现链表头部的消息还没到执行时间,会计算剩余延迟时间 delay = msg.when - now,并调用 nativePollOnce(ptr, delay) 让线程精确休眠指定的毫秒数。

消息处理与高级特性

1.Handler 处理消息的 dispatchMessage 内部逻辑与优先级是什么?

  • Handler.dispatchMessage() 中,处理消息的优先级严格递减如下:
    • Message 的 callback:如果消息是通过 handler.post(Runnable) 发送的,msg.callback 不为空,直接执行 handleCallback(msg)
    • Handler 的 mCallback:如果创建 Handler 时传入了 Handler.Callback 接口,且其 handleMessage 返回 true,则拦截后续处理。
    • 重写的 handleMessage:如果上述两者都未处理,最终才会调用开发者重写的 handleMessage(msg) 方法。

2.什么是同步屏障(Sync Barrier)?它有什么作用?

  • 机制:通过 MessageQueue.postSyncBarrier() 插入一个 targetnull 的特殊消息(屏障)。
  • 作用:当 next() 遇到屏障时,会跳过所有普通的同步消息,只提取并处理后续的异步消息(msg.isAsynchronous() == true)。
  • 应用场景:AndroidUI 绘制(VSync 信号触发)就是利用同步屏障,确保界面渲染任务能够插队优先执行,保证 60FPS 的流畅度。

3.IdleHandler 是什么?在什么场景下使用?

  • 定义:IdleHandler 是一个接口,当 MessageQueue 处于空闲状态(即当前没有待处理的消息,或者所有消息都是延迟消息且未到时间)时,系统会回调其 queueIdle() 方法。
  • 应用场景:适合执行一些低优先级、非紧急的任务,例如应用启动时的延迟初始化、日志清理、GC 触发等,避免阻塞主线程的关键 UI 渲染。

内存管理与线程安全

1.Handler 造成内存泄漏的原因是什么?如何彻底解决?

  • 原因:
    • 非静态内部类(或匿名内部类)的 Handler 会隐式持有外部类(如 Activity)的强引用。
    • 如果 Activity 销毁时,MessageQueue 中还有未处理完的消息(尤其是延迟消息),Message 强引用 Handler,Handler 强引用 Activity,导致 Activity 无法被 GC 回收。
  • 根治方案:
    • 静态内部类:将 Handler 定义为 static class,切断隐式强引用。
    • 弱引用(WeakReference):在 Handler 内部使用 WeakReference 持有外部类,并在 handleMessage 中判空。
    • 主动清理:在 Activity 的 onDestroy() 中调用 handler.removeCallbacksAndMessages(null),清空消息队列。

2.为什么不允许在子线程中直接更新 UI?

  • 非线程安全:Android 的 UI 控件(View)在设计时没有加锁,如果允许多个线程并发修改 View 的属性,会导致界面状态不可预料。
  • 性能考量:
    • 如果给 UI 操作加锁,会使渲染逻辑变得极其复杂,并大幅降低 UI 的访问和绘制效率。
    • 因此,Android 采用了单线程模型,强制所有 UI 更新必须通过 Handler 切换到主线程串行执行。