Android TLHC 场景下,Flutter WebView 滚动由约 70 FPS 升至 120 FPS

看到“Flutter WebView 滚动从 70 FPS 提升到 120 FPS”,很容易误以为只是把设备刷新率切到 120 Hz,或者修改了 FPS 统计上限。实际并非如此:这项优化对应已经合并的 flutter/flutter#195861,调整的是 Android TLHC 混合合成的任务时序,减少 WebView 离屏绘制与主窗口合成之间的竞争。资料中的测试场景因此由约 70 FPS 提升到 120 FPS。

核心原因也不是“WebView 绘制突然变轻了”,而是旧链路会让 WebView 的重型 DrawFrame 提前占用进程内共享的 RenderThread,主窗口同步只能等待。合并后的改动改变了排队关系;Pixel 9 Trace 中,主窗口同步耗时减少约 3.7 ms,而 120 Hz 屏幕每帧只有 8.33 ms,这段时间原本会直接挤压 Flutter 页面可用的帧预算。

70 FPS 升到 120 FPS,究竟改变了什么?

70 到 120 FPS 是资料所记录的 TLHC WebView 快速滚动场景中的观测结果,不应扩大解释成“所有 Flutter WebView 升级后都会增加 50 FPS”。它既不表示 WebView 自身突然获得了两倍算力,也不意味着所有设备、所有页面都能稳定跑满 120 FPS。

热点标题把模式写作“THLC”,而资料正文使用的是“TLHC”。本文统一写作 TLHC(Texture Layer Hybrid Composition,纹理层混合合成),不把这一拼写差异视为两种模式;最终仍应以 flutter/flutter#195861、所用引擎版本和运行时 Trace 为准。

资料支持以下三项结论及其边界:

结论 适用范围 不能据此推出
TLHC WebView 滚动由约 70 FPS 提升至 120 FPS 资料中的 Android 合成时序和测试场景 所有 Flutter WebView 都有相同增益
主窗口同步等待减少约 3.7 ms Pixel 9 Trace 单独增加 3.7 ms 就必然对应固定帧率增益
120 Hz 是值得追求的目标 设备、系统、合成路线和页面负载均满足条件 每个 WebView GPU 任务都低于 8.33 ms,或输入延迟完全消失

从工程角度看,70 到 120 FPS 是完整渲染链路改善后的结果,不能简单用帧率增量反推某个函数节省了多少时间。3.7 ms 是主窗口同步阶段的观测值,实际帧率还取决于 Flutter UI 线程、GPU 光栅化、WebView 内容负载、纹理帧交付时机以及设备调度。

为什么 WebView 一滚动就可能拖住整个页面?

旧版 TLHC 的关键绘制链路可以还原为五步。

  1. Android 主线程调用 PlatformViewWrapper.draw()。
  2. 通过 lockHardwareCanvas() 取得离屏 Surface 的 Canvas。
  3. 调用 super.draw(),将 WebView 及其子 View 的内容绘制进去。
  4. 立即调用 unlockCanvasAndPost(),提交这次离屏绘制。
  5. 通知 Flutter 有新的纹理帧可以使用。

第 4 步中,unlockCanvasAndPost() 看起来只是提交 Canvas,但提交之后,Android RenderThread 还要消费这些绘制命令并执行相关 GPU 工作。也就是说,方法返回不代表 WebView 的离屏帧已经真正完成。

Android View 绘制大体可以分为两个阶段:主线程上的 View#draw 负责遍历 View、记录绘制命令,随后由 RenderThread 执行 DrawFrame。TLHC 又多了一层离屏 Surface,如果它在主窗口遍历尚未结束时就被提交,WebView 的重型绘制任务就会先进入 RenderThread 队列。

Android 并不会为每个 HardwareRenderer 都准备一条独立 RenderThread。WebView 离屏绘制和应用主窗口最终会争用进程内同一条 RenderThread,资源冲突如下:

旧时序:WebView 离屏 Surface 提前提交

Android 主线程   记录主窗口 → 提交 WebView 离屏 Surface → 主窗口同步并等待
RenderThread                       WebView DrawFrame 占用资源 → 主窗口绘制工作

