查看: 4427|回复: 3

鸿蒙Flutter滑动丢帧:UI线程与Raster线程瓶颈定位

[复制链接]
发表于 6 天前 | 显示全部楼层 |阅读模式
用户反馈“页面卡”,十有八九指的是滑动或动画不连贯。在 Flutter 里这类现象对应丢帧(Jank):某一帧的生成耗时超过帧预算,屏幕只能继续显示上一帧。60Hz 屏幕的预算约 16.6ms,90Hz 约 11.1ms,120Hz 约 8.3ms。本文以 OHOS 平台为例,梳理一套用 DevEco Profiler 抓取 trace、圈定滑动区间、判定瓶颈线程、再对症修复的完整流程。

一、先分清 UI 线程与 Raster 线程

Flutter 渲染一帧大致要经过构建、布局、绘制、栅格化合成几个阶段,落到线程上只有两条是开发者必须盯住的:UI 线程运行 Dart VM,执行 Dart 代码和框架代码,产出 Layer tree;Raster 线程接收 Layer tree,调用 Skia 或 Impeller 完成栅格化与合成,最后送屏。

UI 线程慢说明 Dart 层有问题;Raster 线程慢说明绘制太重,比如阴影、模糊、大图、saveLayer。分析时必须把两条线程的耗时一起看。

在 OHOS 上有个容易踩的坑:从 Flutter 3.35 起,UI 线程可能与应用主线程合并成同一条线程,线程名直接是应用包名(例如 .complex_layout),归类为 platform_main 而不是 flutter_ui。这时靠线程名判断 UI 线程会直接判错。正确做法是在 trace 中搜索 flutter::Animator::BeginFrame,该 slice 所在的 tid 才是 UI 线程。

二、在 trace 里圈出“滑动窗口”

DevEco Profiler 抓下来的 trace 往往长达数秒到数十秒,中间混杂启动、空闲、滑动、停止等多个阶段。不先把滑动区间圈出来,后面的丢帧定位就没有意义。

应用代码可以主动埋点 flutter::APP_LIST_FLING 这个滑动生命周期标记。如果 trace 中存在该 slice,它覆盖了手指拖滑加惯性滑动的完整过程,直接取它的 start 与 end 时间戳作为后续分析窗口即可。

需要注意,这个标记不一定完全覆盖实际滑动:滑动开始前埋点未触发,或滑动结束后惯性仍在继续,都会导致窗口偏差。此时要结合触摸事件链把窗口向外扩展。

三、定位丢帧位置

在滑动窗口内找丢帧有两种方法,优先用第一种。

方法一,搜索 flutter::SceneDisplayLag。Flutter 提供了这条 trace 直接标识渲染管线中的丢帧,在滑动窗口里搜到它就能定位丢帧时间点,不必逐帧比对耗时。

方法二,看 render_service 提供的两条 buffer 消费泳道。把这两条泳道收藏起来,找它们不重合或中间出现空隙的时间点。

锁定最慢那一帧之后,先确认 UI 线程与 Raster 线程记录的 frame_number 是否相同,确保看的是同一帧;再以该帧时间戳为中心收窄窗口,分别查看两条线程在窗口内的 slice,谁最耗时,瓶颈就在谁身上。两条线程的耗时并没有绝对基准,要结合慢帧的指标分布来判断。

若结论在 UI 线程:按 UI 线程 tid 过滤 slice,重点看 BUILD、LAYOUT、PAINT、Animate、BeginFrame、PlatformConfiguration 这些名称,确认哪个阶段最重。若抓 trace 时没有开启 --trace-systrace,这些阶段 slice 可能缺失,需要重新采集。想进一步确认耗时原因,可以把数据交给 Flutter 自带的 DevTools 细看。

若结论在 Raster 线程:按 Raster 线程 tid 过滤 Flutter/Impeller 相关 slice,观察其出现情况与耗时,再回代码核对是否存在 saveLayer、模糊、阴影这类高开销绘制操作。

四、根因一:ListView 不定高,布局测量被放大

结论先说:列表项没有固定高度时,Flutter 必须逐项测量高度,快速滑动时布局成本被成倍放大,最终表现为丢帧。

现象特征是长列表快速滑动时丢帧,低端机尤其明显;每出现一个新 item,UI 线程的 LAYOUT 就会变长。Trace 上的判定信号,是慢帧窗口内 UI 线程 LAYOUT 耗时明显偏高。

代码层面的验证很直接:检查 ListView.builder 或 ListView.separated 有没有提供 itemExtent 或 prototypeItem。高度固定就直接给 itemExtent:
  1. ListView.builder(
  2.   itemCount: itemCount,
  3.   itemExtent: 136, // 固定高度,跳过逐项测量
  4.   itemBuilder: (context, index) => _buildItem(index, fixedHeight: true),
  5. )
复制代码

高度确实不固定,也可以用 prototypeItem 提供一条高度样板,让框架按样板估算。改完后回到同一条 trace 上复采对比,看 LAYOUT 是否回到正常水平。

五、根因二:过度绘制与离屏缓冲压垮 Raster 线程

结论同样先给:动画或滑动过程中大量使用 saveLayer、Opacity、模糊、阴影时,Raster 线程绘制负载过重,同样会丢帧。

现象特征很好区分:静态看一切正常,一滑动或一播动画就卡;UI 线程耗时正常,只有 Raster 线程耗时长。

排查方向是逐个检查可见 item 及其动画子树里有没有这些东西:Clip.antiAliasWithSaveLayer、动画中直接使用的 Opacity Widget、大面积的 BackdropFilter 或 ImageFiltered 模糊、复杂的 BoxShadow,以及该加 RepaintBoundary 却没加的高频重绘区域。

