Flutter中UI线程是怎么工作的 Flutter UI 线程UI Task Runner处理任务的核心是一套“永不停歇的事件循环Message Loop / Event Loop”。它之所以既能处理瞬息万变的用户点击、网络回调又能雷打不动地卡住时间点去画界面是因为它在底层对任务进行了严格的优先级分流和串行消费。我们可以把它的任务处理机制拆解为以下三个部分1. 任务的四大来源与分类UI 线程面对的任务不是一锅粥而是分属于不同的“派系”。按优先级从高到低排列C 引擎底层任务比如 Vsync 信号到达、触摸手势事件Pointer Event、原生插件MethodChannel的回调。Dart 微任务 (Microtask Queue)通过scheduleMicrotask()产生。通常是 Dart 内部非常短暂的异步操作必须最优先插队执行。Dart 普通事件 (Event Queue)比如Future、Timer定时器、网络请求返回、输入框文本改变等。Framework 绘制回调 (SchedulerBinding)我们在前文提到的动画、布局、绘制任务它们其实是借道于 C 底层任务Vsync被唤醒执行的。2. 横向全景UI 线程的事件循环轮询流UI 线程在醒着的时候会按照以下顺序循环往复地处理任务单线程串行一次只做一个检查 C 任务队列 ➔ [发现 Vsync] 执行绘制任务(Build/Layout/Paint) ➔ 连续清空所有 Microtask ➔ 取出并执行1个Event 任务 ➔ 再次清空所有 Microtask ➔ 回到开头详细步骤拆解唤醒与 C 优先当线程被唤醒首先看 C 队列。如果是 Vsync 任务立刻通过硬通道调用 Dart 的onBeginFrame。此时Dart 会同步、一口气把组件重构、布局、绘制Build➔Layout➔Paint全部做完打包出 Layer Tree 扔给 Raster 线程。在这个过程中任何普通的 Dart 代码都无法插嘴。榨干微任务重绘完或者没有重绘任务后UI 线程会转头看向 Dart 的Microtask Queue。只要这个队列里有任务线程就会死磕到底直到把它完全清空。赏给普通事件一个机会微任务清空后UI 线程才会去Event Queue里取出一个注意仅仅是一个普通事件比如执行一个Future的then回调。再次清空微任务每执行完一个普通事件线程都会立刻回去检查Microtask Queue是否产生了新任务。如果有再次清空它。休眠如果所有队列都空了UI 线程通过epoll_wait或RunLoop重新进入休眠等待下一次被用户点击或 Vsync 信号唤醒。3. 核心设计为什么普通任务卡不死界面绘制你可能会担心如果Event Queue里堆积了 100 个网络请求和点击事件下一个 Vsync 来了界面不就被卡死了吗Flutter 巧妙地利用了“C 任务 Dart Event 任务”的降维打击机制Vsync 是高级特权Vsync 信号是直接投递到C 层的 UI Task Runner中的。无情插队即使 Dart 的Event Queue里排了 1000 个任务当 Vsync 信号在 C 层触发时事件循环在下一轮轮询中会优先提审 C 任务。结果普通的 Dart 异步任务Future只能乖乖让路等这一帧的画面绘制Build/Layout/Paint完结后UI 线程才会继续去啃剩下的Event Queue。总结Flutter UI 线程就像一个高效的单人流水线工人只要Vsync 监工一喊话C 信号他立刻放下手头所有事情去画图。画完图他优先解决紧迫的组内小事Microtask。最后有空了才去一件一件地挑外部工单Event Queue来做。追问1如果在清空Microtask的过程中收到了绘制任务会怎么样答案是绘制任务不会打断正在执行的 Microtask。它会乖乖在 C 队列里排队直到当前的 Microtask 队列被完全清空才会开始绘制。如果这个清空过程耗时过长就会直接导致掉帧Jank。为了让你彻底看清底层的串行逻辑我们来看看那一瞬间究竟发生了什么1. 为什么绘制任务不能“强行插队”因为 Flutter 的 UI 线程本质上是一个单线程。单线程意味着在任意微秒CPU 只能执行一行代码。当 UI 线程正在清空Microtask Queue时Dart 虚拟机正处于一个紧密的、同步的while循环中// Dart 虚拟机的底层伪代码 while (hasPendingMicrotasks) { runNextMicrotask(); // 正在一件一件死磕微任务 }在这个while循环结束之前Dart 虚拟机绝对不会交出 CPU 的控制权。2. 信号到达时的横向流转就在 Dart 疯狂清空微任务的间隙Vsync 信号突然到了操作系统发信➔ 硬件发出 Vsync。Engine 接收并投递➔ Engine 层的VsyncWaiter收到信号把绘制任务BeginFrame打包调用PostTask扔进 C 的任务队列。默默排队➔ 此时绘制任务已经躺在 C 的队列里了。但由于 UI 线程的 CPU 正在全力以赴执行 Dart 的微任务while循环绘制任务无法强行中断这段 Dart 代码只能在队列里静静等待。微任务清空交出控制权➔ Dart 终于把微任务列表清空了退出while循环当前执行块结束控制权交还给底层的 CMessageLoop。轮到绘制任务➔ CMessageLoop睁眼一看“哟队列里躺着一个 Vsync 绘制任务呢。” 于是立刻执行它再次穿透进入 Dart 世界触发onBeginFrame。3. 致命的隐患微任务饥饿Microtask Starvation基于上述机制Flutter 工程师在编写业务代码时必须警惕一个天坑——渲染饥饿。如果你在 Dart 中写出了类似下面的代码void badIdling() { scheduleMicrotask(() { // 做了一些耗时 5ms 的工作... badIdling(); // 递归调用源源不断地产生新的 Microtask }); }后果Dart 的微任务while循环将永远无法结束。现象哪怕操作系统的 Vsync 信号像雨点一样砸向 C 队列UI 线程也永远没有机会去瞅一眼。整个应用的界面会彻底冻结卡死直到系统弹出 ANR应用无响应弹窗。总结在微任务清空的过程中收到绘制任务绘制任务只能在外部的 C 队列中待命。只有当 Dart 识大体地把眼前的微任务全部扫干净、主动让出 CPU 之后UI 线程才会去执行绘制任务。这也是为什么官方一再强调绝对不要在 Microtask 中执行耗时操作。追问2微任务清完不是会拿Event任务吗在纯 Dart 环境比如写一个纯 Dart 的控制台程序下事件循环的死理就是同步清空所有微任务 ➔ 取出 1 个事件任务 ➔ 再去同步清空微任务。但为什么在 Flutter 运行时微任务清完后绘制任务有时能直接“截胡”普通的事件任务呢因为 Flutter 搞了一个底层融合它把 Dart 的事件队列Event Queue在 C 层给“收编”了。Flutter 隐藏的真相队列的合并在 Flutter 应用中Dart 的Event Queue并不是一个独立的、纯 Dart 语言维系的逻辑队列。每当你写下Future(() foo())或者启动一个Timer时Dart VM 的底层其实是通过MessageHandler向C 层的fml::MessageLoop也就是 UI 线程的底层 C 任务队列投递了一个 C 任务。也就是说在底层的操作系统线程看来一个Future的异步回调任务Dart Event一个系统的Vsync绘制任务BeginFrame它们在 C 层的 UI 线程队列里是平起平坐的、在同一个队列里排队的“同事关系”。真实的执行时序微任务清空后的那一瞬间我们把时间线切回到你说的那个边界瞬间当前状态UI 线程正在执行上一个 Dart 任务可能是一个旧的 Event。在这个任务执行完的刹那Dart 触发了内部机制开始同步、疯狂地清空Microtask Queue。Vsync 突袭就在 Dart 埋头清空微任务的这一微秒硬件发出了 Vsync 信号。Engine 收到后立刻往 C 的MessageLoop队列里塞入了一个绘制任务。微任务清空Dart 终于把微任务全部扫干净了。到此为止当前的这一个 C 核心任务才算真正彻底结束Dart 退出执行上下文控制权交还给底层的 C 事件循环。C 轮询取任务C 核心循环重新接管线程它根本不管 Dart 内部“Event 任务”和“Microtask 任务”的轮询规矩它只认自己的 C 队列。它往自己的 C 队列里一看如果刚刚塞进来的Vsync 绘制任务排在了下一个或者到了它的预定执行时间点C 就会毫不犹豫地立刻执行 Vsync 任务。UI 线程被 Vsync 任务带入 Dart 端的onBeginFrame界面开始重绘。而那些剩下的普通 Dart Event比如其他的 Future只能继续在 C 队列后面排队。结论微任务清完后线程确实会去拿“下一个任务”。只是在 Flutter 中这个“下一个任务”究竟是普通的 Dart 事件Future还是 Vsync 绘制任务不再由 Dart 内部决定而是完全取决于它们在 C 层 UI Task Runner 队列里的排队顺序和时间戳。Vsync 信号作为底层图形硬件时钟一旦在 C 层入队在当前执行块结束、控制权回归 C 的那一刻就会被高效优先执行从而完成了对普通 Dart 事件的“合理截胡”。