查看: 308|回复: 0

鸿蒙Flutter卡顿定位:UI与Raster线程trace分析

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
背景:Flutter 跑在鸿蒙上,卡顿要先能“看懂一帧”

Flutter 应用运行在鸿蒙(OpenHarmony)平台上时,性能问题通常表现为滑动卡顿、点击无反馈、页面加载迟缓。这类现象往往并不指向单一原因,而是分布在 Flutter 引擎、Dart 业务代码和系统 RenderService 合成链路的不同环节。要把问题压到具体阶段,需要两条线索:一是理解 Flutter 自身的渲染流程与线程模型,二是掌握 trace 的采集与解读方法。

在工具层面,鸿蒙侧可以用 DevEco Studio Profiler 的 Frame 模板做系统级 trace 分析,Dart 侧则用 Flutter DevTools 做帧级与事件级分析。两者定位不同、互为补充:先用 Profiler 定界问题归属(UI 线程还是 Raster 线程),再用 DevTools 深入 Dart 侧定位具体函数,这条路径比直接埋头看火焰图更高效。

前置条件与工具版本

开展分析前需要满足两个条件:设备侧为 HarmonyOS 5.0 及以上版本,且前台运行的是 Flutter 页面;工具侧准备 DevEco Studio Profiler 与 Flutter DevTools 两套。原文所用环境为 Flutter 3.27.5-ohos-1.0.7,版本差异会影响线程模型与 trace 泳道的呈现方式,分析前建议先确认版本。

线程模型:先分清谁在干活

Flutter 内部有四类线程,各自承担不同职责,trace 上的泳道也按此划分。需要特别注意一个版本变化:从 Flutter 3.29 起支持 UI 线程与平台线程合并,合并之后,UI 线程中原有的 trace 点都会出现在平台线程泳道中。如果照旧在 ui 泳道里找 UI 线程的耗时点却找不到,首先要排查的就是这一点,而不是怀疑抓取失败。

一帧的完整链路:从 vsync 到上屏

Flutter 采用“UI 线程构建图层树 → Raster 线程光栅化 → 系统合成上屏”的渲染模型,一次完整帧渲染可分三段看。

第一段是帧调度。当输入事件到达、动画 tick 触发或 setState 将 Element 标记为 dirty 时,Flutter 通过 Animator::RequestFrame 向系统请求 vsync 信号;系统在下一个 vsync 到来时回调 Animator::BeginFrame,UI 线程开始一帧的工作。

第二段是 UI 线程构建图层树。UI 线程收到 vsync 后执行四个子阶段,最终输出一棵 LayerTree。这四个子阶段均在 UI 线程同步完成,任何一个阶段超时,都会体现为该帧 UI 线程耗时过长。构建完成的 LayerTree 会通过 Scene 提交给 Engine,交由 Raster 线程消费。

第三段是 Raster 线程光栅化。Raster 线程拿到 LayerTree 后执行 GPURasterizer::Draw,过程包含三步:通过 binder 向 RenderService 进程申请 buffer 用于存储渲染结果(对应 ReuseBuffer 标识);遍历图层树,把绘制指令交给图形库(Skia/Impeller)执行,生成 GPU 指令并写入 buffer;渲染完成后释放 buffer,交由系统合成。Raster 线程的耗时通常由 UI 线程产出的图层复杂度决定,saveLayer 过多、复杂 Shader、大量图片上传都会拖慢光栅化。

最后是系统合成上屏。RSMainThread::DoComposition 在 RenderService 主线程从 buffer 中获取帧数据(对应 acquire buffer),合成渲染树上各节点图层;随后 RSUniRenderThread/RenderFrame 由 GPU 执行最终绘制,帧上屏。

这里有一个明确的判据:当 UI 线程与 Raster 线程的总耗时超过一帧预算时,就会导致 RenderService 缺帧,表现为掉帧卡顿。帧预算随刷新率变化,120Hz 下为 8.33ms,90Hz 下为 11.1ms。

关键 trace 点要按渲染顺序看

