查看: 198|回复: 0

RNOH滑动卡顿丢帧定位与优化:Profiler帧泳道实战

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
为什么单看平均 FPS 定位不到滑动卡顿

在 HarmonyOS 上运行的 RNOH 应用,滑动丢帧指的是滑动过程中画面刷新频率没达到流畅标准,表现为停顿、抖动或延迟。《应用性能体验建议》对滑动场景提出了两项要求:滑动操作响应快,以及滑动过程卡顿率为 0ms/s;AppAnalyzer 已支持或等效支持上述指标检测。当应用出现明显停顿、抖动,或快速滑动时冒出空白块,就需要先判断丢帧环节,再有针对性地优化。

很多同学第一步会去看平均 FPS,但平均 FPS 只能描述整体趋势,无法反映最差帧和连续丢帧。真要定位问题,应优先关注 App Frame 与 RS Frame 中的超时帧(红色帧)、卡顿率与累计丢帧时间、最大连续丢帧、Present Fence 或实际显示帧间隔,以及同一异常窗口内主线程、RNOH_JS 和 Render Service 的状态。另一处容易误判的是空白块:白块不一定等于丢帧,页面可能按时产帧,只是列表项数据或图片尚未填充完成,此时帧泳道正常,问题属于内容准备延迟,需要同时检查帧泳道与内容更新时间再决定走丢帧还是内容准备的分析路径。

帧预算要按实际刷新率算,不能写死 60Hz

帧预算由问题窗口内的实际目标刷新率决定:
  1. 单帧预算(ms) = 1000 / 目标刷新率(Hz)
复制代码

60Hz、90Hz、120Hz 对应的理论预算约为 16.7ms、11.1ms、8.3ms。设备会动态切换刷新率,定位时应从 Display、H:PreferredFrameRate 或等价系统信息读实际目标刷新率,固定按 60Hz 判断会得出错误结论。

先分清两类丢帧:AppDeadlineMissed 与 RenderDeadlineMissed

HarmonyOS 图形系统采用统一渲染模式:应用侧在每个 Vsync 周期内完成输入事件处理、动画、组件状态更新、Measure、Layout 与 Render,把界面描述数据提交给 Render Service;Render Service 在 Vsync 信号触发下执行动效、描画与提交,协调 GPU 合成后送屏显示。应用侧或 RS 侧任一环节超时,用户都会看到丢帧。

由此形成两类故障模型:AppDeadlineMissed 指应用没在帧截止时间前完成处理并提交,常见于应用逻辑效率低,RNOH 应用里还包括 JS 线程负载过重、Fabric 提交与挂载超时;RenderDeadlineMissed 指应用按时提交但 Render Service 处理超时,通常源于界面结构复杂或 GPU 渲染负载高。这两类问题在 DevEco Profiler 的 Frame 模板里可以直接通过 App Frame 与 RS Frame 泳道的先后关系区分。需要说明的是,RNOH 应用的应用侧横跨主线程与 JS 线程:React 渲染、布局计算和 diff 在 JS 线程执行,组件挂载和 ArkUI 绘制在主线程执行,所以定界时两条线程都要看。

六步定位流程与采集前准备

完整的排查路径可以固化为六步:用 AppAnalyzer 识别滑动场景是否存在性能问题并拿到体检报告;在 DevEco Profiler Frame 泳道确认丢帧率与最大连续丢帧;通过 App Frame 与 RS Frame 的异常先后做初步定界;结合线程状态、调用栈与组件泳道定位耗时环节;沿 RNOH 滑动链路按责任域深入;实施优化并复测,未达标就回到第四步重新分析。

采集前的准备往往决定数据是否可用:使用 release 构建和 release bundle,关闭 Metro、HMR、Inspector、高频日志和性能浮层后再正式采集;固定设备、系统与 RNOH 版本、屏幕刷新率策略、数据、网络和操作方式;预热页面后重复相同滑动至少 5 次,保留基线与问题数据;在业务侧为滑动开始、关键数据更新或吸顶切换打 trace 点,方便与系统帧对齐。只有原始 .htrace 时要保留原文件,能导出数据库时优先用数据库做可复核查询。

还要注意两个工具的适用边界:性能浮层的 JS FPS/UI FPS 是 500ms 轮询的趋势指标,适合观察持续负载,不适合定位几十毫秒量级的单次丢帧;AppAnalyzer 在页面存在视频播放或动画补帧时可能检测不到滑动组件丢帧(例如对视频播放页的评论区做滑动检测),此时应以 DevEco Profiler 的 trace 结果为准。

