AI 技术博客
Android全栈13 分钟阅读7784

【全栈第4课】Handler / Looper / MessageQueue 消息机制

Android 全栈工程师进阶教程 第04课。Handler / Looper / MessageQueue 消息机制。基于公开 AOSP 知识整理,含原理、可编译示例、常见问题定位。

> 本文是《Android 全栈工程师进阶教程》系列第 04 课。完整 26 课见 GitHub 仓库

【全栈第4课】Handler / Looper / MessageQueue 消息机制


0. 这节课你将做出什么

纯 Java(不依赖 Android)手写一个 mini 版的 Looper + MessageQueue + Handler,本地 javac 就能跑。跑起来你会看到:三条消息按"执行时间"而非"发送顺序"被取出处理。

学完你能回答:主线程那个"死循环" Looper.loop() 为什么不卡死 App、还几乎不耗 CPU?handler.post(runnable) 到底把东西塞到哪了?ANR 和它什么关系?


1. 背景:UI 线程为什么是个死循环

你可能听过一句反直觉的话:Android 主线程(UI 线程)本质是个 for(;;) 死循环(ActivityThread.main 里的 Looper.loop())。

按常理,死循环会把 CPU 跑满、把线程卡死。但主线程明明能流畅响应点击、还不烫手机。为什么?

因为这个循环是"没活干就睡死,有活才醒"的——靠的不是 Java 的 while,而是底层 epoll 阻塞。理解这一点,你就理解了 Android 整个事件驱动模型,以及 ANR 的本质。


2. 全局图:四个角色怎么转

Handler.sendMessage / post  ──┐
                              │ 把 Message 塞进
                              ▼
                        MessageQueue(按 when 时间排序的单链表)
                              ▲ 不停地取
                              │
                          Looper.loop()  ── for(;;) 死循环
                              │ 取出 Message
                              ▼ msg.target.dispatchMessage(msg)
                          Handler.handleMessage  ← 还给当初发它的 Handler 处理

四个角色:

角色职责
Message一条消息:what(类型)、obj(数据)、when(该执行的时刻)、target(哪个 Handler 发的)
MessageQueuewhen 排序的队列。空了就让取它的线程睡死
Looper死循环,从 MessageQueue 取消息,交给 msg.target 处理
Handler既发消息(sendMessage)又处理消息(handleMessage)

关系:一个线程一个 Looper,一个 Looper 一个 MessageQueue,一个线程可以有多个 Handler(都往同一个队列塞)。Message.target 记着是哪个 Handler 发的,取出来就还给它。


3. 某真实机型 真实源码印证

Looper.loop 的核心(Looper.java):

public static void loop() {                    :395
    for (;;) {                                 :419
        if (!loopOnce(me, ...)) return;
    }
}
loopOnce():                                    :246
    Message msg = me.mQueue.next();   // ① 取消息(可能阻塞睡死)  :248
    msg.target.dispatchMessage(msg);  // ② 分发给 Handler         :315
    msg.recycleUnchecked();           // ③ 回收复用 Message 对象

死循环不耗 CPU 的秘密(LegacyMessageQueue/MessageQueue.java):

Message next() {                               :373
    int nextPollTimeoutMillis = 0;
    for (;;) {
        nativePollOnce(ptr, nextPollTimeoutMillis);   // ← 关键!  :389
        ... 有消息就 return msg ...
        nextPollTimeoutMillis = -1;   // 没消息 → 下次传 -1 = 无限睡
    }
}
private native void nativePollOnce(long ptr, int timeoutMillis);  :94

> 注:Android 新版本 这棵树里 MessageQueue 有 Legacy/Combined 几个变体,这里以经典的 Legacy 版讲,机制一致。


4. 前置准备

  • 只需本机 JDK(javac/java),不依赖 Android。
  • 目的:用 wait/notify 模拟 Android native epoll 的"睡死/唤醒",把机制看透。

5. 动手:手写 mini Looper(逐段讲)

5.1 Message —— 一条消息

class MiniMessage {
    int what;            // 消息类型
    Object obj;          // 携带数据
    long when;           // 该执行的绝对时刻(ms),用来排序+延时
    MiniHandler target;  // 哪个 Handler 发的,取出后还给它
}

5.2 MessageQueue —— 按 when 排序 + 睡死/唤醒

class MiniMessageQueue {
    // Android 是单链表;这里用优先队列(按 when 小的优先)等价
    private final PriorityQueue<MiniMessage> queue =
            new PriorityQueue<>((a, b) -> Long.compare(a.when, b.when));
    private boolean quitting = false;

    // 入队:别的线程调,塞完唤醒 Looper —— 对应 Android 的 nativeWake
    synchronized void enqueue(MiniMessage msg) {
        queue.offer(msg);
        notifyAll();        // 唤醒在 next() 里 wait 的 Looper 线程
    }

