滑动页面占位符加载完成时延问题分析思路&案例
1. 场景导入
滑动页面占位符加载完成时延:可滚动页面中,滚动停止开始算起,到屏幕内占位符(一般为图片)加载完成。
2. 性能指标
2.1 性能衡量起始点介绍
数帧工具:Avidemux 2.6 - 32 bits (32-bit);推荐时间:40ms。
通过视频抓取滑动停止为起始点:
通过视频抓取占位符加载完成为终止点:
根据终止点事件减去起始点事件计算完成时延:5161-4671 = 490ms。
3. 问题定位流程
3.1 常规定位前置流程
3.1.1 查看操作录屏辅助定位
处理三方应用问题时,可以优先查看操作录屏,查看操作场景,看能否发现一些有助于定位的信息,比如滑动过程中是否存在图片加载动画,是否包含网络请求等等。
3.1.2 Trace抓取
冷启动Trace抓取请参考【附录1: Trace抓取方式】。
3.2 问题定位思路
滑动操作占位符完成时延类问题的通用定位思路为先确认时延起止点,然后看起止点时延是否超40ms(40ms为推荐的加载完成时延),就需要进一步分析Trace看看耗时主要发生在什么地方,主要分析网络耗时和帧渲染耗时,最后确定是系统问题还是三方问题。处理流程如下图:
3.2.1 确认起止点
加载完成时延起始点:APP_LIST_FLING终点视为滑动停止,则是加载完成时延起始点。滑动页面占位符加载完成,是以滑动停止为起始点,在Trace中APP_LIST_FLING泳道可以体现滚动视图的FLING惯性滚动状态的起止,惯性滚动停止则滚动停止,此时开始计算占位符加载时延。
查找步骤:
- 搜索APP_LIST_FLING。
- 找到APP_LIST_FLING泳道,星标后即可置顶查看。
- Trace标记了惯性滚动区间。
- APP_LIST_FLING结束点=加载完成完成时延起始点。
加载完成时延终止点:APP_LIST_FLING终点视为滑动停止后,图片加载完成即页面不再发生变化(应用侧不提交Vsync信号到RenderService),则是加载完成时延终止点。滑动页面滚动停止后,会出现两种情形。1,未触发上拉加载,滚动停止后的第一帧,分析异常帧。2,触发上拉加载,分析网络请求,分析异常帧。
精确定位终止点流程应用主线程泳道:H:ReceiveVsync —> H:SendCommandsRender Service泳道: H:RSMainThread::DoCompositionRSHardwareThread泳道:H:Commit。
加载完成时延:起始点与终止点时间间隔。
3.2.2 找问题点
1.如果从应用UI上发现有网络加载的动作,则可以在ArkTS CallStack泳道查找是否发送网络请求,关键Trace点createHttp,继续查找请求响应点off(request),parse数据解析,OnDataReload(LazyForEach刷新数据)来判断请求结束数据刷新时间点。因为在长列表应用中,一般使用分页加载功能实现更多数据,在滚动停止或者将要停止时触发加载更多功能,发送网络请求,收到响应数据后解析并刷新数据源,驱动页面刷新。
2.在FLING结束点,往后查看ArkTS CallStack调用栈,查看耗时任务,如发现耗时任务,则继续查看耗时原因,一般结合应用进程UI主线程查看;如未发现耗时任务(比如idle状态),则查看此时Frame应用侧是否有渲染任务和对应的Component组件情况,一般idle状态应用送帧情况为动画。
3.在FLING结束点,往后查看Frame应用侧帧渲染情况,是否出现异常帧、超长帧。此时一般结合应用进程UI主线程查看具体的组件渲染详情。
泳道 | 问题1 | 问题2 | 问题3 |
Frame/应用线程 | 送帧平滑 | 超长帧 | 异常帧 |
ArkUI Component | 组件极少 | System和Custom组件极多 | System组件居多 |
ArkTS CallStack | 空闲+request | 任务 | 任务 |
异常追踪 | 单一组件动画,后台任务网络请求 | 大量组件创建或刷新渲染 | 系统组件创建或刷新渲染 |
3.2.2 根因分析方法
- 滑动停止有网络请求,则考虑网络时延。
- 滑动停止有出现超长帧、异常帧耗时,考虑复用机制失效或者冗余嵌套渲染时延。
- 列表中Item占位符图片占位符常常加载的是网络图片,则考虑网络时延。
- 占位符图片在加载过程中使用动画,会导致渲染完成时延,比如透明度0到1,缩放比例0到1,则考虑动画时延。
如果想了解更详细的滑动页面占位符加载完成时延的Trace流程解读,见【附录2:场景通用Trace流程点说明】
4. 常见根因归档
4.1 因网络加载导致占位符加载完成时间长
4.1.1 问题场景分析
滑动页面触发上拉加载,在loading动画期间等待数据请求,数据请求完成后刷新列表,占位符加载完成时延不满足标准。
4.1.2 问题根因分析
1.根据起始点确定问题Trace起始点和终止点,如下图加载完成时延总共4s。
2.网络请求数据耗时
根据场景上拉加载更多,数据通过网络请求后刷新,放大Trace找到APP_LIST_FLING尾部,末尾触发request请求数据,即滚动到尾部将要停止时会触发上拉加载,发送请求获取网络接口数据。关键Trace点信息详见【网络关键Trace点】
发送请求request。
发送网络数据请求后,会有Response体现在应用中则是解析后刷新数据,LazyForEach绑定的IDataSource会触发刷新监听,通过OnDataReloaded找出刷新数据Trace点,可得到网络请求耗时495ms。
开始刷新数据OnDataReloaded。
2.网络图片加载耗时本案例中列表中主要占位符为Image组件,加载是通过ImageSource解码生成PixelMap。加载网络图片时,发送图片地址网络请求,接着将返回的数据解码为Image组件中的PixelMap。通过搜索CreateImagePixelMap搜索创建图像像素图,耗时26ms。
加载网络图片资源 CreateImagePixelMap。
4.1.3 优化方案
由于网络时延受多方面因素影响,可尝试优化网络请求和网图加载。
- 采用预请求方式加载更多数据,在快滑动底部某个位置时触发请求,用户无感加载更多,缩减网络请求等待。
- 列表图可适当采用小图模式加载,加速网图资源下载速度。
- Image组件alt属性加载本地占位图,避免占位符在等待网图加载期间空白。
- 图片缓存机制,再次加载网图可从缓存中取图。
4.1.4 问题总结
占位图加载完成时延,一般受首次网络请求时延影响,如果二次加载图片完成实验<=40ms,也算达到预期。
4.2 因组件渲染导致占位符加载完成时间较长
4.2.1问题场景分析
在滚动到底部时,上拉加载更多的网络请求,等待网络请求数据完成后驱动UI刷新。实际测试中发现,上拉加载次数越多,占位图加载完成耗时就越久,可以推断出在加载更多数据后的渲染有异常。
4.2.2 问题Trace特点
1.分析Trace发现列表每次滚动停止触发上拉加载后,会有一个超长帧。
2.分析Trace中应用主线程泳道超长帧,发现有大量组件创建和布局测算。关键Trace点信息详见【UI绘帧关键Trace点】
- 在应用主线程泳道超长帧前,有关键刷新Trace点:OnDataReloaded(LazyForEach通知控制器数据重新加载)。
- 在ArkTS Callstack中查看调用栈发现,js调用notifyDataReload,说明应用侧此时触发了页面刷新。
- 在应用主线程泳道两次超长帧加载LazyItem创建索引可以发现,0-13到0-33,单帧绘制的item越来越多。总结如上3点,结合实际场景上拉加载次数越多,时延越久,说明应用侧使用了全量数据刷新。
详细Trace分析如下:
OnDataReloaded开始触发UI刷新。
查看超长帧,第一次上拉加载更多。
查看超长帧,第二次上拉加载更多。
3.继续分析超长帧,通过应用主线程泳道发现单帧发现有大量BuildItem构建GridItem,而且在懒加载LazyForEach predict中大量aboutToBeDeleted发现析构处理,说明GridItem在滑动过程中被释放。从而分析出列表中子组件未做复用影响性能。关键Trace点信息详见【UI绘帧关键Trace点】
BuildItem构建GridItem。
LazyForEach predict中aboutToBeDeleted析构
GridItem
4.2.3 优化方案
1.可以采用组件复用机制@Reusable优化性能。2,优化LazyForEach的键值刷新规则,采用onDataAdd局部更新。onDataReloaded会通知组件重新加载所有数据,键值没有变化的数据项会使用原先的子组件,键值发生变化的会重建子组件。
4.2.4 问题总结
占位符Image组件加载完成需要通过UI渲染,优化滑动过程中UI组件渲染效率可提高占位符加载完成效率。
4.3 因组件动画导致占位符加载完成时间长
4.3.1 问题场景分析
在滑动列表过程中,占位符图片从无到有的渐变动画。
4.3.2 问题Trace特点
分析Trace滑动过程中的每一帧,发现在GridItem加载过程中使用了自定义动画,查看JSAnimation动画参数duration为150ms,说明此动画完成时间为150ms,影响图片的加载效果。
动画JSAnimation
4.3.3 优化方案
评估动画是否合理或者优化参数。
4.3.4 问题总结
在UI显示阶段,动画是影响响应时延类的重要因素。
附录1: Trace抓取方式
步骤1:打开手机USB调试模式,并将手机和主机连接,此时IDE工具中会自动出现设备信息。
步骤2:使用DevEco Studio的Profiler功能下的Frame模式收集应用在滑动过程中的Trace点信息,如下图,选中设备,应用,选择Frame模式,
步骤3:点击Create Session后会创建一个Frame任务。
步骤4:手机上找到对应的功能页面,点击任务卡片上的启动按钮就可以开始录制应用启动的Trace信息。
步骤5:开始滑动列表,等待加载完成,点击任务卡片上的停止按钮就可以停止录制,停止后工具会自动开始分析数据,分析完成后会自动展示Trace打点数据。
附录2:场景通用Trace流程点说明
加载完成时延主要分为两个部分:
- 网络耗时
- 渲染耗时通过以上两点归纳以下常用Trace点。
网络关键Trace点
序号 | 泳道 | Trace点 | 描述 |
1 | ArkTS CallStack | createHttp | 创建网络请求 |
2 | ArkTS CallStack | request | 发送网络请求 |
3 | ArkTS CallStack | parse | 解析数据 |
4 | ArkTS CallStack | off | 取消订阅 |
5 | 应用进程 | OnDataReloaded | 懒加载数据刷新。懒加载数据刷新分全量刷新和局部刷新,在数据较多时建议局部刷新。 |
UI绘帧关键Trace点。
序号 | 泳道 | Trace点 | 描述 |
1 | 应用进程 | SendCommands | 应用UI提交到Render Service |
2 | render_service | RSMainThread::DoComposition | 合成渲染树上各节点图层 |
3 | RSHardWareThread | Commit | 绘制结果提交上屏 |
序号 | 泳道 | Trace点 | 描述 |
1 | 应用进程 | ReceiveVsync | 接收Vsync信号 |
2 | 应用进程 | OnVsyncEvent | 收到Vsync信号,渲染流程开始 |
3 | 应用进程 | FlushVsync | 刷新视图同步事件,包括记录帧信息、刷新任务、绘制渲染上下文、处理用户输入。 |
4 | 应用进程 | FlushDirtyNodeUpdate | 标脏组件刷新。 页面刷新渲染的时候要尽量减少刷新的组件数量。当状态变量改变后,会先对与状态变量相关的组件进行标脏,然后再对这些组件重新测量和布局,最后再进行渲染。 |
5 | 应用进程 | JSAnimation | 显示动画,动画会影响组件加载完成时延。比如透明度0-1,放大0-1等。 |
6 | 应用进程 | FlushLayoutTask | 执行布局任务。在此阶段会对组件做布局测算,如果层级较深或者组件较多会影响性能。 |
7 | 应用进程 | Builder:BuildLazyItem | 需创建的项目索引,在需要时创建项,并进行缓存 |
8 | 应用进程 | FlushMessages | 发送消息通知图形侧进行渲染。 |
9 | 应用进程 | LazyForEach predict | OnIdle下,一般会用来做预加载之类的。LazyForEach会触发 |
10 | 应用进程 | aboutToBeDeleted | 自定义组件生命周期函数。组件析构时出现,在未使用复用机制时,FlushDirtyNodeUpdate和LazyForEach predict 下会析构组件,导致刷新时组件重复创建,影响性能 |
- FlushDirtyNodeUpdate
(1)执行js代码,修改状态变量
(2)由于状态变量变化,将与状态变量关联的组件都标记为脏节点
(3)重新测量和布局所有的脏节点
(4)将结果进行序列化后传给渲染服务做合成显示