职场比喻:想象你是一个大忙人(主线程),手头正赶着给老板写周报。这时候,产品经理、测试同事、后端小哥都想找你改个bug、加个字段、查个日志。如果他们轮番冲过来拍你肩膀,你肯定疯掉——思路断了,周报写不完,还容易摔键盘。
聪明的做法是什么?在你工位旁边放一个信箱。谁有事就写张纸条扔进去,你忙完手头的事,再按顺序看纸条、处理事情。Handler机制,就是安卓主线程的这个“信箱系统”。
1. 为啥非得用Handler?——不听话的“子线程”不能碰UI
你刚学安卓时肯定踩过这个坑:在子线程里直接更新TextView,结果App崩了,报错“Only the original thread that created a view hierarchy can touch its views”。
用人话说:UI控件是主线程的“亲儿子”,子线程这个外人不能随便碰。那子线程下载完图片、查完数据库,怎么通知界面刷新?
Handler就是那个传话的信使。子线程把“更新UI”的任务写成一封“信”(Message或Runnable),投递到主线程的信箱(MessageQueue)里。主线程空闲时取出信,自己执行更新操作——完美解决跨线程通信。
2. 信箱三件套:Handler、Looper、MessageQueue
这套机制有三个核心角色,咱们用信箱比喻对应起来:
- MessageQueue(信箱):一个按时间排序的“待办事项列表”。你可以往里面扔消息,也可以设定“10分钟后执行”。它是先进先出的,但如果有延时消息,就会插队到合适的位置。
- Looper(邮差):一个死循环工作的邮差。他永远在问“信箱里有没有信?有就拿给收件人,没有就睡觉”。注意,每个线程只有一个Looper。主线程在启动时,系统已经帮你创建好了Looper。如果你在子线程手动调用了
Looper.prepare(),记得在退出时调用quit()或quitSafely(),否则线程会一直活着,可能造成内存泄漏。 - Handler(收件地址+投递口):你给外界留的“投递信箱的钥匙”。子线程通过你给的Handler,就能把消息投进正确的信箱。同时,Handler也是收件人——Looper取出消息后,会回调Handler的
handleMessage()方法,你在这里写UI更新逻辑。
💡 小贴士:Message内部维护了对象池,建议用handler.obtainMessage()替代new Message(),复用消息对象,减少内存波动。
流程串起来:子线程调用handler.sendMessage(msg) → 消息进入主线程的MessageQueue → 主线程的Looper发现队列有消息 → Looper取出消息,回调Handler的处理方法 → 你在主线程安全地更新UI。
3. 主线程的“死循环”为什么不卡死?
你可能会问:Looper.loop()是个while(true)死循环,那主线程岂不是一直转,界面怎么不卡?
比喻一下:邮差不是疯狂跑步,而是“有信就送,没信就坐在信箱旁打盹”。在安卓里,当MessageQueue为空时,Looper会让当前线程阻塞等待(调用nativePollOnce),把CPU时间让给其他任务。一旦有新消息进来,内核会唤醒线程,继续处理。
这个“打盹+唤醒”机制,既保证了主线程不退出(退出App就没了),又不浪费CPU。而ANR(应用无响应)是因为你处理某个消息耗时太久(比如在UI线程里写网络请求),导致后面的“点击事件”消息5秒都没被处理——用户就感觉卡死了。
面试官爱怎么问?
问:Handler会导致内存泄漏吗?怎么解决?
答:会。非静态内部类(包括匿名内部类)会隐式持有外部类的引用。比如你在Activity里写了一个Handler,它内部引用了Activity。如果子线程中有一个延时消息(比如10秒后发),在这10秒内用户退出了Activity,但消息还在队列里,Handler仍然持有Activity,导致Activity无法被回收。解决办法:用静态内部类+弱引用,或者在Activity销毁时调用removeCallbacksAndMessages(null),清空所有未处理的消息。
问:一个线程能有多个Handler吗?多个Looper呢?
答:一个线程可以创建多个Handler,它们共用同一个Looper和MessageQueue。Handler只是消息的“发送端”和“处理端”标识,不占用额外线程资源。但只能有一个Looper,调用两次Looper.prepare()会抛异常。主线程的Looper是系统自动创建的,子线程想用Handler,得自己调用Looper.prepare()和Looper.loop()。
💡 进阶加餐(面试装X用):
- 消息对象池:Message.obtain()从池里拿对象,避免频繁GC。
- 同步屏障:系统需要优先渲染UI时,会往队列头部插入一个“屏障”,让异步消息(如绘制任务)插队执行。日常开发用不到,但面试官问“为什么刷帧不卡”时可以提一嘴。
人话总结
Handler就是主线程的“信箱”——子线程把任务写成信投进去,主线程忙完手头活再拆信处理,既安全又不打架。
总结表格
| 角色 | 信箱比喻 | 技术职责 | 一句话特性 |
|---|---|---|---|
| MessageQueue | 实体信箱 | 存储消息的队列 | 按时间排序,先进先出+延时插队 |
| Looper | 邮差 | 循环从队列取消息并分发 | 每个线程唯一,主线程自动创建 |
| Handler | 投递口+收件人 | 发送消息 + 处理消息 | 可以多个共享同一个Looper |
汇总导航