Flutter Raster 线程:为何吃不满多核 CPU

掘金热榜文章《Flutter Android 性能修复:8 个大核的手机,Raster 居然只跑在 2 个核上?》记录了一个常见观测。 这个现象值得排查,但不能仅凭标题得出“Flutter 只启用了两个核”的结论:同一 Raster 线程在两个 CPU 之间迁移、多个渲染 worker 恰好活跃在两个核心上,或者采样期间 Raster 已经完成工作,都可能造成类似现象。

核心判断是:多核手机不等于 Raster 会自动利用多核。优化前应先区分 Raster 是“没活干”“想运行却拿不到 CPU”,还是“确实在执行大量难以并行的工作”。光栅化负责把绘制指令转换成像素数据,合成则负责叠加并处理各图层数据,两者是不同的渲染阶段。 “Flutter Raster 线程 CPU 利用率低”本身不是故障结论,真正需要关注的是帧耗时、掉帧以及线程在 Trace 中的状态。

先用系统 Trace 确认 Raster 的运行状态

先建立可比较的基线:固定设备、测试场景、操作路径和目标刷新率。诊断阶段使用 Profile 构建,最终结论必须在 Release 构建中复测,否则调试开销、断言和不同代码路径都可能干扰结果。

DevTools 与系统 Trace 的职责并不相同。运行 flutter run --profile 后,可以从启动输出中的 URL 打开 DevTools;Performance Overlay 与 FrameTiming 适合拆解 build、raster、vsync 等阶段。 线程调度、CPU 亲和性、运行核心、频率变化和唤醒延迟,则应交给 Perfetto 或 Android Studio System Trace。

定位前还要确认统计对象究竟是哪一个 Raster TID。单线程先后出现在两个核心上通常只是调度迁移,不等于两个核心在并行处理同一帧;多个线程同时处于活跃状态,才可能说明存在并行执行。可以先用以下命令交叉确认:

flutter --version
adb shell ps -T -p <pid> | grep -Ei 'raster|flutter'
adb shell cat /proc/<pid>/status | grep -E 'Name|State|Cpus_allowed_list'
adb shell cat /proc/<pid>/task/<tid>/status | grep -E 'Name|State|Cpus_allowed_list'
adb shell taskset -pc <tid>

不同 Android 版本和厂商镜像对 shell 权限、taskset 支持程度可能不同,命令不可用时应以 Perfetto 中的线程调度事件和 /proc 信息为准。

读取 Trace 时,需要把“低利用率”拆成几种完全不同的情况:

Trace 或帧指标信号 更可能的含义 优先处理方向
Raster 长时间处于 Sleeping 或等待状态,帧率正常 Raster 已提前完成工作,正在等待下一帧或同步条件 通常无需修复
Raster 处于 Runnable,但长时间没有获得 CPU 亲和性、cpuset、优先级竞争或系统调度限制 检查调度与 CPU 限制
Raster 正在运行,且 Raster 耗时持续偏高 绘制任务过重,或当前任务无法充分并行 降低负载或核对 Engine 并发能力
UI、GPU 提交等阶段耗时更长 瓶颈位于 Raster 上游 修复 UI 或 GPU 阶段

“8 个大核”这一说法需要分别核实:总核心数、在线核心数、性能核心数、线程被允许运行的核心数,以及采样窗口内真正活跃的核心数。这五项的含义不同。只有当 Cpus_allowed_list 或系统 cpuset 确实把 Raster 限制在两个核心时,才能确认存在亲和性限制;如果允许范围很宽,“只看到两个核心”本身不足以证明问题。

再区分亲和性限制、串行工作和上游等待

先比较进程级和线程级的 Cpus_allowed_list。如果整个进程都被限制在少数核心,问题通常来自系统 cpuset、调试脚本或启动配置;如果进程范围正常而某个 Raster TID 被单独收窄,则应继续检查应用、插件或 Engine 内部是否修改过亲和性。taskset -pc 显示的是线程当前允许使用的核心范围,不代表线程此刻实际运行在多少个核心上。

亲和性正常时,还要看调度条件。Perfetto 中的唤醒到运行延迟、线程切换、CPU idle、频率变化和抢占记录,能够区分 CPU 繁忙、线程优先级竞争、后台任务干扰、功耗模式以及温控降频。Raster 优先级高并不意味着应该继续提高它;先确认线程为什么没有及时运行,再决定是否调整应用负载。

