鸿蒙开发5.0【List的滑动丢帧性能问题】分析思路&案例

1. 场景导入

基于ArkUI的List组件实现的滚动列表视图,在手指抛滑场景下,通过分析掉帧情况来判断List滑动是否流畅,保障用户极致流畅体验。

2. 性能指标

最大连续丢帧数:指从页面开始有响应变化到页面结束刷新的过程中,由于显示器画面刷新频率低于预设的画面帧率而未能正常呈现的最大连续帧数。一般而言,当连续值超过3时,用户可以明显感知到卡顿掉帧,数值越大卡顿时间越长。最大连续丢帧数越接近于0,用户流畅性体验越好。

2.2 性能衡量起止点介绍

以大于300mm/s的速度,连续3次抛滑,每次半屏。抓取滑动过程Trace,查看Frame泳道中应用进程和RenderService的最大连续丢帧数。

List组件的抛滑过程,可以通过应用进程下的H:APP_LIST_FLING泳道标识。性能衡量的起点为第一次抛滑开始点,衡量的结束点为第三次抛滑的结束点。

1

泳道描述
H:APP_LIST_FLING从手指按下开始拖动到抬手后的惯性滚动及最后尾动效的抛滑全过程。
H:touchEventDispatch拖滑阶段,从手指开始拖动到抬起。
H:TRAILING_ANIMATION抛滑尾动效阶段

Tip:尾动效阶段系统会进行降帧处理,所以如果要统计FPS情况,通常只会统计从抛滑开始到尾动效起点的这一阶段

3. 问题定位流程

3.1.1 查看操作录屏辅助定位

处理三方应用问题时,可以优先查看操作录屏,查看操作场景,看能否发现一些有助于定位的信息,比如卡顿的页面布局情况、卡顿的现象等等。

3.1.2 Trace 抓取

滑动帧率Trace抓取请参考【附录1: 滑动帧率Trace抓取方法】。

3.2 问题定位思路

滑动丢帧类问题的通用定位思路为先确认抛滑的起止点,然后看抛滑过程中最大连续丢帧数,如果大于0帧,则根据Trace信息进一步确认问题点,确认责任领域并对齐处理,处理流程如下图:

2

3.2.1 确认起止点

参考【2.2 性能衡量起止点介绍】

3.2.2 找问题点

3.2.2.1 判断丢帧进程

首先通过Frame泳道判断丢帧进程,其中绿色代表没有丢帧,其他颜色均为丢帧。其中“粉红色”代表该帧的期望时间,“红色”标识超时部分。

应用进程问题

如下图,应用进程连续丢了5帧,但可以看到只有261帧、263帧、264帧耗时较长,因此只分析这3帧即可。另外发现最后一帧序号也是264,这是因为前一帧耗时较长,导致该帧和前一帧都提交到了RS的264帧上,应用帧上的序号是被提交到的RS帧序号相对应的。

3

RS 进程问题

RS进程丢帧可能是应用进程导致的,如上图RS侧丢了1帧,但可以看到RS侧264帧丢帧原因是由于应用进程的264帧耗时较长,提交较晚导致。所以这种情况只分析应用侧丢帧原因即可。如果应用进程中没有丢帧且每帧耗时比较均匀,但是RS侧发生丢帧,则说明不是应用侧导致丢帧,此时只分析RS进程丢帧原因即可。

4

3.2.2.2 找丢帧Trace

选中Frame泳道,点击Statistics下面的应用进程右侧图标进入Frame List

5

过滤Jank Type为AppDeadlineMissed类型的帧,点击跳转应用进程。

6

详细分析丢帧Trace

7

3.2.3 根因分析方法

应用侧的渲染流程如下图所示,了解ArkUI的渲染流程有助于我们定位应用侧的卡顿问题出现在哪个环节

8

