Thread ↔ Looper ↔ MessageQueue ↔ Handler ↔ Message之间的关系

一、他们各自是谁?怎么"串"起来?

ini 复制代码
[Thread] 1:1 [Looper] ------持有------> [MessageQueue]  <------被操作------  [Handler] *N
                                              ↑                        |
                                              └────────包含───────────┘
                                                            [Message]
  • Thread :线程本身。只有调用过 Looper.prepare() 的线程才"有 Looper/消息泵"。

  • Looper :每线程唯一(保存在 ThreadLocal)。负责循环取消息并派发。

  • MessageQueue:此线程的消息队列,存放将来要执行的 Message(按 when 时间排序、阻塞等待)。

  • Handler :消息入口。谁创建它,它就绑定哪个 Looper/Queue。sendMessage/post(Runnable) 都是往 Queue 里塞条目。

  • Message :一次投递。包含 what/arg1/arg2/obj/when/target 等,其中 target=投递它的 Handler

主线程(UI 线程) :在 ActivityThread.main() 里 Looper.prepareMainLooper(); Looper.loop() 已就绪,所以你直接 Handler(Looper.getMainLooper()) 就能发消息到 UI 线程。
子线程若要收消息:必须自己 Looper.prepare() → 创建 Handler → Looper.loop()。


二、一次消息是如何走完的?

  1. 投递:handler.sendMessage(msg) 或 post { ... }

    • MessageQueue.enqueueMessage() 把消息按 when 插入有序单链表;msg.target = handler。
  2. 取出:Looper.loop() 调 queue.next()

    • 若队列为空或最近的 when 未到,阻塞等待(native pollOnce,底层 epoll/kqueue/管道唤醒)。
  3. 派发 :拿到 msg → msg.target.dispatchMessage(msg)****

    • dispatchMessage():优先跑 msg.callback(post(Runnable) 存这里),否则调 Handler.handleMessage(msg)。
  4. 回收:Message.recycleUnchecked() 放回全局池(Message.obtain() 复用)。

伪码核心(高度概括):

scss 复制代码
for(;;) {
  Message msg = queue.next();          // 可能阻塞
  msg.target.dispatchMessage(msg);     // 回到发送它的 Handler 实例
  msg.recycleUnchecked();
}

三、最小可运行模板

1) 在主线程发到主线程(UI 刷新/切换)

ini 复制代码
val main = Handler(Looper.getMainLooper())
main.post { textView.text = "Hi" }

2) 子线程自带 Looper(纯手动)

kotlin 复制代码
class LooperThread : Thread() {
  lateinit var handler: Handler
  override fun run() {
    Looper.prepare()             // 绑定到当前 ThreadLocal
    handler = object: Handler(Looper.myLooper()!!) {
      override fun handleMessage(msg: Message) { /* 处理 */ }
    }
    Looper.loop()                // 开始消息泵(阻塞)
  }
}
// 使用
val t = LooperThread().apply { start() }
// 等 handler 初始化完(可用 CountDownLatch)再:
t.handler.sendEmptyMessage(1)

3) 用HandlerThread(封装好了)

scss 复制代码
val ht = HandlerThread("worker").apply { start() }
val worker = Handler(ht.looper)
worker.post { /* 后台任务 */ }
// 退出
ht.quitSafely()

四、关键细节与进阶

1) 每线程唯一:ThreadLocal

  • Looper.myLooper() 从 ThreadLocal 取;一个线程只能有一个 Looper

  • 主线程在框架里已经 prepareMainLooper(),你不需要再 prepare()。

2) MessageQueue 的阻塞/唤醒

  • enqueueMessage() 发现新消息更早,会通过管道唤醒正在 pollOnce() 的队列。

  • next() 会:

    • 先看超时是否到了(when),到了就返回该消息;

    • 没到就睡一会(或无限期),被新消息/异步事件唤醒继续。

3) 同步屏障 & 异步消息(UI 渲染相关)

  • 框架会插入同步屏障 (Sync Barrier),阻塞**"同步消息" ,只放行异步消息**(msg.isAsynchronous())。

  • Choreographer 的 vsync 驱动就利用了异步消息确保输入/动画/绘制在合适的节拍执行。

  • 业务一般很少手动用;知道有这层机制即可。

4)post(Runnable) vs sendMessage(Message)

  • post(Runnable):把 Runnable 塞进 Message.callback,到达后直接 run()。

  • sendMessage:走 handleMessage(msg) 分发。

5) 退出 Looper

  • Looper.quit():立刻退出,移除所有待处理消息。

  • Looper.quitSafely():处理到所有已到期的同步消息后退出(更温和)。

  • 退出后再发消息会抛 IllegalStateException。

6) IdleHandler:队列空闲时回调

arduino 复制代码
Looper.myQueue().addIdleHandler {
    // 队列暂时空了;做些低优先级任务
    false // 返回 true 可反复回调
}

五、常见问题 & 最佳实践

  • 泄漏:匿名/非静态内部 Handler 持有外部 Activity;在 onDestroy 调

    handler.removeCallbacksAndMessages(null),或把 Handler 写成 static + WeakReference。

  • ANR:主线程 handleMessage/Runnable 里做了重活阻塞了 Looper(>5s)。重活丢给后台线程或异步。

  • 跨线程更新 UI :只能投递到 主线程 Looper 的 Handler;直接在子线程改 View 会崩。

  • 顺序/延时 :同一个 Handler/Queue 的消息按 when 有序;postAtTime/postDelayed 控制时机。

  • 与协程结合:可用 Dispatchers.Main;若必须与 Handler 交互,suspendCancellableCoroutine + Handler.post/removeCallbacks 封装。


六、一句话总览

Thread (线程)里若准备了 Looper ,就有一个MessageQueueHandler 绑定该 Looper,负责把 Message 投递到队列Looper.loop() 不停从队列取消息,并回调到 msg.target(也就是 Handler) → dispatchMessage/handleMessage 执行。主线程生来就有 Looper,子线程想收消息就自己 prepare/loop。

相关推荐
汤姆Tom2 小时前
CSS 新特性与未来趋势
前端·css·面试
南北是北北3 小时前
list并发与共享
面试
南北是北北3 小时前
泛型的三种型变类型:逆变,协变和不变
面试
ShooterJ4 小时前
Mysql小表驱动大表优化原理
数据库·后端·面试
小时前端4 小时前
🚀 面试必问的8道JavaScript异步难题:搞懂这些秒杀90%的候选人
javascript·面试
Takklin4 小时前
JavaScript 面试笔记:作用域、变量提升、暂时性死区与 const 的可变性
javascript·面试
知其然亦知其所以然4 小时前
面试官一开口就问:“你了解MySQL水平分区吗?”我当场差点懵了……
后端·mysql·面试
老马啸西风4 小时前
力扣 LC27. 移除元素 remove-element
算法·面试·github
南北是北北5 小时前
List排序/查找最佳实践
面试