详解Handler的 时序与调度:投递方式、优先级与顺序、同步屏障/异步消息

94 阅读4分钟

1) 投递方式:时间如何决定“何时执行”

时间基准:SystemClock.uptimeMillis()(设备睡眠停走)。

一个 Looper = 一个 MessageQueue;同一 Looper 下的所有 Handler 都把任务塞进同一个队列。

投递 API 与效果:

  • post {} / sendMessage()

    立即任务:when = now(按当前队列头部时间排位)。

  • postDelayed(r, delay) / sendMessageDelayed(msg, delay)

    延时任务:when = now + delay。

  • postAtTime(r, uptime) / sendMessageAtTime(msg, uptime)

    绝对时间任务:when = uptime。

  • sendMessageAtFrontOfQueue(msg)

    插到队头(when 设成当前、并放到链表首位)。注意:只影响本次入队位置,不改变“消息类型”(仍是同步消息,见第 3 节)。

  • IdleHandler(MessageQueue.addIdleHandler)

    队列空闲(无到期消息、或刚处理完最后一条且下一条未到时)才回调,适合低优先级收尾。

结论:何时执行 = 取队头到期消息的时刻。延时、定时、队头插入只是改变 when 或链表位置。


2) 优先级与顺序:队列如何出队

MessageQueue 是按 when 升序的单链表;取消息规则(简化):

  1. 先看队头

    • 如果是普通消息且 msg.when <= now → 立刻取出;
    • 如果 msg.when > now → 休眠到到期或被新消息/唤醒打断。
  2. 相同 when(同一毫秒)按入队先后(FIFO)执行。

  3. 多个 Handler 共用同一队列,谁先到期谁先执行,与属于哪个 Handler 无关。

  4. sendMessageAtFrontOfQueue 插入队头(高于所有“when>=now”的消息);但见第 3 节——可能仍被同步屏障拦下。

  5. removeCallbacks/ removeMessages 会从链表中删除尚未执行的项;已经取出的正在执行的不能取消。

小结优先级(从高到低,常见情形):

(在没有屏障的前提下)
队头插入 > 立即/到期消息 > 延时未到期消息 > IdleHandler

3) 同步屏障(Sync Barrier)与异步消息(Async)

3.1 是什么

  • 同步屏障:MessageQueue 里一种特殊节点(target == null),用来暂时阻塞所有“同步消息” ,只放行**“异步消息”**。

    • 框架在主线程做一帧绘制前会插入屏障(ViewRootImpl/Choreographer),确保输入、动画、绘制这类异步工作优先执行,降低卡顿。
  • 异步消息:Message.isAsynchronous() == true 的消息;或用 异步 Handler 发送的消息(new Handler(looper, callback, /async=/true))。

    • 框架把**输入(Input)/动画(Animation)/遍历绘制(Traversal)**标记为异步。

3.2 有屏障时如何取消息(核心逻辑)

  • 队头是屏障:队列会向后扫描,寻找第一条异步消息

    • 找到 → 立刻取出执行;
    • 找不到 → 休眠,直到有异步消息入队或屏障被移除。
  • 同步消息在屏障之后会被卡住在屏障之前的同步消息仍能执行(例如你后来用了队头插入把同步消息塞到了屏障前面)。

3.3 实践要点

  • 正常业务不要自己插/移屏障(那是框架级 API),以免破坏调度节拍。
  • 不要滥用“异步 Handler” :把普通业务都标成异步会插队到渲染前,引发掉帧或优先级紊乱。
  • sendMessageAtFrontOfQueue 不会自动变异步;若队头有屏障,且你的消息被插在屏障后,就仍会被挡住;若插在屏障前,则会绕过屏障(这也是不建议在主线程乱用“队头插入”的原因)。

4) 组合示例(验证时序)

val q = Looper.getMainLooper().queue
val h = Handler(Looper.getMainLooper())
val asyncH = Handler(Looper.getMainLooper(), null, /*async=*/ true)

h.postDelayed({ log("A @+500ms 同步") }, 500)
asyncH.postDelayed({ log("B @+500ms 异步") }, 500)

// 框架在一帧开始前加了同步屏障(示意):q.enqueueSyncBarrier(now)

// 到 500ms:若屏障还在 → B 会先执行,A 被卡住;屏障移除后 A 才执行。

验证顺序要点:改成 h.sendMessageAtFrontOfQueue(msgA) 可能让 A 在当前就执行(甚至在屏障前),这就是“队头插入可能绕过屏障”的实际体现——慎用。


5) FAQ(高频易错)

  • 延时是按什么时钟算?

    uptimeMillis(睡眠停走)。深度睡眠期间到期的消息会等设备醒来才执行。

  • 多个 Handler 会不会抢顺序?

    不会。大家共用一个队列,按 when 与入队先后决定全局顺序

  • 为什么我 post() 了却很晚才跑?

    可能前面有大量到期任务/长耗时回调;或主线程被阻塞(ANR 风险)。

  • 怎么做低优先级任务?

    用 IdleHandler or 较长延时;不要标异步去“插队”。

  • 如何取消?

    handler.removeCallbacks(r) / removeMessages(what) / removeCallbacksAndMessages(tokenOrNull)。


6) 一页小抄

  • 何时执行:取队头到期消息 → dispatchMessage。

  • 顺序:when 升序;同 when FIFO;AtFrontOfQueue 插到头;IdleHandler 在空闲时。

  • 屏障:挡同步、放异步;慎用“队头插入”与“异步 Handler”。

  • 时间基:uptimeMillis;睡眠期间不前进。

把这三点吃透,你就能准确预判“这条消息会在什么时候跑”,并且在需要时用正确手段(延时、队头、异步/同步、IdleHandler)控制时序而不破坏主线程的绘制节奏。