修复思路:把 Opacity 换成 FadeTransition,让透明度变化走合成层而不是每帧产生 saveLayer;圆角优先用 Clip.hardEdge;缩小或移除阴影与模糊;高频重绘区域补上 RepaintBoundary。
  1. // 不推荐:Opacity 每帧变化,会在 paint 阶段触发 saveLayer
  2. Opacity(opacity: _animation.value, child: child)
  3. // 推荐:FadeTransition 在合成层调整 opacity,不产生 saveLayer
  4. FadeTransition(opacity: _animation, child: child)
复制代码

改完之后同样要复采对比:Raster 线程慢帧窗口内的 slice 耗时是否下降,是最直接的验证方式。

六、把排查流程固化成团队习惯

这套方法最好在团队里标准化下来:先用 APP_LIST_FLING 圈出滑动窗口,再用 flutter::SceneDisplayLag 找丢帧,确认 frame_number 一致后收窄窗口,按 tid 分流到 UI 线程或 Raster 线程,最后对号入座排查 itemExtent 缺失与 saveLayer 滥用这两类高频根因。多数滑动卡顿问题会在前两步就暴露出来,剩下的只是按图索骥地验证与修复。
回复

使用道具 举报

发表于 6 天前 | 显示全部楼层

Re: 鸿蒙Flutter滑动丢帧:UI线程与Raster线程瓶颈定位

这套排查流程整理得挺清楚,尤其是 UI 线程和主线程合并之后改用 flutter::Animator::BeginFrame 来定位 tid 这一点,确实容易踩坑。我刚开始接触 OHOS 上的 Flutter 时也是看线程名去找,结果一直找不到 flutter_ui,浪费了不少时间。 补充一点自己实践中的体会:itemExtent 和 prototypeItem 的选择上,如果列表项高度差异很大,prototypeItem 估算出来的位置和实际会偏差,滚动条偶尔会跳,这种场景我一般倾向于强制把高度固定下来,或者用 SliverList 配合估算更准的布局。另外 RepaintBoundary 也不是加得越多越好,包得太细反而会增加合成层的数量,我在一个卡片列表里每项都加了之后,Raster 线程反而没降下去,后来只给真正高频变化的子树加才有效果。 关于复采对比,我觉得这一步最容易被忽略,很多人改完代码凭感觉觉得“好像快了”就收工了,其实同一条 trace 上前后对比才能说明问题。文章里把 APP_LIST_FLING 圈窗口、SceneDisplayLag 找丢帧固化下来,这个习惯养成了排查效率会高很多。感谢分享,回头准备把这几条整理成组内的自查清单。
回复 支持 反对

使用道具 举报

发表于 6 天前 | 显示全部楼层

Re: 鸿蒙Flutter滑动丢帧:UI线程与Raster线程瓶颈定位

感谢分享,这篇把丢帧排查从抓 trace 到修复验证串起来了,整体流程很清晰。尤其两个点很受用:一是 OHOS 上从 Flutter 3.35 起 UI 线程可能和应用主线程合并,不能只靠线程名判断,要用 flutter::Animator::BeginFrame 所在 tid 来确认;二是先用 APP_LIST_FLING 圈滑动窗口,覆盖不全再结合触摸事件链扩展,然后用 flutter::SceneDisplayLag 定位丢帧,比逐帧比对省事很多。 两个根因的判定信号也讲得比较明确:UI 线程慢就看 LAYOUT 是否异常偏高,对应 ListView 不定高,用 itemExtent 或 prototypeItem 去解;Raster 线程慢就回查 saveLayer、Opacity、模糊和阴影,用 FadeTransition、Clip.hardEdge、RepaintBoundary 这些手段收敛绘制开销。最后强调在同一条 trace 上复采对比,这个闭环很关键,不然改完很难证明真的有效。 想请教一下,团队落地时会不会把“改前改后同场景复采对比”作为提测标准?另外低端机复测覆盖一般怎么安排比较稳妥?整体很实用,收藏了。
回复 支持 反对

使用道具 举报

发表于 6 天前 | 显示全部楼层

Re: 鸿蒙Flutter滑动丢帧:UI线程与Raster线程瓶颈定位

这篇总结很实用,流程讲得比较完整,尤其是把“页面卡”拆成 UI 线程和 Raster 线程两条路径来定位,排查思路很清楚。里面提到的几个点很有价值:从 Flutter 3.35 起 UI 线程可能和应用主线程合并,不能只靠线程名判断,要用 flutter::Animator::BeginFrame 所在 tid 来确认;先用 APP_LIST_FLING 圈滑动窗口,再用 flutter::SceneDisplayLag 找丢帧,比逐帧比对更高效;确认慢帧后还要核对两条线程的 frame_number 是否一致,避免看错帧。根因部分也很有针对性,ListView 不定高导致 LAYOUT 变长,以及 saveLayer、Opacity、模糊、阴影压垮 Raster 线程,都是滑动场景里常见又容易被忽略的问题。修复建议里用 itemExtent 或 prototypeItem、把 Opacity 换成 FadeTransition、补 RepaintBoundary,再复采同一条 trace 对比验证,这个闭环很值得团队固化下来。整体看下来,这套方法不仅能解决单次丢帧,也适合作为团队排查滑动性能问题的标准动作。
回复 支持 反对

使用道具 举报

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

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

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

官方核心成员

关注微信公众号

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

GMT+8, 2026-10-27 17:24 , Processed in 0.033129 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部