【全栈第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 发的) |
| MessageQueue | 按 when 排序的队列。空了就让取它的线程睡死 |
| 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 nativeepoll的"睡死/唤醒",把机制看透。
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);别的线程 enqueue 时 notifyAll() 把它叫醒。这就是 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. 踩坑提醒
- 子线程直接 new Handler 崩:报
Can't create handler inside thread that has not called Looper.prepare()。子线程要先Looper.prepare()再Looper.loop()。主线程已由系统 prepare 过。 - Handler 内存泄漏:非静态内部类 Handler 持有 Activity 引用,延时消息没执行完 Activity 就泄漏。用静态内部类 + 弱引用。
- 在 handleMessage 里做耗时操作:主线程的 handleMessage 卡住,后面的消息排队 → 卡顿/ANR。
- 以为 postDelayed 精确:延时是"最早不早于",前面有消息没处理完会延后。
9. 常见问题分析与定位(消息机制实战)
9.1 常见问题清单
- ANR(Application Not Responding),日志显示主线程卡住
- UI 卡顿、掉帧
Handler内存泄漏导致 OOM- 子线程 Handler 崩(没 prepare)
- postDelayed 不准 / 消息没执行
9.2 分析与定位
① ANR
- 现象:App 无响应弹框,
adb logcat有ANR 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. 小结
- 四角色:Handler 发/收,MessageQueue 按
when排序,Looper 循环取,Message 带target。 - 死循环不耗 CPU 的真相:没消息时
nativePollOnce(底层 epoll)让线程睡死,来消息nativeWake唤醒。mini 版用 wait/notify 模拟。 - 执行顺序按 when,不是发送顺序(亲眼验证:1→3→2)。
- ANR/卡顿的本质:主线程某个 Message 处理太久,后面排队超时。
- 排查口诀:ANR 看 traces.txt 主线程栈;卡顿用 perfetto;泄漏用静态+弱引用+removeCallbacks。
下节预告
L5:JNI 原理 + 注册方式 —— Java 怎么调到 C/C++?framework 用的动态注册(RegisterNatives)长什么样?给服务加一段 native 调用。