串行化边界取决于当前 Flutter Engine、渲染后端和具体帧结构。资料对 Flutter、Android 渲染路线的概括是以直接光栅化为主,光栅化和合成同步执行;Chromium 常见的异步分块光栅化则允许光栅与合成异步进行。 这可以帮助理解传统实现边界,但不能代替对目标 Flutter 版本和实际渲染后端的核对。手机有八个核心,不代表同一帧会自动被拆给八个 worker。

上游等待也容易被误判为 Raster 性能问题。应通过 Trace slice 和线程栈确认 Raster 是否在等待 UI 线程产出图层树、上一帧提交或 GPU 同步条件。图片解码如果发生在其他线程或资源加载阶段,就不应未经证据直接计入 Raster 耗时。

首屏卡顿和持续掉帧也应分开处理。首屏优先检查图片解码、资源缓存和首次绘制;持续掉帧则检查复杂图层、过度绘制、离屏缓冲、线程竞争及温控。长时间测试后逐渐掉帧时,还应记录功耗模式、CPU 频率与温度变化。Profile 正常而 Release 异常,或者不同设备结果差异明显时,需要同时记录 Flutter SDK、Engine revision、渲染后端和启动参数,不能把一台设备的结论直接套到另一台设备。

按证据修复,并用帧指标验收

如果确认是亲和性或 cpuset 限制,修复方式取决于限制来源。由调试脚本或人为设置产生时,应恢复合理范围;由应用或 Engine 自身设置时,应优先升级版本或修正对应实现;由系统或 OEM 策略造成时,应用侧不宜长期硬绑核心,而应降低渲染负载并保留系统调度空间。

不建议把“手动把 Raster 绑定到大核”作为通用修复方案。手动绑定可以作为受控实验,但可能让 Raster 与其他性能核任务直接竞争。只有在证据证明存在非预期限制,并且 A/B 数据证明绑定后帧耗时稳定改善时,才值得保留。

调度竞争和温控问题应从持续负载入手。设备已经降频时,增加线程或开启并发不能绕过系统限制;更有效的方向通常是减少无效绘制、降低同时发生的资源峰值,并排除后台任务干扰。

Engine 和渲染后端差异需要按版本核对。Flutter 官方文档提供了升级稳定版、切换发布通道以及获取历史 SDK 的路径。 如果目标版本确实提供并发光栅化能力,应按照该版本文档进行单独 A/B,保留设备、场景和构建模式不变。旧文章中的具体开关名和默认值不能直接照抄,发布前应再次核实。

Raster 工作量过大时,应减少不必要的可见图层、过度绘制、复杂裁剪和阴影,以及没有明确收益的离屏缓冲。资料指出,为图层分配独立像素缓冲区会引入额外缓冲和间接光栅化成本,因此应把 RepaintBoundary、Relayout Boundary 视为边界控制工具,而不是“自动并行光栅”的开关。

首屏资源压力可以通过预取缓解,例如:

await precacheImage(
  const AssetImage('assets/banner.png'),
  context,
);

precacheImage 可以提前准备即将使用的图片资源,降低首次使用时的解码与加载压力。 但它不会让 Raster 自动使用更多核心,也不能替代对首屏绘制成本的测量。如果 Trace 显示 UI 或 GPU 提交才是主要瓶颈,应优先修复对应阶段。

验收时应在相同设备和场景下比较 Raster 耗时、总帧耗时、掉帧情况、线程状态分布、亲和性范围、CPU 频率和温度变化,并且每次只修改一个变量。对于热榜中的“8 核设备、Raster 看起来只跑在 2 个核”案例,完整的前后对照至少应记录设备实际核心拓扑、Flutter SDK 与 Engine 版本、渲染后端、Trace 中的 Raster TID 与线程状态、唯一修复动作,以及 Raster 耗时和掉帧指标。原标题没有提供的具体版本、命令和数值,不应自行补写。

CPU 占用更高不是验收标准。负载优化后 Raster 利用率进一步下降,同时帧耗时和掉帧改善,往往是正常且理想的结果。

可对相同场景录制系统 Trace,确认 Raster 的 TID、线程状态和 Cpus_allowed_list;每次只改变一个限制因素,再复测同一组帧指标。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容