做性能分析时,需要按渲染流程顺序逐个关注关键 trace 点,用它们确定各阶段的起止边界。前文已经点到的几处是最常用的锚点:Animator::RequestFrame 与 Animator::BeginFrame 对应帧调度,flutter::GPURasterizer::Draw 对应光栅化,RSMainThread::DoComposition 对应系统合成,flutter::DefaultPointerDataDispatcher::DispatchPacket 则用于观察点击事件的分发耗时。同样要记住线程合并的影响:UI 线程与平台线程合并后,UI 线程原有的 trace 点都会出现在平台线程泳道中。

DevEco Studio Profiler 实操:抓取与收藏

Profiler 是 DevEco Studio 提供的场景化调优工具,其中 Frame 模板可以录制应用运行过程中的关键数据,用于深度分析卡顿丢帧、耗时长等问题。在 Flutter 场景下,抓取步骤如下:

第一,使用 release 或 Profile 构建模式安装应用并连接设备;

第二,打开 DevEco Studio 的 Profiler,选择设备、目标应用与进程,选择 Frame 模板,单击 Create Session 创建会话;

第三,单击启动按钮开始录制,在设备上执行待分析的操作,完成后停止录制并保存 trace 文件;

第四,抓取完成后,收藏应用进程的 ui、raster 线程以及 RenderService 主线程泳道,便于后续反复查看。

收藏泳道看似是操作细节,实际上决定了分析效率:Flutter 侧与系统侧的耗时点分处不同进程,不提前固定观察窗口,很容易在长 trace 中反复横跳。

两个标识符:把一帧串起来

掌握流程之后,还需要掌握流程各单元之间的匹配方法,为每个单元找到合适的标识符,才能在 trace 上正确串联一帧的完整渲染过程。

第一个标识是 frame_number,用于建立 ui 与 raster 线程之间的联系。收藏这两个线程并关联观察,就会在 trace 上找到该标识符,它是 Flutter 应用帧渲染的第一个标识符。如果 frame_number 没有同时出现在 ui 和 raster 中,说明当前帧不是 Flutter 自渲染,此时需要重新定界性能问题,而不是继续在 Flutter 侧深挖。

第二个标识是 ReuseBuffer / acquire buffer,用于建立 raster 与 render_service 之间的联系。在 raster 线程 flutter::GPURasterizer::Draw 的 trace 过程中,会向 RenderService 进程申请 buffer 内存用于存储渲染的帧内容,点击 binder transaction 的 trace 可以跳转到另一线程查看对应的 ReuseBuffer 编号;而在 render_service 线程 RSMainThread::DoComposition 的 trace 过程中,会获取 buffer 内存里的帧数据,其 acquire buffer sequence 中的编号与 raster 线程申请的 ReuseBuffer 编号一致。通过这两个标识,就能识别 Flutter 应用到 RenderService 进程上的帧渲染链路。需要说明的是,RenderService 进程上后续单元目前没有直观的 trace 可供识别,这是当前分析能力的边界。

定界规则与一个点击卡顿案例

抓取 trace 并收藏关键泳道后,按帧渲染流程定界问题归属。基本规则是:一帧渲染流程中 ui 与 raster 的总时长超过一帧时间,问题即可定界为 Flutter 侧问题。曾经出现过 RenderService 缺失帧的现象,但根因是 UI 线程单帧耗时较长、超过了 120 帧率下 8.33ms 的一帧时间——从 ui 线程代码执行开始到 raster 线程代码执行结束的总时长超标,就需要继续定位具体阶段,而不是把问题归给系统合成。

具体定位过程可以用一次 Flutter 页面点击卡顿为例。第一步,用 Profiler 的 Frame 分析能力抓取该过程的 trace,根据 mmi_service 泳道的 trace 点找到页面滑动过程,并点击 Frame 泳道下的子泳道 Display Vsync,可以看到该过程的屏幕刷新率为 89,因此 Flutter 页面各线程的单帧处理时间需控制在 11.1ms(1000/90)以内,否则就会出现滑动卡顿。第二步,查看页面滑动附近 ui 线程的 trace,发现点击操作后 flutter::DefaultPointerDataDispatcher::DispatchPacket 耗时 828ms,远超 11.1ms。第三步,在 Callstack 泳道的 ui[65405] 子泳道中查看 ui 线程单帧绘制时的调用栈,耗时主要集中在 libflutter.so,但对应的是 ui 线程中的点击事件,由此判断问题点在 Dart 代码点击事件的业务逻辑耗时太长。

