跳到主要内容

Android 消息机制

Android 消息机制把工作送到指定线程,再由该线程的 Looper 取出和分发。Handler 负责提交与处理入口,MessageQueue 保存待处理工作,Looper 驱动取消息与分发过程。代码最终在哪个线程执行,取决于 Handler 绑定的 Looper;发送消息的线程不会因为调用了 post 就被搬到另一个线程。实现细节以 AOSP Android 14 为例。

1. 面试核心​

先记住这几条,再读正文
  • 一个 Looper 对应其所属线程及 MessageQueue,同一线程可以创建多个 Handler,共用这个 Looper 和队列。卡 · 对象关系与分发链路
  • 子线程可以创建 Handler,关键是指定一个可用 Looper,而不是规定 Handler 只能在主线程构造。卡 · 线程归属与 HandlerThread
  • 普通消息按目标处理时间入队,postDelayed 表达最早处理条件,不承诺精确执行时刻。卡 · 时间顺序
  • post 的 Runnable 也封装成消息,最终在目标 Looper 线程执行,直接调用 handleMessage 则只是普通方法调用。卡 · post 与 sendMessage
  • 队列暂时无可处理消息时会等待,Looper 的循环不等于不停消耗 CPU;长回调才会占住后续分发机会。卡 · 等待与唤醒
  • 同步屏障可以让异步消息越过被阻挡的同步消息,这里的异步标记不代表另开一个线程。卡 · 同步屏障
  • 未执行的消息可以间接持有页面,需要清理无效任务并校验结果,而不只是把 Handler 改成静态类。卡 · 生命周期、引用与退出
  • HandlerThread 提供带 Looper 的线程,任务仍串行执行,quitSafely 也不会等待所有未来延迟消息。卡 · 线程归属与 HandlerThread

2. 对象关系与分发链路​

Message 保存一次待分发工作的内容,例如 what、参数、对象或 Runnable。MessageQueue 管理消息的待处理顺序。Looper 在所属线程上不断取出可处理消息。Handler 关联目标 Looper,并提供发送和分发回调。

一次普通 post 的过程是:

消息的 target 指向负责分发的 Handler,因此多个 Handler 共用队列时,Looper 仍能把每条消息交回正确入口。Handler 不为每条消息创建一个线程,也不为自己单独创建一个队列。

Android 14 的 Looper.prepare() 使用 ThreadLocal 保存当前线程对应的 Looper,并拒绝在同一线程重复准备。ThreadLocal 在这里用于按线程找到关联对象,不是把多个线程的消息复制到相互独立的 Handler 字段中。

应用主线程的 Looper 由框架准备并进入循环,普通应用代码不需要再手动 prepare 主 Looper。普通工作线程则不自动拥有 Looper;只有执行了相应准备和循环逻辑,绑定到它的消息才有机会被处理。

3. 线程归属与 HandlerThread​

线程归属由 Handler 构造时关联的 Looper 决定。即使在工作线程中构造 Handler(Looper.getMainLooper()),提交的回调仍交给主线程。

import android.os.Handler
import android.os.Looper
import android.util.Log

fun deliverToMain(text: String) {
val handler = Handler(Looper.getMainLooper())
handler.post {
val onMain = Looper.myLooper() == Looper.getMainLooper()
Log.d("Delivery", "onMain=$onMain, text=$text")
}
}

当回调被正常分发时,onMain 为 true;调用 deliverToMain 的线程不影响这个归属。实际项目通常复用已有 Handler,不需要每次都为同一个 Looper 创建新对象。

如果希望工作在线程自己的消息循环里串行运行,可以使用 HandlerThread。它把线程启动、Looper 准备和循环过程组织在一起:

import android.os.Handler
import android.os.HandlerThread
import android.util.Log

fun runSerialJobs() {
val worker = HandlerThread("serial-worker")
worker.start()
val handler = Handler(worker.looper)
handler.post { Log.d("Jobs", "first") }
handler.post {
Log.d("Jobs", "second")
worker.quitSafely()
}
}

两项普通立即任务按入队顺序在 worker 上处理,第二项完成后请求退出。调用 start 之后读取 worker.looper,HandlerThread 会协调 Looper 准备过程;只创建线程对象而不启动,不能得到一个正在处理消息的线程。

HandlerThread 适合需要线程归属和串行消息处理的工作,不是线程池。某项工作阻塞会拖住后面所有工作,突发任务也可能使队列积压。普通独立后台任务可以按需要使用 Executor 或协程,并分别设计并发度、取消和结果归属,不必统一改造成消息循环。

4. 入队时间与回调选择​

4.1 时间顺序​