阶段描述
Animation动画阶段,在动画过程中会对相应的组件标记脏区
Events事件处理阶段,比如手势事件处理。在手势处理过程中也会对组件标记脏区
UpdateUI组件在首次创建或状态变量变更时会标记为需要rebuild状态,在下一次Vsync过来时会通过View的方法生成相应的组件树结构和属性样式修改任务。
Measure执行组件的大小测算任务。
Layout执行组件的布局任务。
Render执行绘制任务,执行完成后会标记请求刷新RSNode绘制
SendMessage将绘制数据提交到RS侧,请求刷新界面绘制

应用进程丢帧分析

跟据Trace图,初步分析耗时较长阶段。261帧由于懒加载组件预创建耗时较长导致丢帧;263帧由于组件复用耗时长丢帧;264帧由于组件结构复杂嵌套层级多导致丢帧。

9

序号所属泳道Trace点描述
1应用进程H:LazyForEach predictLazyForEach预处理
2应用进程H:CustomNode:BuildRecycle 自定义组件名自定义组件的复用,包含执行aboutToReuse方法的耗时
3应用进程H:CreateTaskMeasure[组件名][self:组件id][parent:父组件id] && H:Measure[组件名][self:组件id][parent:父组件id]执行组件的布局测量任务

通过ArkTS CallStack泳道,可以看到应用侧具体调用栈,进一步分析定位问题原因。如下图通过调用栈可以分析出:组件复用时会组件树进行递归,这个过程耗时较长,可以看下组件数是否组件嵌套层级过深;updateDirtyElement耗时长,应用侧可以分析下是否存在冗余节点被触发更新;aboutToReuse耗时长可以看下应用侧该回调中是否存在耗时逻辑。

10

应用UI组件树的嵌套情况,可以通过ArkUI Inspector查看。

11

经验总结: 应用进程丢帧通常是组件结构嵌套层级深、耗时应用业务逻辑阻塞UI线程等问题导致。如果是UI结构复杂问题可以让应用通过减少嵌套层级、使用组件复用等方式优化。如果是有耗时业务逻辑,则可以通过将耗时逻辑放到Taskpool或Worker中优化。

RS 进程丢帧分析

RS进程丢帧一般是由于界面结构过于复杂或者GPU负载过大等原因导致的。如果应用侧没有丢帧且每帧耗时比较平均,则可以初步判断应用侧没有问题,同时也可以通过应用侧Trace中H:SendCommands下的H:MarshRSTransactionData cmdCount查看应用提交的绘制指令树是否过多。如下图RS侧丢帧原因是由于RS侧的H:RSUniRender:FlushFrame阶段耗时较长,此时可以找图形子系统进一步确认耗时根因。

12

经验总结: RenderService侧丢帧通常是应用侧UI线程阻塞提交绘制指令较慢导致,此时应当初步定位应用侧耗时长原因。如果应用侧无丢帧情况,绘制指令正常提交,则可以找图形子系统协助进一步分析丢帧原因。

4. 典型问题

4.1 耗时任务阻塞UI 主线程

Stage模型下的线程主要有三类:主线程、TaskPool、Worker。主线程主要用于执行UI绘制、处理应用代码逻辑,TaskPool和Worker的作用是为应用程序提供一个多线程的运行环境,用于处理耗时的计算任务或其他CPU密集型任务。当主线程存在耗时的计算任务时,会使主线程阻塞,导致应用丢帧。

4.1.1 问题根因分析

应用进程中间有一段大段“空白”,UI线程未提交任何绘制指令,同时CPU10大核却处于Running状态,表示此时应用侧正在执行ArkTS的业务代码。按每帧8.3ms算,这里阻塞了75.9ms,丢了9帧左右。

13

通过ArkTS Callstack泳道,可以看到耗时点主要在三个文件:FlowApi.ets、BaseFeedFlowListVM.ets、Mapi.ets。

