3. Android消息机制:Handler、Looper、MessageQueue归纳总结
咱们做Android开发的,天天跟消息机制打交道。说句实在话,ANR的根源,十有八九都卡在消息处理上。主线程被加密阻塞?那基本上就是消息队列被堵死了,Looper转不动了。
我当年刚入行时,对这套机制也是一知半解。直到有一次线上出现大面积ANR,主线程卡在一段加密逻辑里——嗯,从那以后,我把Handler、Looper、MessageQueue翻来覆去啃了好几遍。今天我就把这套机制的核心脉络捋一捋。
3.1 消息机制的本质:一个“生产者-消费者”模型
说白了,Android主线程就是一个Looper驱动的无限循环。它从MessageQueue里一条一条取消息,然后交给Handler去处理。谁往MessageQueue里塞消息?可以是其他线程,也可以是主线程自己。
你想想看,如果某个消息的处理耗时特别长(比如你在主线程做了加密、解密、文件IO),那Looper就只能等着,后面的消息排着队干瞪眼——这不就ANR了嘛。
3.2 三个组件的分工
我把它们的职责拆成一张表,清晰点:
| 组件 | 职责 | 我的一点理解 |
|---|---|---|
| Handler | 发送消息(send/post);处理消息(handleMessage) | 像个信使,既负责投递,也负责拆包干活 |
| Looper | 循环从MessageQueue取消息,分发给对应的Handler | 永动机,主线程的“心脏” |
| MessageQueue | 按时间顺序存储Message,支持同步/异步屏障 | 像一条传送带,按优先级排队 |
我个人习惯画一张图来理解它们的关系——你一看就明白:
从图里能看到,Looper是发动机,它从MessageQueue里取消息,然后甩给对应的Handler去处理。Handler干完活再回来,Looper继续取下一条。
3.3 Handler:两种用法,一个坑
Handler的使用场景就两种:在同个线程内部用,或者跨线程用。你想想,子线程做耗时任务,结果需要更新UI,那就得通过Handler把消息发到主线程。
典型代码(子线程发消息到主线程):
// 主线程创建Handler
Handler mainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
// 这里已在主线程,可以更新UI
textView.setText((String) msg.obj);
}
};
// 子线程中发送
new Thread(() -> {
// 模拟耗时加密…
String result = doEncrypt(data);
Message msg = Message.obtain();
msg.what = 1;
msg.obj = result;
mainHandler.sendMessage(msg);
}).start();
这里有个细节——如果你在主线程用new Handler()而不指定Looper,它会自动绑定当前线程的Looper。所以我建议,跨线程使用时一定显式传Looper.getMainLooper(),否则容易造成消息发错线程,引发神秘bug。我曾经在子线程里直接new Handler(),当时就想“哎,怎么handleMessage不执行?”——嗯,那是因为子线程根本没有Looper,直接抛异常了。
3.4 Looper:主线程的“生命线”
Looper其实只有一个核心方法:loop()。咱们看看简化后的伪代码:
public static void loop() {
final Looper me = myLooper();
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // 可能会阻塞在这里
if (msg == null) {
return; // 队列退出
}
msg.target.dispatchMessage(msg);
msg.recycleUnchecked();
}
}
注意这个queue.next(),它是阻塞式的。没有消息时,线程就在这里休眠。一旦有消息入队,就会唤醒。这也就是为什么主线程永远不会退出——它一直在这个死循环里,等待处理下一个消息。
next()内部用了Linux epoll机制,没消息时线程进入休眠,不消耗CPU。这个设计太巧妙了,也是Android能流畅运行的基础。
3.5 MessageQueue:优先级队列 + 屏障机制
MessageQueue不是简单的FIFO队列。它按消息的when(触发时间)排序。你发一个延迟10秒的消息,它就会排在普通消息后面。但这里面有个高级玩法——同步屏障。
我记得有一次排查UI渲染卡顿,发现大量同步消息被异步消息插队了。后来才知道,当View.post()或Choreographer发起帧刷新时,系统会插入一个屏障(Barrier),让异步消息优先执行。这样就能保证渲染任务不被普通消息阻塞。
| 消息类型 | 示例 | 屏障影响 |
|---|---|---|
| 同步消息 | Handler.sendMessage() | 屏障后会被暂缓,直到屏障移除 |
| 异步消息 | Handler.sendMessageAtFrontOfQueue() + 设置异步 | 屏障期间仍然可以执行 |
| 屏障消息 | MessageQueue.postSyncBarrier() | 框架内部使用,开发者不可控 |
为什么聊这个?因为你在主线程做耗时加密,本质就是阻塞了同步消息的处理。屏障机制也救不了你——它只能加速异步消息的调度,但同步消息照样得排队等你的加密完成。
3.6 阻塞排查:从消息机制找根因
遇到主线程ANR,我习惯先看“当前消息是什么?处理了多久?”。在Logcat里搜索Choreographer的日志,或者用dumpsys activity processes导出主线程堆栈。堆栈里如果看到android.os.MessageQueue.nativePollOnce,说明线程在等消息,没阻塞。如果看到你的加密代码堆栈(比如AESUtils.encrypt),那这就是罪魁祸首。
我个人习惯在关键业务入口加一个自定义消息时间监控——在Handler的handleMessage里记录开始时间和结束时间,如果耗时超过500ms就上报。这样即使线上没有ANR,也能提前发现潜在拥堵。
3.7 小结
Handler、Looper、MessageQueue这三兄弟,说白了就是一套协作的线程调度模型。理解它们,你才能真正明白“为什么主线程不能做耗时操作”。不是Looper堵了,是你的回调函数堵了Looper的路。
记住这点,排查ANR时就能一针见血:看堆栈里哪个方法正在占用handleMessage,它就是元凶。