当主窗口随后进入同步阶段时,RenderThread 可能仍在处理 WebView 的重任务。主窗口同步路径需要等待 RenderThread 响应,Android 主线程因此被连带阻塞,最终影响同一帧中的 Flutter 内容与 WebView 合成。

因此,WebView 离屏帧的提交顺序会改变整个窗口的合成节奏,不只影响 WebView 自身的滚动。

新合成时序怎样把主窗口同步耗时压下来?

这次优化针对的是任务排队关系,而不是通过降低 WebView 分辨率、关闭动画或限制帧率来换取流畅度。合并后的关键效果,是让 WebView 的重型 DrawFrame 不再抢先占住共享 RenderThread,从而减少主窗口同步阶段的额外等待。

旧、新时序的因果关系可概括为:

旧链路:
WebView DrawFrame 提前入队
        ↓
RenderThread 被占用
        ↓
主窗口同步等待,Android 主线程被拖住

优化后的链路:
WebView DrawFrame 不再提前抢占主窗口所需的工作
        ↓
主窗口同步少等待约 3.7 ms
        ↓
Flutter 页面获得更多帧预算

这只是逻辑时序,不是函数级实现说明。参考资料没有披露 flutter/flutter#195861 的完整 diff,因此不应根据标题推测具体新增了哪些 API、回调或调度方法。真正的实现细节应以 PR 为准,能够确认的结论只有两个:WebView 离屏绘制不再以相同方式提前阻塞主窗口同步,Pixel 9 Trace 中的同步等待减少了约 3.7 ms。

3.7 ms 在 120 Hz 环境中并不是可以忽略的时间。每帧总预算只有 8.33 ms,主窗口同步多消耗几毫秒,就可能挤掉 Flutter UI、图层合成以及纹理上屏的时间。它解释了为什么提帧收益会很明显,但不能据此认为 70 到 120 FPS 是由单个数值线性换算出来的。

同样,120 FPS 只描述最终输出帧率,不表示 WebView 的每一个 GPU 任务都控制在 8.33 ms 内,也不代表输入延迟、页面加载抖动和所有长帧都已经消失。帧率、平均耗时和交互响应需要分别观察。

HC、TLHC 和独立 Surface 合成应该怎样选?

Flutter 与 Android 原生 View 的合成路线不同,性能收益和兼容成本也不同。资料给出的三条路线可以按合成主体、同步位置和适用条件来比较,而不应只根据模式名称判断。

合成路线 内容如何参与合成 主要优势 主要代价 更适合的判断条件
Android View 先绘制成纹理,再交给 Flutter 合成 原生内容进入纹理,由 Flutter 参与最终合成 Flutter 侧性能较好,合成链路相对统一 WebView 快速滚动曾是已知性能问题 页面内容较轻,且能够确认纹理合成路径没有成为瓶颈
传统 HC:保留 Android 原生 View 层级 原生 View 留在 Android View 层级,由 Android 合成 原生兼容性较好 存在 Flutter 线程合并开销 原生交互和兼容性优先,同时 Trace 证明 HC 路径可以接受
Flutter 与原生内容分别使用 Surface,再由系统合成 Flutter 和原生内容各自使用 Surface,由系统完成合成 可减少传统 HC 的同步开销 存在明确的设备和系统要求 目标设备支持相应能力,并且所用 Engine 包含相关优化

这三条路线并不存在脱离场景的绝对优劣。HC 的原生兼容性更好,但线程合并开销并没有因为 TLHC 的改进而自动消失;独立 Surface 路线减少了部分同步成本,其可用性取决于设备和系统能力;先绘制为纹理、再交由 Flutter 合成的路线在 Flutter 侧表现较好,但 WebView 快速滚动正是此次优化针对的历史问题。

因此,模式选择不能只问“哪个理论性能更高”,而要先确认:

  • 目标设备实际上进入了哪条合成路线;
  • 当前 Flutter Engine 是否包含 flutter/flutter#195861;
  • WebView 内容和原生交互是否给 HC 增加了不可接受的兼容成本;
  • Trace 中主窗口同步、RenderThread 和纹理帧交付是否仍存在明显冲突。

这次提帧需要满足哪些设备和系统条件?