sendMessageDelayed、postDelayed 会根据 SystemClock.uptimeMillis() 计算目标时间。这个时钟不包含深度睡眠时间,因此不能将延迟消息当作精确墙钟闹钟,也不能承诺设备休眠期间仍准时执行。

Android 14 的 MessageQueue 使用按 when 排列的消息链表。普通定时消息会找到合适位置:后提交但更早到期的消息可以排到前面;相同目标时间的普通消息按实际入队先后保留顺序。多个线程同时发送时,业务代码不能凭各自开始调用的墙钟顺序,断言最终谁先完成入队。

已提交的目标时间没有特殊屏障或队首操作时的候选顺序
先提交 100,再提交 20,再提交 5020、50、100
两条消息目标时间相同按实际入队顺序处理
目标时间已到,但线程还在处理一个长回调等长回调结束后才有机会处理

postAtFrontOfQueue 等特殊入口还会改变普通排序假设,滥用会让其他消息饥饿。解释某段日志顺序时,先检查是否有队首操作、同步屏障、不同 Handler 的异步标记,以及同一线程中尚未结束的工作。

4.2 post 与 sendMessage​

post 把 Runnable 放到 Message 的 callback 中;sendMessage 通常传递 what 和参数,让 Handler 的消息回调解释。Android 14 的默认 dispatchMessage 依次判断:

  1. 消息有 Runnable callback 时,执行它。
  2. 否则若 Handler 构造时提供了 Callback,先调用它;返回 true 表示已经处理。
  3. 若未被前一步处理,再调用 Handler 的 handleMessage。

因此重写 handleMessage 并不意味着所有 post 的 Runnable 都先经过这个方法。把 handler.handleMessage(message) 直接写进工作线程,也不会完成排队或线程切换,它只是当前线程上的普通方法调用。

消息可以从对象池获得并在分发后回收。发送后就应把它视为交给队列管理,不能继续修改同一 Message,或在框架回收后保留并重复使用。需要把业务数据传给回调时,也要考虑对象是否还会被其他线程同时修改;入队不是给任意共享对象拍快照。

5. 等待、唤醒与同步屏障​

5.1 等待与唤醒​

MessageQueue 没有到期且可处理的消息时,会通过 native 层等待。Android 14 的 native Looper 使用 epoll 等待相关文件描述符事件,并使用 eventfd 等机制唤醒;Java 层同时根据下一条目标时间决定等待时长。

这与 while(true) 中不断执行空操作不同:线程等待期间可以让出 CPU,有新工作或到达等待条件后再继续。队列等待也不是一直持有 Java 队列锁睡眠;发送方可以在受保护的入队操作中插入消息,并在需要时通知 native 唤醒。

每次入队都调用一次唤醒也没有必要。目标线程本来就在运行,或新消息不会改变最近需要处理的时间时,可以省去多余唤醒;若队列在等待而新消息需要更早处理,才需要调整等待。

Looper 本身维持循环不等于 ANR。ANR 涉及特定组件或输入处理超过响应条件;长消息回调、锁等待、同步 Binder 调用和阻塞 I/O 都可能占住主线程。一次栈快照停在 nativePollOnce,可能只是采样时线程空闲,不能单凭这一帧就认定“消息循环卡死”。

5.2 同步屏障​

同步屏障是队列内部用于暂缓同步消息的一种机制。Android 14 中屏障消息没有普通 target;当队首是屏障时,取消息逻辑会继续寻找可通过的异步消息。

异步消息在这里指被标记为可越过同步屏障的消息,不是后台线程。Handler.createAsync 创建的 Handler 可以让其消息避开这类屏障,但回调仍在它绑定的 Looper 线程执行;目标时间没有到达时也不会因此提前执行。

界面调度可以利用屏障,让需要优先完成的渲染相关工作获得机会。普通业务不应通过反射操作隐藏屏障 API,也不应把所有消息都标记为异步来追求“更快”;那会改变原有调度关系,且不能消除回调本身的计算成本。

6. 生命周期、引用与退出​

消息持有链可能是主线程 Looper → MessageQueue → Message → Runnable 或 Handler → Activity。页面已经退出,但一个很久以后执行的 Runnable 仍引用页面时,页面就可能继续保持可达。

静态 Handler 配合弱引用可以打断某一条持有关系,但不能消除 Runnable 直接捕获页面、消息 obj 持有大对象或失效任务继续执行等其他路径。应先明确工作何时不再需要,再清理对应回调。

import android.app.Activity
import android.os.Handler
import android.os.Looper

class NoticeActivity : Activity() {
private val handler = Handler(Looper.getMainLooper())
private val showNotice = Runnable { title = "Ready" }

override fun onStart() {
super.onStart()
handler.postDelayed(showNotice, 5_000)
}

override fun onStop() {
handler.removeCallbacks(showNotice)
super.onStop()
}
}