    // 取下一条:对应 MessageQueue.next() + nativePollOnce
    synchronized MiniMessage next() {
        while (!quitting) {
            long now = System.currentTimeMillis();
            MiniMessage head = queue.peek();
            if (head == null) {
                try { wait(); }                  // 队列空 → 无限睡死(对应 nativePollOnce(ptr,-1)),0 CPU
                catch (InterruptedException e) { return null; }
            } else if (head.when > now) {
                try { wait(head.when - now); }   // 有延时消息 → 睡到该执行(对应 timeout=when-now)
                catch (InterruptedException e) { return null; }
            } else {
                return queue.poll();             // 到点了 → 取出返回
            }
        }
        return null;
    }
    synchronized void quit() { quitting = true; notifyAll(); }
}

> 核心就在 next():没消息时 wait() 让线程睡死(不空转、0 CPU);别的线程 enqueuenotifyAll() 把它叫醒。这就是 Android 主线程死循环不烫手机的原理——只不过 Android 用的是更底层的 epoll(等在 binder fd / 管道上),思想一模一样。

5.3 Looper —— 死循环取消息分发

class MiniLooperCore {
    final MiniMessageQueue queue = new MiniMessageQueue();
    // ThreadLocal 保证一个线程一个 Looper(对应 Android sThreadLocal)
    private static final ThreadLocal<MiniLooperCore> TL = new ThreadLocal<>();

    static void prepare() {
        if (TL.get() != null) throw new RuntimeException("一个线程只能有一个 Looper");
        TL.set(new MiniLooperCore());
    }
    static MiniLooperCore myLooper() { return TL.get(); }

    static void loop() {                          // 对应 Looper.loop()
        MiniLooperCore me = myLooper();
        for (;;) {
            MiniMessage msg = me.queue.next();     // ① 取(可能睡死)
            if (msg == null) return;               // quit
            msg.target.dispatchMessage(msg);       // ② 分发给 Handler
        }
    }
}

> ThreadLocal 保证"一线程一 Looper":每个线程拿到的是自己那份。这就是为什么子线程要用 Handler 得先 Looper.prepare()——给这个线程造它专属的 Looper+Queue。

5.4 Handler —— 发 + 处理

class MiniHandler {
    private final MiniLooperCore looper;
    MiniHandler(MiniLooperCore looper) { this.looper = looper; }

    void sendMessage(int what, Object obj) { sendDelayed(what, obj, 0); }

    void sendDelayed(int what, Object obj, long delayMs) {     // 对应 sendMessageDelayed/postDelayed
        MiniMessage m = new MiniMessage();
        m.what = what; m.obj = obj;
        m.when = System.currentTimeMillis() + delayMs;          // 延时 = 现在 + delay
        m.target = this;                                        // 标记是我发的
        looper.queue.enqueue(m);
    }

    void handleMessage(MiniMessage msg) {                       // 子类重写,对应 handleMessage
        System.out.println("    >> handleMessage what=" + msg.what + " obj=" + msg.obj);
    }
    void dispatchMessage(MiniMessage msg) { handleMessage(msg); }
}

5.5 main —— 把消息泵跑起来

public static void main(String[] args) {
    MiniLooperCore.prepare();                          // 主线程造 Looper(对应 prepareMainLooper)
    MiniLooperCore looper = MiniLooperCore.myLooper();
    MiniHandler handler = new MiniHandler(looper);

    new Thread(() -> {                                  // 另起线程模拟"从别处往主线程发消息"
        handler.sendMessage(1, "立即执行");
        handler.sendDelayed(2, "延时500ms", 500);
        handler.sendDelayed(3, "延时200ms", 200);        // 后发,但会先于 2 执行(按 when 排序)
        sleep(800);
        looper.queue.quit();
    }, "sender").start();

    MiniLooperCore.loop();                              // 主线程进消息泵,阻塞在这直到 quit
}

6. 看底层:为什么是 epoll 不是空转

Android 真实的 nativePollOnce 底层调 epoll_wait,等在一组 fd 上(管道 fd、binder fd 等):

  • 队列空 → epoll_wait(timeout=-1) → 线程进内核睡眠,完全不占 CPU
  • 别的线程 enqueueMessage → 通过 nativeWake 往管道写一个字节 → epoll_wait 立即返回 → 线程醒。
  • 有定时消息 → epoll_wait(timeout=when-now) → 睡到点自动醒。

我们 mini 版用 wait/notify 模拟的就是这套"睡死—唤醒"。本质一样。


7. 编译 & 运行(真验证过)

javac MiniLooper.java && java MiniLooper

实际输出(本机真跑过):

[Looper] 开始 loop,线程=main
[Handler] 发送 what=1 延时=0ms
[Handler] 发送 what=2 延时=500ms
[Handler] 发送 what=3 延时=200ms
[Looper] 取出消息 what=1
    >> handleMessage what=1 obj=立即执行
