在 Android 中,点击事件、绘制、生命周期回调,最终都要在相应线程上执行。Handler 经常被一句话概括为“切回主线程的工具”,但它真正依托的是一套线程内的消息循环。理解这套机制,才能回答:postDelayed 为什么不是准时执行、主线程为什么不会因为 Looper.loop() 而卡死、同步屏障为何能让部分消息“插队”,以及 Handler 内存泄漏究竟是谁持有了谁。
本文以 Android SDK 的公开行为为主,源码流程使用简化伪代码说明;涉及 MessageQueue、ViewRootImpl 等内部实现时,会明确其不是稳定的公开 API。
一、先看全貌:四个角色,一条链路
| Message | 携带 what、obj、时间、Runnable 等任务信息 | 发送后会关联目标 Handler |
| Handler | 把任务送入队列,并在目标线程分发它 | 构造时绑定一个 Looper |
| MessageQueue | 按执行时间管理待处理消息,并在无可执行任务时等待 | 每个 Looper 持有一个队列 |
| Looper | 不断从队列取消息,交给目标 Handler 执行 | 通常一个线程最多准备一个 Looper |
工作线程 / 其他调用方
│ handler.post(runnable) / sendMessage(message)
▼
目标线程的 MessageQueue ── 按 when 排队;无就绪消息时等待
│ next() 取出到期消息
▼
目标线程的 Looper.loop()
│ msg.target.dispatchMessage(msg)
▼
Runnable / Handler.Callback / handleMessage()
│
└─ 分发结束后回收 Message
最重要的一点:**Handler.post() 不创建线程;代码在哪个线程执行,取决于 Handler 绑定的 Looper 属于哪个线程。**同一个 Looper 可以有多个 Handler,它们共用该线程与消息队列。
二、从一个最小例子开始
private val mainHandler = Handler(Looper.getMainLooper())
fun loadPreview() {
backgroundExecutor.execute {
val preview = decodePreview() // 耗时工作在后台线程
mainHandler.post {
previewView.setImageBitmap(preview) // 在主线程更新 UI
}
}
}
Handler(Looper.getMainLooper()) 明确绑定主线程。后台线程调用 post,只是把 Runnable 入主线程队列,post 返回并不代表 UI 已更新。主线程取到消息后才会执行它;如果前面有耗时任务,它还要等。
不要用无参的 Handler() 猜测“当前线程是谁”:这个隐式绑定构造方式已被弃用,也可能在没有 Looper 的线程上失败。需要主线程就写 Looper.getMainLooper();需要工作线程则显式使用其 Looper。
post 和 sendMessage 有何区别?
post(runnable) 会把 Runnable 放进消息的 callback 字段;sendMessage(message) 更适合根据 what 或数据做统一分发。两者最终都走同一条队列与 dispatchMessage 链路。
private const val MSG_REFRESH = 1
val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
if (msg.what == MSG_REFRESH) refreshUi()
}
}
handler.sendEmptyMessage(MSG_REFRESH)
新代码通常更容易用 post {} 表达一次性工作;what 消息适用于确实需要按类型集中处理的场景。不要把大量业务数据塞进 Message.obj 后靠强制类型转换维护协议。
三、Looper 是怎样和线程绑定的?
普通工作线程默认没有消息循环。概念上,手动创建 Looper 的流程是:
val thread = Thread {
Looper.prepare() // 为当前线程创建 Looper 和 MessageQueue
val handler = Handler(Looper.myLooper()!!)
publishHandler(handler) // 示例伪代码:安全地交给其他线程
Looper.loop() // 开始取消息,通常直到 quit
}
thread.start()
这里的 publishHandler 只是说明必须考虑初始化同步,不是 Android API。Looper.prepare() 把 Looper 存入当前线程的 ThreadLocal;同一线程重复调用会报错。Looper.myLooper() 只查当前线程,Looper.getMainLooper() 则获取主线程 Looper。
应用主线程的 Looper 由系统在应用进程启动时准备并运行,开发者不应在主线程再次调用 prepare() 或 loop()。业务代码若需要长期运行的工作消息循环,可用 HandlerThread;多数普通后台任务更适合线程池、协程或其他有明确生命周期的执行器。
HandlerThread 的使用边界
val workerThread = HandlerThread("sensor-serial").apply { start() }
val workerHandler = Handler(workerThread.looper)
workerHandler.post { processSensorBatch() }
// 组件不再需要这个专有线程时:
workerThread.quitSafely()
HandlerThread 是带 Looper 的线程,适合需要串行处理、线程亲和性的工作。quitSafely() 会处理已到期的待处理消息,未来才到期的延迟消息不会保证执行;已经开始运行的回调也不会被强制中断。线程退出后不应继续向它的 Handler 投递任务。若任务需要并行、等待网络或跨进程持久执行,HandlerThread 不是默认选择。
四、一条消息从发送到执行,经历了什么?
以 handler.postDelayed(task, 300) 为例,可把内部流程理解为六步:
在常见 AOSP 实现里,队列以按时间链接的消息节点组织待办工作;next() 计算距离下一条到期消息的等待时间,再通过原生层的 nativePollOnce 等待事件或超时。若新消息需要更早处理,入队侧会通过 nativeWake 唤醒它。这些函数和内部数据结构用于解释“为什么不会忙等”,不属于应用应依赖的稳定接口。
分发顺序可以概括为:
msg.callback(post 的 Runnable)存在? → 运行 Runnable
否则 Handler.Callback.handleMessage(msg) 返回 true? → 已消费
否则 → Handler.handleMessage(msg)
Handler.Callback 与子类重写的 handleMessage 并不是无条件都执行;前者返回 true 就不会再走后者。
为什么 Looper.loop() 不会让主线程一直“忙等”?
“无限循环”只意味着主线程不断接收任务,不意味着 CPU 不断执行空循环。队列没有到期消息时会等待底层事件;新消息入队或等待时间到达后再被唤醒。真正拖慢主线程的是被分发的回调执行太久,或过多任务持续占满队列。
postDelayed(300) 是 300 毫秒后准时执行吗?
不是。它表示最早满足延迟条件后才有资格执行,还必须等前面的工作结束;设备深度休眠期间 uptimeMillis() 不计时,也会影响实际墙上时间。Handler 不是精确计时器,更不能保证进程退出后任务仍执行。需要可持久的延期工作,应选 WorkManager 等合适机制。
sendMessageAtFrontOfQueue() 可以把消息放到队首,但滥用会饿死普通消息;它也无法打断当前正在运行的回调。
五、同步屏障和异步消息:为什么有消息能越过队头?
正常情况下,队列按时间选择消息。Android 内部还存在同步屏障:队列中放入一个没有目标 Handler 的特殊节点。屏障生效时,其后的普通同步消息暂时不能通过,但异步消息可以被选出执行。
队列顺序:同步消息 A → [同步屏障] → 同步消息 B → 异步消息 C
屏障前:A 正常执行
屏障后:B 暂缓,C 可以越过屏障执行
屏障移除:B 才继续获得执行机会
上图是概念示意,不表示所有消息一定按图中的时间先后入队。Android 的视图遍历调度会在内部使用同步屏障,把绘制相关工作安排在合适阶段。同步屏障的插入和移除不是应用可依赖的公开接口,不要通过反射自己操作它。
从 API 28 起可以使用 Handler.createAsync(looper) 创建异步 Handler;它发出的消息不会被同步屏障挡住,但仍在同一个线程执行,不会并行、不会抢占正在运行的任务,也不是解决主线程卡顿的捷径。异步消息与普通同步消息之间的相对顺序不应被当作业务保证。
六、IdleHandler:队列空闲时能做什么?
MessageQueue.IdleHandler 可以在队列没有就绪消息、即将等待时收到回调;队列里有未来才到期的消息,也可能处于这种“当前空闲”状态。queueIdle() 返回 true 表示保留以后继续调用,返回 false 表示移除。
它运行在 Looper 所在线程。如果加在主线程队列,就不能在里面做大量计算、磁盘读写或等待网络;“空闲时调用”不等于“有无限空闲时间”。业务上更稳妥的方式是把真正的耗时工作交给后台执行器,并把可见结果送回主线程。
七、Handler 的内存泄漏到底怎么发生?
常见的引用链不是“Handler 本身神秘地泄漏”,而是队列里的未执行任务延长了对象寿命:
主线程 MessageQueue
└─ 延迟 Message
└─ Runnable / Handler
└─ Activity、View 或大对象
例如匿名内部类、lambda 捕获了 Activity,消息又延迟十分钟。Activity 销毁后,队列仍持有任务,直到任务执行或被移除。把 Handler 改成静态类只能切断一部分引用;Runnable 继续捕获页面对象,仍可能保留它。
private val mainHandler = Handler(Looper.getMainLooper())
private val refreshTask = Runnable { refreshUi() }
fun scheduleRefresh() {
mainHandler.postDelayed(refreshTask, 10_000)
}
override fun onDestroy() {
mainHandler.removeCallbacks(refreshTask)
super.onDestroy()
}
上例中 Handler 和任务由当前组件独占。若 Handler 是共享的,不要随手 removeCallbacksAndMessages(null),否则可能误删别人的工作。removeCallbacks、hasCallbacks 等需要扫描队列,复杂度随队列长度增长;大量任务更适合用独立取消标记、任务所有权或生命周期感知 API 设计。弱引用也不是万能修复:仍需定义任务何时取消、结果过期后如何处理。
八、Handler 与 ANR:该查的是哪段回调?
ANR 的关键不是“队列里消息多”这个数字,而是主线程在关键时刻不能及时响应。一段主线程回调进行同步网络请求、数据库查询、锁等待或大规模计算,后面的输入、绘制及生命周期工作就只能排队。即使每个小任务都不长,持续高频投递也可能让主线程长期繁忙。
排查时可按下面的顺序:
调用 Handler.post 本身成功,只表示任务被接受入队,不证明它在限定时间内完成。post / sendMessage 也可能返回 false,例如目标 Looper 正在退出;需要交付保证的业务不能只依赖内存中的消息队列。
九、Handler、协程和 WorkManager 怎么选?
| 已有后台结果,简单切回主线程 | 主线程 Handler.post 或 Dispatchers.Main | 轻量地安排 UI 线程工作 |
| 页面内异步任务,需随页面取消 | viewModelScope、lifecycleScope 等协程作用域 | 生命周期与取消关系更明确 |
| 长期串行、线程亲和的回调处理 | HandlerThread / 专用 Looper | 同一线程依次处理消息 |
| 多个独立后台任务 | 有界线程池或协程调度器 | 能控制并发,不受单个 Looper 串行限制 |
| 离开页面或进程后仍需完成的延期工作 | WorkManager 等系统调度方案 | 可持久化与重试;普通 Handler 消息不具备这些能力 |
协程没有“取代”底层消息循环。Dispatchers.Main 在 Android 上仍需要把恢复工作安排到主线程执行。选型的重点是业务语义:是否需要结果、取消、持久性、并行性和生命周期所有权。
十、面试高频追问速答
最后把 Handler 机制记成一句话:**调用方把任务交给绑定目标 Looper 的 Handler;队列按规则保存与等待;目标线程的 Looper 取出消息并回调 Handler。**性能与内存问题也沿这条链查:任务什么时候入队、谁持有它、什么时候被执行或取消。
参考资料
- Android API:Handler
- Android API:Looper
- Android API:MessageQueue
- Android API:Message
- Android API:HandlerThread
- Android API:SystemClock
- Android 开源代码:ViewRootImpl



