Appearance
05 Handler-Looper-MessageQueue:一个线程的事件泵
本页导读:Android 的线程哲学是「一个线程一个队列,事件排队进,代码排队出」。Looper 是泵,MessageQueue 是管道,Handler 是出水口。本页把三件套从 Java 讲到 native epoll,讲清同步屏障为什么存在、IdleHandler 何时被喂、
postDelayed的时间基准到底是谁,最后给出主线程初始化链与泄漏案例集。基线 Android 16(API 36),这套机制自 Android 1.0 稳如老狗。
一、三件套分工:泵、队列、出水口
- Looper:一条线程至多一个(
ThreadLocal保管),loop()是个死循环:queue.next()取消息 →msg.target.dispatchMessage(msg); - MessageQueue:名字有误导,它不是队列而是按
when排序的单链表,插入是 O(n) 遍历,到期判断靠时间戳; - Handler:消息的收发器,
post把this写进msg.target,dispatchMessage按「内建 Runnable > HandlerMessage 回调 > handleMessage 覆写」三级分发。
java
// frameworks/base/core/java/android/os/Looper.java(示意骨架)
public static void loop() {
for (;;) {
Message msg = me.mQueue.next(); // 没消息就睡,有定时就定时睡
if (msg != null) {
msg.target.dispatchMessage(msg);
msg.recycleUnchecked();
}
}
}二、native 底座:epoll + eventfd
Java 的 MessageQueue.next() 在无事可做时调用 nativePollOnce(ptr, timeoutMillis),native 侧(frameworks/base/core/jni/android_os_MessageQueue.cpp + NativeMessageQueue)把唤醒做成 epoll + eventfd(早年用 pipe 自管道,后改 eventfd,以 AOSP 为准):
next()算出下一条消息的等待时长timeout,交epoll_wait定时睡眠;enqueueMessage判定「新消息插到了队头」时调nativeWake(ptr),向 eventfd 写 1 个字节,唤醒睡眠中的epoll_wait;- 除了 eventfd,epoll 还挂着其他 fd——这正是
FileObserver(ContentObserver 监听文件的底层)的寄生点(第 08 页)。
两个常见误解一次说清:postDelayed 不产生闹钟,定时完全靠 epoll_wait 的 timeout;消息不按「插入序」而按 when 序执行,postDelayed(0) 与 post 几乎同时到。
顺带记住三个入口函数的真实归宿:post(Runnable) 造一条 callback 消息;sendMessage(Message) 直接用你给的 what/arg;postAtTime(r, when) 把绝对时刻写进 when 后按序插入链表——三者最终都是同一张链表上的一次插入。
native 与 Java 两层队列是一体两面:Java 侧只管排序与唤醒决策,native 侧只管睡与醒;dumpsys activity 或线程栈里看到主线程停在 nativePollOnce,含义是「队列空,正常睡觉」——这是最常见的「假现场」,别在漏帧分析里把它当凶手。
三、时间基准:uptimeMillis 与 atTime 的相对论
postDelayed(r, 1000) 的真实语义是 sendMessageAtTime(r, SystemClock.uptimeMillis() + 1000):
uptimeMillis:开机以来经过的时间,不含深度睡眠——所以「延时 1 小时」遇休眠会顺延;elapsedRealtime:含深度睡眠的墙钟流逝,AlarmManager 的 RTC 闹钟用它;- Handler 的世界只认 uptimeMillis;拿
System.currentTimeMillis()算atTime参数是事故现场(改系统时间立刻失效/提前)。
四、同步屏障:给渲染消息开 VIP 通道
postSyncBarrier() 向队列插入一个 target == null 的无主消息,此后 next() 会跳过屏障,只放行异步消息(msg.isAsynchronous()),普通同步消息全部压后。
谁在用?Choreographer:每个 vsync 帧回调都以异步消息投递。渲染流水线对延迟极度敏感,如果 vsync 回调排在某个大循环任务后面,整屏幕都会抖;屏障保证帧事件永远插队。用完的 removeSyncBarrier(token) 千万别漏——漏了之后这条队列再也不会执行同步消息,是最恶心的死法之一。
五、IdleHandler:捡漏的优雅姿势
next() 在「当前没有到期消息」的空档执行 QueueIdleHandler.queueIdle();返回 false 自动移除,返回 true 留下次机会。
java
// 典型用法示意:首帧后做重初始化
Looper.myQueue().addIdleHandler(() -> {
heavyInitAfterFirstFrame();
return false; // 干完一次就走
});系统里的名场面:Activity 启动收尾后的部分统计上报、View 树缓存清理类任务。注意两个坑:idle 不代表 CPU 空闲(只是队列没活);handler 里再 post 消息会让下一个 idle 推迟——「idle 饿死」在高帧率动画期真实发生。
另一个实用角度:idle 时机 = 「首帧渲染完成且队列短暂空」,比 onCreate 晚、比用户交互早,是延迟初始化的黄金窗口;但要防重入,多个模块挤同一 idle 队列时要各自限量,否则首帧后的第一个滑动帧会被集体拖死。
六、主线程初始化链:App 的第一口气
text
app_process(zygote fork,第 02 页)
→ ActivityThread.main() # core/java/android/app/ActivityThread.java
→ Looper.prepareMainLooper() # 主线程建 Looper
→ new ActivityThread().attach(false) # 绑定进程名、ApplicationThread
→ AMS.attachApplication(binder) # 反向 Binder,第 06 页
→ bindApplication 下发 Application/Provider/首个 Activity 消息
→ Looper.loop() # 主线程永不返回,返回即异常退出Looper.loop() 返回即进程完蛋——这是「为什么主线程要死循环」的官方答案;ANR 监视、生命周期、输入、渲染回调全部共用这一条队列,主线程的忙闲决定 App 的体感(第 06 页 ANR 归因回到这里)。
主线程 mH 里能看到的典型消息(名字以 AOSP 为准):BIND_APPLICATION、LAUNCH_ACTIVITY(旧版本;新架构为 EXECUTE_TRANSACTION 统一入口)、SERVICE_ARGS、RECEIVER、STOP_SERVICE、CONFIGURATION_CHANGED——四大组件在应用侧的入口全部出自这一个 Handler。
七、quit 与 quitSafely,以及子线程模板
quit() 立即清场,丢弃未到期消息;quitSafely() 放行所有已到期消息(API 18 起)。子线程标准模板:
java
// 手写一个可关闭的工作线程(等价 HandlerThread)
Thread worker = new Thread(() -> {
Looper.prepare();
Handler h = new Handler(Looper.myLooper()) { /* handleMessage */ };
Looper.loop();
});
worker.start();
// 结束时:new Handler(worker.getLooper()).getLooper().quitSafely()生产环境直接用 HandlerThread,或干脆用协程/Executor——但要知道协程的 Dispatchers.Main 背后就是这条主队列。
八、泄漏与卡顿案例集
泄漏三连(LeakCanary 的靶子,也是真实事故):
java
// 反面教材
class LeakActivity extends Activity {
private final Handler handler = new Handler() { ... }; // 隐式持 Activity
onCreate() { handler.postDelayed(heavyTask, 60_000); } // 消息在队列里排队 60s
// Activity 销毁,但「消息→Runnable→外部类→Activity」链仍被队列持着
}
// 正解三板斧:静态内部类 + WeakReference;onDestroy 里 removeCallbacksAndMessages(null);
// 或干脆把 Handler 挂在有明确生命周期的对象上(Lifecycle 感知)卡顿定位:主线程队列排长队用 adb shell dumpsys activity 里的队列快照或 MessageQueue.IdleHandler/Logging 钩子(Looper.getMainLooper().setMessageLogging,BlockCanary 原理:记录 dispatchMessage 前后时间差);perfetto 的 Looper slice 直接画出每条队列的入队/执行;StrictMode 的磁盘/网络策略则把「主线程干了不该干的活」在 debug 包当场炸出来。
其他高频坑:
Looper.prepare()在已有 Looper 的线程重复调用抛Only one Looper per thread;- 跨进程回调的 Stub 方法运行在 Binder 线程而非主线程——UI 操作必须再 post 回主队列;
Message复用前未recycle/obtain会污染消息池,轻则串数据重则直接崩;- 子线程 quit 后继续 post 会得到
sending message when looper quit警告并静默丢弃,任务没了声音——给后台线程发消息前先判isQuit(或管好生命周期); - 一条队列里所有消息串行:给队列「让路」的正道是把重活拆出队列,而不是堆优先级——Handler 没有优先级概念。
九、Message 解剖:一条消息里都写了什么
| 字段 | 作用 | 一句话提醒 |
|---|---|---|
what | 业务分支码 | 枚举常量即可,别拿它存数据 |
target | 发送它的 Handler | 屏障消息就是 target==null;分发前不可改 |
when | uptimeMillis 绝对时刻 | 排序键,postDelayed 换算后就存在这里 |
callback | post(Runnable) 的 Runnable | 优先于 handleMessage 执行 |
obj/arg1/arg2/data | 传参 | data 是 Bundle,重量级内容另走通道(第 04 页 1MB 教训同样适用) |
| 回收池 | obtain/recycle | 循环里 new Message 是 GC 抖动源;复用前确认已从队列取出 |
十、Handler 周边生态速览
- HandlerExecutor / HandlerThread:system_server 内部把 main/io/bg/ui/display 等命名线程包成执行器族,服务指定线程跑重活(第 03 页 Watchdog 监的就是这批);应用侧用 HandlerThread 或协程等价物;
- Choreographer:独立于主队列的帧调度器,靠同步屏障拿 VIP(第四节);
postFrameCallback的回调仍在主线程; - IntentService:历史上的「后台串行任务 + 自动停 自」包装,API 30 废弃,后继者是 JobIntentService/WorkManager/协程;
- 协程 Dispatchers.Main:本质还是往主线程 MessageQueue 投递;挂起函数不会让 Looper 变并发,主线程仍是一条队列;
- AsyncTask:已删除级废弃,但它的「后台干活 + 主线程回调」双 Handler 模型仍是理解很多老框架代码的钥匙。
十一、调试工具箱速查
| 工具/手段 | 看什么 | 备注 |
|---|---|---|
kill -3 <pid> | 全线程 Java 栈 | 主线程当前在执行哪条消息一目了然 |
| perfetto(sched + appTrace) | 队列 slice、入队/执行配对 | 应用侧需在代码里开 Trace 打点效果最佳 |
Looper.setMessageLogging | 每条消息的 dispatch 耗时 | 线上可做成采样版 BlockCanary |
| StrictMode | 主线程磁盘/网络/慢 UI | debug 包策略拉满,release 不开 |
| LeakCanary | Handler/回调引发的对象驻留 | 定位「谁持有 Activity」类问题 |
| dumpsys activity 的 proc 段 | 主线程堆栈快照 | ANR 报告同源,适合事后复盘 |
十二、版本注记
- API 1~17:唤醒机制为 pipe 自管道;
quit()一统江湖。 - API 18(Android 4.3):新增
quitSafely();MessageQueue重构。 - API 21(Android 5):pipe→eventfd 的 native 唤醒改造;ART 下消息对象复用策略调整。
- API 22(Android 5.1):
Message.setAsynchronous()公开;屏障从 Choreographer 私有约定走向 API 化(内部 API 灰名单)。 - API 28~29(Android 9~10):
Handler(Looper, Callback, boolean async)构造废弃,异步消息改走Message工厂标记。 - API 31~36(Android 12~16):非 SDK 名单持续收紧,屏障相关内部 API 仅限平台使用;语义本身十年零破坏。
- API 30(Android 11):
IntentService废弃标记落地;Handler无行为变更,迁移压力在应用侧。
十三、一句话备忘
- Looper 是泵,队列是表,Handler 是嘴:消息永远从 target 的那张嘴吐出;
- 一切延时都是
when的算术,一切唤醒都是 eventfd 的一字节; - 屏障让帧先行,idle 捡漏不抢跑;
- 主线程队列即体感,隐式引用即泄漏。
本页小结
本页小结:一条线程一个 Looper,一个 Looper 一条按时间排序的链表,epoll 定时睡眠、eventfd 唤醒,屏障给帧消息开 VIP,idle 捡漏,主线程队列就是 App 的体感本身。调试口诀:延时不准查时间基准,消息不跑查屏障和 quit,内存不降查隐式引用。
参考
- 线程与 Looper 官方说明:developer.android.com/guide/components/fundamentals(进程与应用生命周期章)
- Looper 源码:cs.android.com/android/platform/frameworks/base/+/main/core/java/android/os/Looper.java
- MessageQueue 源码:cs.android.com/android/platform/frameworks/base/+/main/core/java/android/os/MessageQueue.java
- Handler 源码:cs.android.com/android/platform/frameworks/base/+/main/core/java/android/os/Handler.java
- Message 源码:cs.android.com/android/platform/frameworks/base/+/main/core/java/android/os/Message.java