[Looper] 取出消息 what=3          ← 注意!后发的 3 先执行
    >> handleMessage what=3 obj=延时200ms
[Looper] 取出消息 what=2
    >> handleMessage what=2 obj=延时500ms
[Looper] 收到 quit,退出 loop

> 关键观察:发送顺序是 1→2→3,但执行顺序是 1→3→2。because MessageQueue when 排序,不是按发送顺序。这是消息机制最核心的一点,你亲眼看到了。 > 验证状态:本课纯 Java,本机 javac + java 真编真跑,输出如上。


8. 踩坑提醒

  1. 子线程直接 new Handler 崩:报 Can't create handler inside thread that has not called Looper.prepare()。子线程要先 Looper.prepare()Looper.loop()。主线程已由系统 prepare 过。
  2. Handler 内存泄漏:非静态内部类 Handler 持有 Activity 引用,延时消息没执行完 Activity 就泄漏。用静态内部类 + 弱引用。
  3. 在 handleMessage 里做耗时操作:主线程的 handleMessage 卡住,后面的消息排队 → 卡顿/ANR。
  4. 以为 postDelayed 精确:延时是"最早不早于",前面有消息没处理完会延后。

9. 常见问题分析与定位(消息机制实战)

9.1 常见问题清单

  1. ANR(Application Not Responding),日志显示主线程卡住
  2. UI 卡顿、掉帧
  3. Handler 内存泄漏导致 OOM
  4. 子线程 Handler 崩(没 prepare)
  5. postDelayed 不准 / 消息没执行

9.2 分析与定位

① ANR

  • 现象:App 无响应弹框,adb logcatANR in ...,/data/anr/traces.txt 主线程栈卡在某处。
  • 原因:主线程的某个 Message 处理太久(>5s 输入超时/10s 广播),后面消息排队超时。
  • 定位:
adb shell dumpsys activity      # 看 ANR
adb pull /data/anr/traces.txt   # 主线程栈卡在哪个方法 = 罪魁
adb logcat | grep -iE "ANR in|Reason"
  • 看主线程栈卡在哪个 handleMessage/调用,那就是处理太久的活,挪到子线程。

② 卡顿掉帧

  • 原因:主线程消息处理超过一帧(16ms@60Hz),来不及画下一帧。
  • 定位:
# perfetto/systrace 抓主线程,看哪个 Message/方法超 16ms
adb shell dumpsys gfxinfo <pkg>   # 看丢帧统计
  • 修复:把重活(IO/解码/计算)移出主线程。

③ Handler 内存泄漏

  • 现象:Activity 反复进出后内存涨、OOM。
  • 原因:Handler 持 Activity 引用 + 延时消息还在队列 → Activity 无法回收。
  • 定位:adb shell dumpsys meminfo <pkg> 看 Activity 实例数;heap dump 看 Handler 引用链。
  • 修复:静态 Handler + WeakReference;onDestroy 里 removeCallbacksAndMessages(null)

④ 子线程 Handler 崩

  • 报错:Can't create handler inside thread that has not called Looper.prepare()
  • 修复:子线程 Looper.prepare() → new Handler → Looper.loop();或直接用 HandlerThread

⑤ postDelayed 不准/消息丢

  • 原因:前面有消息处理慢(延后);或 Handler 被 remove 了;或 Looper quit 了。
  • 定位:dumpsys 看 MessageQueue(dumpsys activity 里有 Handler dump);确认 Looper 没退出。

9.3 定位决策树

消息机制出问题
├─ ANR → /data/anr/traces.txt 主线程栈 → 卡在哪个 handleMessage → 挪子线程
├─ 卡顿掉帧 → perfetto/gfxinfo → 主线程超 16ms 的活 → 移出主线程
├─ OOM/泄漏 → meminfo Activity 数 + heap dump Handler 引用 → 静态+弱引用+removeCallbacks
├─ 子线程 Handler 崩 → "未 prepare" → Looper.prepare 或 HandlerThread
└─ postDelayed 不准/丢 → 前面消息慢/Handler被remove/Looper退出

10. 小结

  1. 四角色:Handler 发/收,MessageQueue 按 when 排序,Looper 循环取,Message 带 target
  2. 死循环不耗 CPU 的真相:没消息时 nativePollOnce(底层 epoll)让线程睡死,来消息 nativeWake 唤醒。mini 版用 wait/notify 模拟。
  3. 执行顺序按 when,不是发送顺序(亲眼验证:1→3→2)。
  4. ANR/卡顿的本质:主线程某个 Message 处理太久,后面排队超时。
  5. 排查口诀:ANR 看 traces.txt 主线程栈;卡顿用 perfetto;泄漏用静态+弱引用+removeCallbacks。

下节预告

L5:JNI 原理 + 注册方式 —— Java 怎么调到 C/C++?framework 用的动态注册(RegisterNatives)长什么样?给服务加一段 native 调用。

评论