抓 trace 与确认丢帧的具体操作

在 DevEco Studio 的 Profiler 中选择目标和 Frame 模板创建会话后,录制前要确认两件事:一是 RNOH trace 编译开关 WITH_HITRACE_REACT_MARKER、WITH_HITRACE_SYSTRACE 已开启,否则 trace 里不会出现 ReactMarker::* 与 #RNOH::* 标记;二是需要查看 ArkUI Component 泳道时必须手动勾选,录制中无法补开。随后启动录制,在设备上执行预热后的重复滑动,结束后保存 trace。

分析时在时间轴上框选一段区间(例如 2.5 秒),选中 Frame 主泳道,在 Statistics 栏读取丢帧数与丢帧率。绿帧是正常渲染,红帧表示卡顿——期望结束时间点之前的部分为浅红,超出部分为深红,黄色为异常帧。选中超时帧可看到期望处理时间(1000ms/帧率,120Hz 约 8.3ms)与实际处理时间。框选时要通过滑动开始 trace、输入事件或录屏对齐同一次滑动,取最差帧或最大连续丢帧区间,并记录绝对时间戳、换算相对时间;不同次滑动的数据不要拼接分析。

定界:应用侧超时还是系统合成侧超时

App Frame 与 RS Frame 都异常时,不能因为应用线程存在长任务就直接判定为应用负责,应当比较时间顺序、帧依赖和调度状态。排除系统侧时先看线程状态与运行核:持续 Running 说明确实在执行长任务,沿调用栈找热点;长时间 Runnable 说明就绪但拿不到 CPU,要检查整机竞争、QoS 与温控;休眠或阻塞则要看锁、同步 TurboModule、跨线程等待或 I/O,其中 IO 不可中断睡眠态表示线程被 I/O 或磁盘操作阻塞,低内存触发 page fault 也会出现该状态。若出现执行频率偏低、线程长期跑在小核(多核设备通常按频率区分大中小核)、频繁在 Running 与 Runnable/Sleep 之间切换、不重要线程占用大核等情况,可在线提单反馈系统异常。

确认属于应用侧后,再逐层看帧内耗时:RNOH_JS 线程是否存在覆盖异常窗口的长任务(长段 ExecuteJs、onScroll 业务计算),事件是否持续排队;主线程的 didMount、组件创建、属性更新、ArkUI measure/layout/flush 是否集中超时;ReactMarker::FABRIC_COMMIT/DIFF/LAYOUT 与 #RNOH::MountingManager::doMount、didMount 的时序关系是否错位。ArkTS 侧热点函数通过 ArkTS Callstack 泳道查看(双击可跳转源码),Native 侧高耗时函数切到 Callstack 泳道,JS 侧长任务结合调用栈或火焰图定位,组件绘制耗时则看录制前勾选的 ArkUI Component 泳道。

这里有一条判断纪律:线程高负载本身只说明存在负载,只有该负载位于异常帧的关键路径并推迟了该帧提交,才能作为根因证据。

沿 RNOH 滑动链路判断 JS 是否真的在关键路径上

RNOH ScrollView 的 scroll 事件路径决定了 JS 负载是否影响滑动:ArkUI 自身可以持续滚动,不要求 JS 每帧返回结果;Animated 的滚动事件在 ArkUI 侧旁路处理,不经过 JS。因此 RNOH_JS 负载高并不必然造成滚动丢帧,只有当滚动视觉效果依赖 JS 计算、列表补项或 React 更新时,JS 延迟才进入关键路径。

ScrollViewComponentInstance::onEmitOnScrollEvent() 决定一次滚动是否向 JS 分发 scroll 事件,满足任一条件即分发:即将到达 scrollTo 目标位置(这时要让 JS 感知,否则滚动无法正确停止),或距上次分发已达到 scrollEventThrottle 设定的间隔且发生显著位移。当前实现中 scrollEventThrottle 为 0 会放开时间间隔门控,但同帧内的重复事件仍会被合并,不会一帧发多次;滚动位置的内部状态回写另有约 51ms 的节流,这与业务 onScroll 回调频率不是一回事。scrollEventThrottle 不建议固定为 16ms——在 120Hz 屏幕上约等于每两帧分发一次,应结合业务是否需要逐帧 JS 回调、回调耗时和目标刷新率实测确定;仅用于动画联动时优先交给 ArkUI 驱动,减少 JS 参与逐帧渲染。