与伙伴确认业务逻辑主要为:在列表滑动过程中,List将要到达底部时,会通过网络请求会获取到一个博文的列表数据,数据量较大。然后在FlowApi.ets、Mapi.ets中会对数据进行转换处理,处理后的数据通过BaseFeedFlowListVM.ets文件再转换成

14

4.1.2 优化方案

在List滑动过程中对数据进行处理耗时较长,占用大量CPU资源,导致主线程被阻塞,这部分数据处理的相关业务逻辑与UI绘制无关,但却长时间占用CPU资源,导致UI线程被阻塞丢帧。可以将该数据处理逻辑放到TaskPool中利用多核并行化处理优化。

除应用侧的耗时逻辑外,某些与UI绘制无关的耗时系统接口调用也可以放到TaskPool中优化。

4.2 @Prop 传参深拷贝耗时长

4.2.1 问题根因分析

观察应用Trace发现Measure阶段的H:CustomNode:BuildItem [MediaCard]耗时较长4ms 661μs,通过观察ArkUI Component泳道,得出自定义组件MediaCard构建耗时较长。

15

通过ArkTS CallStack观察应用ArkTS调用栈。其中observeComponentCreation2为@Observed传参时相关逻辑调用栈。resetLocalValue、copyObject、deepCopyObject、deepCopyObjectInternal、getDeepCopyOfObjectRecursive为@Prop接收参数时拷贝数据的相关调用栈。

图片

由此分析得出:应用侧MediaCardComponent.ets文件中的自定义组件MediaCard通过@Observed+@State方式声明了某个对象类型的数据,并将该变量传给了子组件MediaImage,但在MediaImage中并未使用@ObjectLink接收该变量,而是使用@Prop接收,导致该状态变量发生了深拷贝,深拷贝过程耗时较长3ms 339μs。

4.2.2 优化方案

@Prop装饰器存在性能问题,@Prop装饰的变量会对父组件传入状态值进行深拷贝,如果@Prop装饰器装饰的变量为复杂Object、class或其类型数组时,会增加状态创建时间以及占用大量内存。如果需要观察嵌套类对象的深层属性变化,推荐选择@State+@Observed+@ObjectLink组合方案。

4.3 UI 复杂导致单帧超长

4.3.1 问题根因分析

List滑动场景中,应用侧单帧耗时较长91.8ms,导致应用丢帧。通过Trace可以看到在这一帧中创建了大量组件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.3.1.1 组件复用失效

首先,观察Trace发现,在ListItem的Measure阶段,出现了大量H:CustomNode:BuildItem,说明此时发生了大量自定义组件重新创建,而没有被复用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

经与应用讨论分析,应用的单个ListItem包含4个部分(头像、文本、九宫格、底部按钮)。ListItem的结构非固定的,其子组件可能会出现多种情况,如图文、视频、纯文本等等,动态性较高。同时又因为其复用是以整个ListItem为单位,所以在进入复用池时ListItem会存在多种可能性。导致在新ListItem期望复用创建时,在复用池中可能未找到对应的可被复用组件,自定义组件被重新创建,组件复用失效。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.3.1.2 嵌套层级深

通过ArkUI Inspector观察UI组件树结构,可以发其中存在大量自定义组件的__Common__节点(如下图红框),且存在容器之间组件冗余嵌套的情况(如下图绿框)。冗余的嵌套会带来不必要的组件节点,加深组件树的层级,在创建和布局阶段会产生较大的性能开销。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.3.1.3 冗余的状态变量

框选该帧并筛选Trace点:H:ViewPU.viewPropertyHasChanged,该Trace点表示状态变量发生了更新,其中后三个参数分别为自定义组件名、自定义的状态变量名、该状态变量更新后影响的组件数量。可以发现这里有大量为“0”的Trace点,表示该状态变量更新时未触发任何组件刷新,即该状态变量未绑定UI组件,因此可以将其改为普通变量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.3.2 优化方案

4.3.2.1 组件复用失效优化