要复现这项改动,Flutter 引擎版本是首要条件。必须确认项目所用 Engine 已经包含 flutter/flutter#195861;参考资料没有给出对应的稳定版版本号,因此不能直接写成“某个 Flutter 版本及以上”。PR 已合并,也不等于所有历史稳定版或项目当前锁定的引擎都会自动包含它。

设备与系统能力决定独立 Surface 路线是否可用。资料明确提到该路线存在设备和系统要求,但没有提供完整的 Android 版本、GPU 或机型清单,因此更可靠的做法是查询目标机型支持情况,并在真机 Trace 中确认实际合成路径,而不是自行补一份并不存在的最低版本矩阵。

现有证据仅来自 Android:它依赖 Android 主线程、RenderThread、离屏 Surface 和 Pixel 9 Trace。这套结论不能直接外推到 iOS、桌面端或 Web 平台,因为这些平台的原生内容合成路径不同,需要单独分析。

WebView 内容负载仍然是变量。普通 HTML 与包含较重 GPU 绘制任务、WebGL 3D 或液体动画的页面,对 RenderThread 和 GPU 的压力并不相同;即使主窗口同步等待已经减少,内容本身过重仍可能留下长帧。

兼容性还受以下条件约束:

维度 有利条件 风险或未知项 验证方式
Flutter/Engine 版本 已包含 flutter/flutter#195861 仍使用旧引擎或锁定版本未包含改动 查看 flutter --version 中的 Engine revision
合成模式 实际走 TLHC/Surface 路线 实际回退到传统 HC,收益不能直接套用 在系统 Trace 中确认合成路径
设备与系统 支持相应 Surface 和系统合成能力 不支持、被厂商策略限制或行为存在差异 查目标机型支持情况,并在多台真机上抓 Trace
WebView 内容 普通页面或 GPU 负载可控 WebGL、复杂动画、频繁纹理更新 使用固定脚本做前后对照压力测试
Flutter UI 线程 BUILD/LAYOUT/PAINT 耗时稳定 Dart 逻辑过重,形成独立瓶颈 使用 DevTools Performance 检查
平台 Android Android 之外的合成机制未被这组资料覆盖 换平台后重新建立基线

这张矩阵的重点不是找到一个“最低配置”,而是避免把版本、模式和设备条件混在一起。只要其中一项没有确认,120 FPS 都只能算目标,不能算结果。

如何验证项目是否接近 120 FPS?

验证时首先需要固定基线,而不是直接比较两个不同项目里的平均 FPS。至少记录设备型号、系统版本、当前刷新率、Flutter 与 Engine revision、WebView 版本、页面内容和测试手势。版本信息可以先用下面的命令采集:

flutter --version
flutter devices
adb shell devices
adb shell dumpsys display

dumpsys display 的字段会随 Android 版本和厂商变化,需要在输出中确认设备支持的刷新率和当前显示模式,不要依赖某个固定字段名。

随后应使用目标真机,在系统允许的最高刷新率模式下测试。Profile 构建比 Debug 构建更接近实际渲染开销:

flutter run --profile

系统 Trace 用于确认这次优化是否生效。录制时只做固定范围内的重复滚动,并沿用 Pixel 9 Trace 的定位思路,重点检查以下事件:

  • Android 主线程上的主窗口同步;
  • RenderThread 上的 WebView DrawFrame;
  • WebView 离屏 Surface 的提交与处理;
  • 新的纹理帧到达 Flutter 的时间;
  • 主窗口同步结束与整帧提交之间的关系。

如果优化前能看到“WebView 重型 DrawFrame 占用 RenderThread,主窗口随后进入同步并等待”,而优化后主窗口同步不再被这段工作额外阻塞,才能说明改动确实作用于目标路径。仅看到顶层 FPS 数字变化,无法排除测试环境、刷新率或页面内容变化带来的干扰。

DevTools 用于判断卡顿是否来自 Dart UI 线程。Flutter Performance 可以查看 CPU、GPU、build、layout、paint 等阶段耗时;资料中的 Flutter 调优案例也将 BUILD、LAYOUT 和 PAINT 作为 Dart 层排查重点。 如果系统 Trace 显示合成阻塞已经缓解,但 DevTools 中 UI 线程仍频繁出现长任务,就应转而检查 Flutter 状态更新、布局重建和自定义绘制,而不是继续调整 WebView 合成参数。

