先把结论放在前面:Handler 的消息机制由四段构成——Looper 循环取消息、MessageQueue 按时间排序、Message 对象池复用、dispatchMessage 找到目标 Handler 回调。 其中决定"能不能跑起来"的前提只有一个:Handler 与 Looper 必须绑定,而 Looper 与 Thread 通过 ThreadLocal 一对一绑定。 讲不清 ThreadLocal 这一层,整套机制就只是一串 API。
这道题之所以考判断力,是因为它没有高深 API,全是工程细节。同一个知识点,会用的人和只会背的人,差别体现在三处:知不知道 Message 有对象池、说不清 post 与 sendMessage 的差别在哪、遇到"子线程用 Handler"时是知道该 prepare 还是改用 HandlerThread,还是现场编译报错。
从 ThreadLocal 讲起:一切的前提
理解这套机制的起点,是 Looper 的存储方式。每个线程最多只有一个 Looper,而它不是存在线程对象上,是存在 ThreadLocal<Looper> 里。 这意味着三件事:
- 主线程的
Looper是系统准备的(ActivityThread.main里调Looper.prepareMainLooper()),所以主线程new Handler()不需要任何准备。 - 子线程的
Looper必须自己Looper.prepare()+Looper.loop(),这是"为什么子线程 new Handler 会抛异常"的根因——异常信息里那句Can't create handler inside thread ... that has not called Looper.prepare()说的正是这个。 Looper与线程的绑定是隐式的,所以Handler无参构造时去查Looper.myLooper(),查不到就抛异常。换成Handler(Looper.getMainLooper())就绕开了这个查询,直接绑定主线程——这是跨线程投递的标准做法。
MessageQueue 底层是按 when 字段排序的单链表,next() 取出头部(时间最早的)消息。"按时间排序"这一点是延迟消息能实现的基础:postDelayed 并不是"睡 5 秒再执行",而是投递一条 when = 当前 uptime + 5000 的消息,由队列在时间到达时取出。所以主线程卡住 3 秒,延迟消息会连带延迟——它不是定时器,是队列的一部分。
Message 有一个静态的 sPool(sPoolSize 默认 50),obtain() 从池里取、recycle() 放回池。这是性能上的关键点:高频投递时 new Message() 会持续制造垃圾,而 obtain() 的命中率在稳定流里能达到 90% 以上。
三个 API 的差别
Handler.post(Runnable):post内部sendEmptyMessage之后再msg.callback = r,dispatchMessage时优先执行msg.callback,callback != null时直接handleMessage都不会被调用。优点是写法简洁;缺点是无法携带arg1/arg2与obj,也无法sendToTarget给别的 Handler。Handler.sendMessage(Message):标准的对象化消息,可用arg1/arg2/obj/replyTo携带数据,且可以指定目标 Handler(Message.target)。需要跨对象通信、需要返回值(replyTo+Handler组合)时必须用它。Handler.postDelayed(r, delay)与sendMessageDelayed(msg, delay):都设when,但前者同样走 Runnable 路径。注意delay是相对延迟量,when是时间点基准——sendMessageAtTime用后者,跨 Handler 复用延迟逻辑时不容易出错。
最常见的坑是
第一层坑:在没有 Looper 的线程 new Handler(),运行期崩溃。
这是最高频的线上崩溃之一,尤其在子线程里创建了字段成员 Handler(在构造时创建,等于随对象创建而创建,而该对象可能是在子线程被 new 的)。规范是字段 Handler 一律用 Handler(Looper.getMainLooper()) 显式指定 Looper,把"用哪个线程"从隐式变显式;如果确实要在子线程回调,就用 HandlerThread。
第二层坑:高频路径 new Message(),造成内存抖动。
sendMessage(new Message()) 在滑动、动画、埋点上报这类每秒上千次的路径上,会持续制造对象。规范是obtain() + arg1/arg2/obj 携带数据 + 明确 recycle()。但要注意 recycle() 的时机:handleMessage 里回收是安全的,因为 dispatchMessage 在回调返回后不再使用该消息;如果在 postDelayed 的延迟窗口内手动回收,会导致队列里出现已被复用的对象,引发"消息内容被别的任务改写"的诡异 bug。更稳的实践是依赖 dispatchMessage 末尾自动回收(Handler 内部会 msg.recycle()),只在特殊场景手动回收。
第三层坑:延迟消息被误当作定时器,用于精度要求高的场景。
因为 postDelayed 依赖主线程 Looper 存活,主线程卡顿 500ms,延迟消息就晚 500ms。因此心跳轮询、超时判定、对账重试这类业务定时任务不应依赖它,规范是用 ScheduledExecutorService 或专门的调度组件,postDelayed 只用于 UI 层的短延迟(动画衔接、Toast 时长、防抖)。
还有一个更隐蔽的坑:消息堆积导致内存增长与 ANR 风险。 如果生产消息的速度快于消费速度(比如 post 一个耗时 20ms 的任务,却按 60Hz 投递),队列会持续增长,单个 Message 虽小,几万条就是几十 MB。规范是给延迟任务设上限、给高频任务做合并、给队列加监控;此外post 到已销毁的 Activity 会持有它直到执行完,这是典型的 Activity 泄漏路径。
代码里见真章
先看一个规范的主线程 Handler 声明,重点是"隐式变显式":
class FeedViewModel : ViewModel() {
// 规范:字段 Handler 一律显式指定主线程 Looper
// 这样即便 ViewModel 在子线程被创建,回调也保证在主线程
private val mainHandler = Handler(Looper.getMainLooper())
private val pending = mutableMapOf<String, Runnable>()
fun notifyDataChanged(key: String) {
val task = object : Runnable {
override fun run() {
render(key) }
}
// 关键:同 key 的旧任务先取消,避免堆积与乱序
pending.remove(key)?.let {
mainHandler.removeCallbacks(it) }
pending[key] = task
mainHandler.post(task)
}
override fun onCleared() {
// 规范:ViewModel 销毁时清空队列,避免回调已失效页面
mainHandler.removeCallbacksAndMessages(null)
super.onCleared()
}
}
这段代码值得盯三处:第一处,字段 Handler 显式传 Looper.getMainLooper(),把线程归属从隐式约定变成代码事实;第二处,同 key 的旧任务先 removeCallbacks,避免"刷新 10 次执行 10 次"的堆积;第三处,onCleared 里 removeCallbacksAndMessages(null),这是防止页面销毁后仍被回调的关键一步。
再看子线程处理的标准解法,HandlerThread 优于手写 prepare/loop:
// 方案一(推荐):HandlerThread —— 内部已配好 prepare + loop
class DbThread : HandlerThread("db-worker") {
val handler: Handler
init {
start() // 启动线程,内部会执行 Looper.prepare()
handler = Handler(looper) // 绑定该线程的 Looper
}
fun postQuery(sql: String, cb: (Cursor?) -> Unit) {
handler.post {
val cursor = try {
// 耗时 DB 操作在专用线程,不阻塞主线程
db.rawQuery(sql, null)
} catch (e: Exception) {
null
}
// 回调切回主线程更新 UI
mainHandler.post {
cb(cursor) }
}
}
fun shutdown() {
// 规范:quitSafely 会处理已入队消息,quit 会直接丢弃
quitSafely()
}
}
// 方案二:手动 prepare/loop(面试要能写出来,工程上不推荐)
class Worker(name: String) : Thread(name) {
private val looper: Looper
private lateinit var workerHandler: Handler
override fun run() {
Looper.prepare() // 必须在当前线程创建 Looper
workerHandler = Handler(looper) // 绑定当前线程 Looper
synchronized(this) {
ready = true }
notifyAll()
Looper.loop() // 循环取消息,直到 quit
}
fun post(task: Runnable) {
synchronized(this) {
while (!ready) {
wait() } // 避免 post 早于 looper 初始化
}
workerHandler.post(task)
}
fun quit() {
looper.quitSafely() // 安全退出,清理 MessageQueue
}
}
这段代码值得盯两处:第一处,ready 标志 + wait/notifyAll,解决"外部 post 早于线程内 Looper 初始化"这个高频竞态;第二处,quitSafely() 与 quit() 的区别——quitSafely() 会让队列里已排队的消息处理完再退出,quit() 直接丢弃剩余消息,前者是必须知道的细节。
Message 池的正确用法要注意回收时机:
// 规范用法:obtain + 携带数据 + 不手动 recycle(由 dispatchMessage 末尾统一回收)
fun sendState(state: Int, token: String) {
val msg = Message.obtain() // 优先从 sPool 取
msg.what = WHAT_STATE
msg.arg1 = state
msg.obj = token
sendMessage(msg) // 队列持有,dispatch 结束后自动 recycle
}
// 需要跨 Handler 请求返回值:replyTo 是标准做法
fun requestInfo(token: String, callback: (String) -> Unit) {
val reply = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
callback(msg.obj as String)
}
}
val msg = Message.obtain()
msg.what = WHAT_QUERY
msg.obj = token
msg.replyTo = reply // 对方处理完用 replyTo 回结果
remoteHandler.sendMessage(msg)
}
这段代码的关键是"不手动 recycle"——Message 在队列中期间被回收会导致内容被覆盖,这类 bug 表现为"延迟消息偶尔参数变乱",极难复现。
关于主线程卡顿时消息延迟的实测写法,可以用一个可运行的例子说明:
fun demoDelaySemantics() {
val start = SystemClock.uptimeMillis()
val handler = Handler(Looper.getMainLooper())
// 方式一:postDelayed —— 相对当前时刻推迟
handler.postDelayed({
val actualDelay = SystemClock.uptimeMillis() - start
Log.d("Delay", "postDelayed 实际延迟 = $actualDelay ms(不小于 delay)")
}, 500)
// 方式二:sendMessageAtTime —— 指定时间点基准
val at = SystemClock.uptimeMillis() + 500
val msg = Message.obtain().apply {
what = 1 }
handler.sendMessageAtTime(msg, at)
// 关键:postDelayed 期间主线程若被阻塞 800ms,
// 两者都会被推迟到阻塞结束之后才执行,不会中断补跑
heavyBlock(800)
}
private fun heavyBlock(ms: Long) {
// 模拟主线程卡顿
val end = SystemClock.uptimeMillis() + ms
@Suppress("ControlFlowWithEmptyBody")
while (SystemClock.uptimeMillis() < end) {
}
}
这段代码的价值在于把"延迟消息不是定时器"讲成了可运行的证据——主线程卡 800ms 后,两个消息的实际延迟都会超过 800ms,而不是并发补跑。
面试中的经典考点
问:Handler 的执行顺序怎么保证?
答:靠 MessageQueue 的时间排序。 队列是按 msg.when 升序排列的单链表,next() 只取头部。相同 when 的消息按入队顺序执行(插入时会跳过 when 相等的新消息,插到其后)。但要留意:post 的即时消息 when = 0(post 内部是 sendEmptyMessage 后的 when = uptimeMillis 变体),普通延迟消息也能插到它前面——Handler 提供了 sendMessageAtFrontOfQueue(when = 0)来明确插队。回答这题时主动提"插队 API"是加分项。
问:Message 的对象池是怎么工作的?为什么 obtain 比 new 好?
答:Message 内部持有 next 字段,sPool 就是一个以 next 串起来的静态链表,obtain() 从链表头取并把链表头后移,recycle() 把自己挂回链表头。池大小 sPoolSize 默认 50(MESSAGE_POOL_MAX_SIZE 限制上限)。收益有两层:减少 GC 压力(高频路径上是数量级的差距)、减少分配导致的内存抖动。 但要强调前提:池的命中率取决于"obtain 后是否及时 recycle",如果长期持有消息不放,池空了还是得 new。
问:主线程消息为什么会卡顿?怎么定位具体是哪条消息?
答:定位手段有三档。① Looper.setMessageLogging(true)——打印每条消息的 when(入队时间)、target、回调方法名、执行耗时,能直接看到"某条消息执行了 80ms";② StrictMode 的 detectCustomSlowCalls——专门检测自定义 Runnable 的慢执行;③ Choreographer 帧回调 + Perfetto/System Trace 看主线程线程状态。规范是线上 release 包保留 setMessageLogging 或用采样 trace,而不是靠猜。
问:Handler 什么时候会泄漏?怎么排查?
答:成因为内部类 Handler 隐式持有外部类引用 → 外部类持有 Activity/View。两条最常见的路径:① 字段 Handler 里投递了耗时任务,队列持有 Message、Message 持有 Handler、Handler 持有 Activity,即使页面销毁也要等任务执行完才释放;② 内部类 Handler 的 handleMessage 里做长耗时操作。规范是内部 Handler 在 onDestroy 里 removeCallbacksAndMessages(null),且不投递长耗时任务;排查用 LeakCanary 或 heap dump 查 GC Root 到 Activity 的引用链。注意:View.post 也有同样问题,它最终也是 Handler.post。
问:HandlerThread 和 Executor 有什么区别?什么时候用哪个?
答:HandlerThread 是带 Looper 的线程,适合需要"消息队列语义"的场景(有优先序、有延迟、需要串行且可中断的连续任务),并且天然是单线程串行。Executor(线程池)适合任务之间无顺序要求、需要并发、任务之间要复用线程。规范是有依赖顺序的串行任务用 HandlerThread(或单线程 Executor),无依赖的耗时任务用线程池。HandlerThread 的队列同样会堆积,任务过多时依然有内存与延迟风险。
落到项目里怎么做
一条能写进规范的红线:字段 Handler 必须显式传 Looper,销毁时必须清队列。 前者消除"在子线程创建对象"这类隐蔽崩溃,后者消除页面销毁后的悬挂回调。
配套实践三条:① 建立统一 EventBus/Dispatcher 封装,内部收口 Handler 的创建、投递、取消,避免业务代码各建各的;② 埋一条队列长度监控(定期 dump 队列、统计 when 最老消息的延迟),"延迟消息实际延迟"是比"是否 ANR"更早的预警指标;③ 延迟类需求分两类落地——UI 层短延迟走 postDelayed,业务层定时任务走调度器,并在代码注释里写明这条选择理由。
给正在准备面试的你
这题的答法要落到"ThreadLocal 是根、MessageQueue 排序是骨、Message 池是性能"这条主线。推荐这条线:先讲 Looper 与线程的 ThreadLocal 绑定,说明为什么子线程要 prepare 或用 HandlerThread → 讲队列按 when 排序,讲清 postDelayed 不是定时器 → 讲 obtain 池化与"不手动 recycle" → 讲三个坑:子线程 new Handler、高频 new Message、消息堆积与 ANR。能被追问到"postDelayed 的任务会不会在卡顿后补跑"并答出"不会,被推迟",说明真的在主线程卡顿场景上翻过车;能讲清 quit 与 quitSafely 的差别,说明真写过线程管理。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:RecyclerView-多类型与-DiffUtil:列表更新的正确姿势
下一篇预告:Handler-内存泄漏:为什么匿名内部类最危险
有任何问题欢迎在评论区留言交流。