Fabric 与挂载侧要注意:FABRIC_UPDATE_UI_MAIN_THREAD_* 只包围 synchronouslyUpdateViewOnUIThread,不能用来代表所有 transaction 的主线程挂载时长。commit/diff/layout 很长时优先检查 JS 更新与组件树规模;这些阶段按时完成但 didMount 或 ArkUI measure/layout 超预算时,优先检查 mutations 数量、组件创建成本和 ArkUI 布局。

案例一:onScroll 回调过重

现象是列表惯性滚动时连续丢帧、滚动联动动画落后于手势,120Hz 设备上某个超时帧期望约 8.3ms、实际处理 22.8ms。定位时 App Frame 先异常而 RS Frame 正常,定界到应用侧;帧内详情显示 RNOH_JS 持续执行 14.6ms,位于 onScroll 回调内;展开调用栈发现回调里对列表数据做同步排序,并为每个事件创建大对象,JS 任务结束后 Fabric commit 才开始,该帧自然错过截止时间。

优化动作有三点:把排序移出 onScroll,改为数据变化时执行一次并缓存结果,埋点、预取等非必要逻辑挪到 onMomentumScrollEnd 或异步任务;避免在回调内创建大对象、解析大 JSON 或触发多次状态更新;滚动联动动画改用 ArkUI 驱动(useNativeDriver: true),插值计算不再经过 JS。复测后该帧处理时间回到 8.6ms,连续丢帧消失,关闭排序逻辑前后的对照数据确认了因果关系。

案例二:列表项重渲染过多

长列表快速滑动掉帧明显、FPS 波动剧烈,基线中同样滑动距离的渲染窗口为 700ms,问题窗口达到 1700ms。trace 显示每个 scroll 事件后紧跟成串的 FABRIC_COMMIT/DIFF/LAYOUT,提交频率约为基线的 3 倍;React DevTools Profiler 显示滚动期间 Item 的 render 次数同样是基线的 3 倍。根因是业务把滚动位置写进了页面级 state,整个列表跟着重渲染,加上 renderItem 内联定义、onPress 使用匿名函数,React.memo 全部失效。

优化后只把滚动位置写入真正依赖它的子组件(如吸顶指示器),列表项用 React.memo 包裹,onPress 用 useCallback 缓存、数据引用用 useMemo 保持稳定,renderItem 提取为独立函数,并合并状态更新缩小每次提交的组件范围。复测渲染窗口降至 760ms,Item render 次数回到基线,期间 commit 成串的现象消失。

案例三:列表虚拟化配置不当导致快速滑动白块

现象是快速滑动时新出现的列表项为空白,约 480ms 后内容才补齐,帧泳道正常,属于内容准备延迟。定位发现 Fabric batch 中 create 数量集中出现,随后 ArkUI measure 明显增长,说明新列表项创建与测量跟不上滑动速度;列表项高度为动态计算、未提供 getItemLayout,windowSize 保持默认,离屏组件大量驻留。

优化按收益逐步叠加并分三档复测:仅调小 windowSize,空白时长从约 480ms 降到约 300ms;对固定尺寸的列表项提供 getItemLayout,让 RN 直接按索引计算偏移、跳过异步测量,initialNumToRender 只渲染首屏数量,maxToRenderPerBatch 控制每批补项规模,这两项配置后空白块消除、补项耗时降到约 90ms;removeClippedSubviews 能进一步减少离屏 ArkUI 节点参与绘制,但在复杂变换下有显示风险,需 A/B 验证后再启用。动态高度场景若无法提供准确的 getItemLayout 尺寸,不应强行配置,应先固定列表项布局。

优化验证的原则

每个结论至少要通过一种对照手段验证:关闭目标逻辑、把负载降为 1/2、用本地数据替换网络请求、用占位图片替换真实图片、移除三方组件或切换 RNOH 版本。如果处理某个长切片后丢帧没有同步下降,说明该切片不在当前丢帧的关键路径上,或者瓶颈已经转移,应重新比较异常窗口。上述任一现象单独出现都不足以认定是 RNOH 框架问题,框架问题的判定仍需回到基础篇的定界方法与常见错误标准。

滑动卡顿的排查本质上是先确认帧预算、再定界应用侧与系统合成侧、最后沿 RNOH 的 JS 与主线程两条链路找关键路径耗时。工具给的是数据,对照验证给的才是结论。
回复

使用道具 举报

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

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

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

官方核心成员

关注微信公众号

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

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

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部