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。
- 因为
- 为什么在主线程创建的 Handler,就能把消息发送到主线程?
-
为什么不会把 CPU 跑满?(阻塞机制)
- loop() 是一个 while(true) 死循环,为什么它不会让手机发烫、电量耗尽?
- 秘密在于
MessageQueue.next()中的lock.wait()。 - 当队列里没有消息时,线程会主动释放
CPU资源并进入休眠(阻塞)状态。 - 只有当子线程调用
enqueueMessage并触发lock.notify()时,主线程才会被操作系统唤醒。 - 这就是
Android消息机制极其省电、高效的底层原因。
- 秘密在于
- loop() 是一个 while(true) 死循环,为什么它不会让手机发烫、电量耗尽?
-
消息是如何精准找到处理者的?
- 注意看
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)方法。
- Message 的 callback:如果消息是通过
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 切换到主线程串行执行。