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

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。
  • 创建时机:
    • 主线程的 Looper 在 ActivityThread.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 自动关联主线程的 Looper 和 MessageQueue。
  • 发送消息:
    • 子线程执行耗时任务后,通过主线程的 Handler 调用 sendMessage() 或 post() 发送消息。
    • Handler 会将自身作为 target 绑定到 Message 上,并将 Message 加入主线程的 MessageQueue。
  • 循环取消息:主线程 Looper 在 loop() 中不断从 MessageQueue 取出 Message(无消息时进入休眠)。
  • 处理消息:Looper 调用 Message 对应的 Handler 的 dispatchMessage(),最终回调到 Handler 的 handleMessage(),完成 UI 更新。

常见使用方式与最佳实践

使用 Handler + Message

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

使用 Handler.post(Runnable)

post() 本质是将 Runnable 封装为 Message 的 callback 字段,其优先级高于 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 就永远无法被回收,从而导致内存泄漏。
kotlin 复制代码
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 回收时,无论内存是否充足,都会被回收。
kotlin 复制代码
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 手中。
kotlin 复制代码
/**
 * 模拟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),最终回调到 Handler 的 handleMessage(),实现线程切换。

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() 插入一个 target 为 null 的特殊消息(屏障)。
  • 作用:当 next() 遇到屏障时,会跳过所有普通的同步消息,只提取并处理后续的异步消息(msg.isAsynchronous() == true)。
  • 应用场景:Android 的 UI 绘制(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 切换到主线程串行执行。
相关推荐
火柴就是我7 小时前
Flutter 项目本地 Maven 引入 AAR 找不到?一次 Gradle 路径问题排查
android
火柴就是我8 小时前
Android 打包报错 25.0.3
android·前端
码上有光8 小时前
Linux进程通信——共享内存、消息队列和信号量
android·linux·运维·共享内存·通信
Android打工仔12 小时前
Kotlin 协程源码解析:DispatchedContinuation 里的 Dispatcher 从哪里来?
android·kotlin
打工仔折腾 AI12 小时前
从BPE到SentencePiece:Transformer分词原理与Python实战对比
android·人工智能·python·深度学习·langchain·transformer·ai agent 实战
Dovis(誓平步青云)13 小时前
几个方案来回选不定?做一个随时切换的候选推荐页
android·java·服务器·开发语言·javascript·数据库·智能化
恋猫de小郭17 小时前
KMP 又改了编译流程, Separate Compilation 禁止了 `commonMain` 的依赖穿透
android·前端·flutter
执明wa17 小时前
Android RecyclerView 实战:点击频道栏切换对应内容
android·开发语言·windows·microsoft·android studio
新鲜势力呀17 小时前
PHP 定时数据同步实战:从第三方接口超时到异步同步 + 增量更新架构优化全过程
android
星间都市山脉19 小时前
RecoveryUI 圆角屏安全边距配置及代码调用链
android·java·linux·windows·ubuntu