压力测试需要覆盖不同内容。除普通长页面外,可以加入 WebGL 3D、液体动画和快速连续滚动场景,观察优化前后是否仍存在 RenderThread 长任务;这些内容可用于检验 GPU 负载较重时的边界。

最后不要只看平均 FPS。至少同时记录单帧时间、卡顿发生位置、系统 Trace 和 DevTools 数据。60 Hz 每帧预算约为 16.6 ms,120 Hz 约为 8.3 ms,平均值可能掩盖少量但明显的交互长帧。 资料 中的 60 Hz、120 Hz 帧预算可用于理解时间尺度,但其中的 HiAppEvent、HiLog 和滑动丢帧上报流程属于 HarmonyOS DFX,不能直接当作 Android RenderThread 的诊断工具。

升级后仍然卡顿时,如何定位瓶颈?

先检查 Engine 是否包含合并改动。如果仍在使用旧引擎,先解决版本问题,否则后续的模式分析缺少前提。

随后确认实际合成模式。如果设备因为系统能力或厂商策略没有走 TLHC/Surface 路线,而是使用传统 HC,就不能把仍存在的卡顿简单归因于“#195861 优化无效”。需要先从 Trace 判断它究竟处于哪条路线。

最后将系统 Trace 与 DevTools 放在同一时间轴上判断:

观察结果 更可能的方向 后续动作
Engine 不包含改动 版本问题 升级或替换 Engine 后重新测试
实际走 HC,Flutter 线程合并开销明显 合成路线问题 对比 HC 与可用 TLHC 路线的 Trace
RenderThread 持续被 WebView DrawFrame 或重型 GPU 工作占用 WebView 内容或离屏绘制问题 缩小页面、减少复杂 GPU 内容和纹理更新压力
DevTools 中 BUILD/LAYOUT/PAINT 占用明显 Dart UI 线程问题 检查高频 setState、复杂布局和昂贵绘制
卡顿集中在首次加载或动态更新 加载与脚本处理问题 拆分静态内容和动态脚本
平均帧率尚可但交互仍不顺 输入、长帧分布或局部卡顿 补充单帧时间与卡顿位置数据

“Trace 忙而 DevTools 正常”通常意味着主要瓶颈不在 Flutter Dart 层。此时应继续从 WebView 页面负载、GPU 绘制和纹理帧交付成本入手,而不是把问题简单归因于 Widget 树。

反过来,如果 DevTools 显示 BUILD、LAYOUT 或 PAINT 忙碌,问题也可能完全独立于 WebView 合成。Flutter 页面滑动卡顿可以来自构建、布局或绘制耗时,不能把所有滚动长帧都归到 RenderThread。

加载和动态更新造成的卡顿需要单独排查。将静态 HTML 与脚本分开,并通过 evaluateJavascript 注入动态脚本,可以减少运行时 Base64 转换和重复编码的成本;但这属于加载优化,不能替代 RenderThread 时序修复。 同样,tree shaking 和延迟加载可以作为一般 Flutter 性能优化手段,但现有资料并未证明它们能够解决此次 WebView 共享 RenderThread 争用。

这次优化解决了什么,又留下了哪些边界?

这次合并解决的是一个因果链明确的问题:在特定 Android TLHC 路径中,WebView 离屏绘制提前抢占共享 RenderThread,继而拖慢主窗口同步;调整时序后,Pixel 9 Trace 中的主窗口同步等待减少了约 3.7 ms。

它没有消除所有 WebView 性能问题。Flutter/Engine 版本、实际合成路线、设备与系统能力、WebView GPU 负载以及 Dart UI 线程状态,都会决定最终能否稳定接近 120 FPS。

因此,验收时不应只问“有没有达到 120 FPS”,而应检查“版本—模式—设备—内容—Trace 数据”这五项是否一致。满足这组条件后,才能把 120 FPS 作为可验证的目标;条件未确认时,它只是一个需要实测的预期值,而不是 Flutter WebView 的普遍承诺。

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

请登录后发表评论

    暂无评论内容