这个提示只在页面可见时有意义,因此在 onStop 移除同一个 Runnable 实例。不能重新创建一个内容相同的 Runnable,期待它必然匹配原来的回调。实际页面还要检查请求版本与对象身份,防止旧结果更新新的界面。

removeCallbacks 只能移除尚在队列中的相应工作,不能强行中断已经开始执行的回调。扫描删除也有队列遍历成本,不能用频繁全队列扫描代替合理状态设计;removeCallbacksAndMessages(null) 会移除属于该 Handler 的所有待处理内容,使用共享 Handler 时尤其要避免误删别人的任务。

工作 Looper 的 quit() 请求停止,不保证处理完剩余消息;quitSafely() 会处理已经到期的剩余工作,但未来才到期的延迟消息不会都等完。退出后继续投递会失败,post/send 返回成功也只代表入队成功,不保证进程退出或其他变化后仍一定执行。应用主 Looper 不允许通过这些方法随意退出。

7 · 面试题卡片​

本页题目与参考回答

7.1 Handler、Looper、MessageQueue 怎样协作?​

考察重点:对象职责、目标线程与分发链路。

相关内容:对象关系与分发链路。

参考回答

Handler 将工作封装成消息并提交到关联 Looper 的队列,Looper 在自己的线程取出消息,再交给消息的 target Handler 分发。 同一 Looper 可以有多个 Handler,它们共享消息队列。切换的是工作执行位置,发送线程本身不会迁移。

7.2 子线程可以创建 Handler 吗?​

考察重点:构造位置、Looper 可用性与处理线程。

相关内容:线程归属与 HandlerThread。

参考回答

可以,关键是 Handler 关联哪个可用 Looper。 子线程中显式指定主 Looper,回调仍在主线程;若希望回调在该子线程执行,就需要它自己的 Looper 和消息循环,通常可用 HandlerThread。普通线程没有自动准备 Looper,不能无条件使用依赖当前 Looper 的构造方式。

7.3 消息队列怎样按时间组织消息?​

考察重点:when、入队顺序与实际执行条件。

相关内容:时间顺序。

参考回答

Android 14 的普通消息按 when 插入有序链表,相同目标时间按实际入队先后排列。 延迟依据 uptimeMillis,目标时间只表示可处理条件,线程忙、屏障或设备深度睡眠都会影响实际时刻。队首插入等特殊 API 还会改变普通排序前提。

7.4 post 的 Runnable 一定会进入 handleMessage 吗?​

考察重点:dispatchMessage 的回调优先级。

相关内容:post 与 sendMessage。

参考回答

默认分发会先检查 Message 的 Runnable callback,有则直接运行;否则才检查 Handler.Callback,再决定是否调用 handleMessage。 因此不能假定 post 必经 handleMessage。直接调用 handleMessage 也不会产生排队或线程切换。

7.5 Looper 一直循环,为什么不等于主线程一直忙等?​

考察重点:事件等待、CPU 调度与响应超时。

相关内容:等待与唤醒。

参考回答

没有可处理消息时,队列可以进入 native 等待,有事件或需要调整时再唤醒,所以不是不断执行空循环。 Android 14 实现中涉及 epoll 和 eventfd。主线程响应问题要检查长回调、锁或 I/O 等阻塞,单次采样在 nativePollOnce 可能只是空闲等待。

7.6 异步 Handler 是否会在后台线程执行?​

考察重点:同步屏障与线程归属的区别。

相关内容:同步屏障。

参考回答

不会仅因异步标记改变线程,它仍在指定 Looper 的线程执行。 异步消息表示可以越过同步屏障,不代表忽略目标时间,也不减少计算成本。不能把 createAsync 当作启动工作线程或通用性能优化开关。

7.7 Handler 为什么可能导致 Activity 内存泄漏?​

考察重点:待处理消息的强引用链与清理范围。

相关内容:生命周期、引用与退出。

参考回答

队列中的消息可能通过 Handler、Runnable 或 obj 继续持有已退出的 Activity,使其仍然可达。 应在工作失效时清理对应任务,并避免长期对象不必要地持有页面。静态 Handler 或弱引用只能处理特定引用路径,不能代替取消、结果校验或移除 Runnable 捕获。

7.8 HandlerThread 的作用是什么?​

考察重点:线程、Looper 初始化、串行工作与退出。

相关内容:线程归属与 HandlerThread、生命周期、引用与退出。

参考回答

它提供一个准备并运行 Looper 的线程,适合需要固定线程和串行消息处理的工作。 start 之后才能建立可用的执行环境,长任务会阻塞后续任务。退出要由所有者管理;quitSafely 只继续处理已到期工作,不等于等待全部未来延迟任务。

8. 参考资料​

来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明