细化组件复用的颗粒度,将原来对ListItem的复用改为对ListItem中各部分子组件复用,提高组件复用成功率。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

相关修改代码参考:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.3.2.2 嵌套层级深优化

方案一: 当自定义组件设置通用属性后,UI组件树就会产生__Common__节点,可以通过属性内移解决。将自定义组件上设置的属性内移到自定义组件中的第一层系统组件上。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

方案二: 避免冗余的嵌套,对于这类冗余的容器,应该尽量优化,减少嵌套深度。建议采用相对布局RelativeContainer进行扁平化布局,有效减少容器的嵌套层级,减少组件的创建时间。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

方案三: 使用@Builder代替@Component自定义组件。通过@Component声明的组件在创建时会产生额外耗时,建议尽量使用@Builder声明组件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.3.2.3 冗余的状态变量优化

将没有跟UI组件绑定的状态变量改为普通变量。@State、@Prop等装饰器修饰的状态变量在创建、Get、Set时都会产生耗时,因此应该尽量减少冗余的状态变量,避免性能损耗。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

附录1 :滑动帧率Trace 抓取方法

Step1 电脑连接上设备,在DevEco Studio上打开Profiler

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Step2 设备上运行需要测试的应用,在设备列表选择设备,选择要测试的应用,和主进程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Step3: 创建Frame模板,并点击录制,待所有泳道都进入到recording状态后

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Step4 执行相关滑动操作

Step5 操作完成,点击结束录制,待分析完成后,可以在泳道上看到trace数据

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Step6 trace的路径点击Help -> Show Log in Explorer

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

返回到上一层,找到.insight文件下

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

附录2 :List 滑动场景通用Trace 点说明

基础List 滑动

以一个最基础的List Demo为例,通过脚本抛滑并使用IDE Profiler工具抓取Trace。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

拖动阶段

选取拖动阶段某一Trace如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

序号Trace描述参数说明
1H:client dispatch touchId:32903系统派发touch事件事件id
2H:OnVsyncEvent now: [时间戳]收到Vsync信号,渲染流程开始时间戳–纳秒级
3H:FlushVsync处理用户输入、刷新视图同步事件、计算帧信息、提交绘制渲染等
4H:DispatchTouchEvent id:0, pointX=[x坐标] pointY=[y坐标] type=2处理拖拽手势事件触摸点的xy坐标信息,type=2表示手势事件类型为移动
5H:HandleDragUpdate执行拖拽更新任务
6H:AddDirtyLayoutNode[List][self:7][parent:6]标记List组件为脏区组件名、组件ID、父组件ID
7H:UITaskScheduler::FlushTask刷新UI界面,包括布局计算、渲染和提交等
8H:FlushLayoutTask执行布局任务
9H:CreateTaskMeasure[List][self:7][parent:6] && H:Measure[List][self:7][parent:6]执行List组件的布局测量任务组件名、组件ID、父组件ID
10H:ListLayoutAlgorithm::MeasureListItem:27 && H:Measure[ListItem][self:10][parent:7][key:]计算ListItem列表项的布局尺寸列表项索引、组件名、组件ID、父组件ID
11H:SkipMeasure组件大小布局未发生变化,跳过measure过程
12H:CreateTaskLayout[List][self:7][parent:6] && H:Layout[List][self:7][parent:6]执行List组件布局任务组件名、组件ID、父组件ID
13H:Layout[ListItem][self:10][parent:7][key:]执行ListItem组件布局任务组件名、组件ID、父组件ID
14H:SyncGeometryNode[List][self:7][parent:6][key:]同步几何节点组件名、组件ID、父组件ID
15H:FlushRenderTask 1 && H:FrameNode[List][id:7]::RenderTask执行渲染绘制任务当前页面需要绘制的节点数量、需绘制的组件名和ID
16H:FlushMessages && H:SendCommands通知图形侧进行渲染
17H:MarshRSTransactionData cmdCount:14 transactionFlag:[24390,75]向图形侧发送绘制指令绘制指令数量、应用进程号、指令序列号
18H:OnIdle, targettime:222549467458334Vsync中的空闲,一般会用来做预加载之类的操作,当H:OnVsyncEvent时间小于某值时触发该事件时间戳