这个案例的方法论价值在于顺序:先由刷新率换算帧预算,再由线程耗时判断是否超预算,最后由调用栈把耗时落到具体代码域。三步都基于 trace 事实,而不是经验猜测。

DevTools:从系统侧转到 Dart 侧

DevEco Studio Profiler 侧重系统侧 trace 分析,Flutter DevTools 侧重 Dart 侧帧级与事件级分析。使用方式是:连接真实设备,以 Profile 模式运行 Flutter 应用;运行成功后,用浏览器打开日志中输出的 DevTools 调试地址即可。在命令行中按 P 可以切换应用中 performance overlay 视图的显示或隐藏。

性能图层显示 Raster 线程(上)和 UI 线程(下)的叠加图,垂直的绿色条代表当前帧。红色竖条表明当前帧的渲染和绘制都很耗时;当两张图表都是红色时,应优先对 UI 线程(Dart VM)进行诊断。

进入 DevTools 的 Performance 界面后,当 Flutter frames 视图中出现红色或耗时较长的帧,可以这样操作:点击有问题的帧,下方的时间线事件区域会同步更新,显示该帧内发生的所有事件;在时间线中可以看到按线程(如 UI 线程、GPU 线程)分类的事件流,事件以色块形式展示,色块宽度代表其耗时;随后寻找耗时最长的事件,UI 线程中异常宽的色块(例如一个 build 事件)通常就是瓶颈所在,悬停或点击事件即可查看详细信息与调用栈。

应用层高频问题速查

UI 线程侧,最常见的是 Build 阶段耗时过长:火焰图中 Build 相关色块又宽又大,通常意味着 build() 方法里执行了耗时同步操作(如大数据排序、JSON 解析),或触发了超大范围的不必要重建。排查方法是开启 Track Widget Builds 选项,Timeline 中会显示具体是哪个 Widget 的 build 方法被调用,再结合 CPU Profiler 的火焰图或调用树定位到具体耗时函数。另一类是 Layout 阶段耗时,可能源于嵌套层级过深,或使用了 IntrinsicHeight/Width 这类需要多次测量布局的组件。

Raster 线程侧,saveLayer() 调用过多是性能下降的头号元凶,它会创建离屏缓冲区、非常消耗性能。DevTools 的 Frame Analysis 标签页会直接给出类似“Canvas.saveLayer() was called 16 times during this frame”的警告。应用层原因通常是过度使用 Opacity、ClipPath、ShaderMask 或复杂的半透明重叠效果,这些都会触发隐式的 saveLayer() 调用。此外,复杂的图形效果(过度使用 PhysicalModel 阴影、大量图片绘制等)同样会拖慢光栅化。

方法总结

把上面的内容压缩成一条可复用路径:先确认 HarmonyOS 版本、Flutter 版本与构建模式(Profile/Release),避免因线程合并等版本差异误读泳道;用 Profiler 的 Frame 模板抓 trace 并收藏 ui、raster、RenderService 主线程三条泳道;用 frame_number 验证是否为 Flutter 自渲染,用 ReuseBuffer/acquire buffer 打通到 RenderService 的链路;按刷新率换算帧预算,比较 ui 与 raster 的总耗时完成定界;定界到 Flutter 侧后,再切到 DevTools 用 Performance 视图和 performance overlay 逐帧寻找最宽事件,最终落到具体 Widget 的 build、布局或图层绘制上。工具各管一段、结论逐层收敛,是这套流程在鸿蒙 Flutter 性能问题上最值得复用的部分。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-21 12:59 , Processed in 0.021767 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部