Skip to content

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;分发前不可改
whenuptimeMillis 绝对时刻排序键,postDelayed 换算后就存在这里
callbackpost(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主线程磁盘/网络/慢 UIdebug 包策略拉满,release 不开
LeakCanaryHandler/回调引发的对象驻留定位「谁持有 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

基于 Android 16 / API 36 · 内容为原创整理,转载请注明出处