惯性滑动阶段

惯性滑动阶段Trace点与拖动阶段基本一致,唯一区别点在于拖动阶段是通过手势事件标脏,而惯性滑动阶段是通过动画触发的组件标脏。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

序号Trace描述参数说明
1H:RunningCustomAnimation num:[1]自定义动画,RSModifierManager管理的在UI线程运行的动画num表示动画的数量,如果大于0,则表示有动画在运行
2H:AddDirtyLayoutNode[List][self:7][parent:6]标记List组件为脏区组件名、组件ID、父组件ID
3H:FlushDirtyNodeUpdate更新被标脏的节点

尾动效阶段

尾动效阶段相较其他两阶段的区别点,在于尾动效阶段如果移动距离小于1像素则不会向RS提交H:SendCommands。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

选取一个应用进程未提交帧,放大后可以看到H:SendCommands下方并无H:MarshRSTransactionData的Trace点。无该Trace点时说明ArkUI中没有需要绘制的内容,因此没有提交绘制指令到RenderService侧。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

懒加载场景滑动

基于上文List Demo改造,实现懒加载效果。List懒加载场景下滑动的Trace点与上文基础List基本一致,其区别点主要在于懒加载的滑动场景下会出现组件创建和销毁过程,因此本章节主要介绍创建和销毁组件的关键Trace点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

创建组件

当List组件的cachedCount属性设为0时,ListItem的创建会发生在H:FlushLayoutTask阶段。如果不为0,则会在H:OnIdle阶段预创建组件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

序号Trace描述参数说明
1H:Builder:BuildLazyItem [4]创建一个LazyItem项目创建的项目索引
2H:Create[Child][self:28]创建一个自定义组件自定义组件名、组件ID
3H:CustomNode:OnAppear && H:aboutToAppear执行自定义组件的aboutToAppear 方法
4H:CustomNode:BuildItem [Child][self:28][parent:27]执行自定义组件的build方法自定义组件名、组件ID、父组件ID
5H:Create[Text][self:31]创建一个Text组件组件名、组件ID
6H:Measure[Text][self:31][parent:27][key:]计算Text组件布局组件名、组件ID、父组件ID
7H:LazyForEach predictLazyForEach预处理
8H:List predictList组件预处理

销毁组件

组件销毁只会发生在H:OnIdle空闲阶段。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

序号Trace描述参数说明
1H:LazyForEach predictLazyForEach预处理
2H:aboutToDisappear执行自定义组件的aboutToDisappear方法
3H:aboutToBeDeleted删除组件

组件复用场景滑动

基于懒加载场景Demo改造,实现组件复用效果。与懒加载场景相比,在滑动过程中不会发生组件的销毁和创建,而是会在组件将要销毁时,将其放入缓存池中,在需要创建时再从缓存池中取出,并重新赋值。因此本章节主要介绍Reuse阶段和Recycle阶段关键Trace点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Reuse 阶段

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

序号Trace描述参数说明
1H:CustomNode:BuildRecycle Child自定义组件复用,执行aboutToReuse方法复用的组件名
2H:Create[Text][self:12]创建Text组件组件名、组件ID
3H:AddDirtyLayoutNode[ListItem][self:61][parent:0]标记ListItem为脏区组件名、组件ID、父组件ID

Recycle阶段

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

序号Trace描述
1H:LazyForEach predictLazyForEach预处理
2H:aboutToRecycleInternal标识组件进入复用池并调用组件的aboutToRecycle方法
3H:ViewFunctions::ExecuteRecycle执行组件回收
  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值