鸿蒙开发5.0【帧率】解析
一、组件渲染和更新
1、渲染流程
在整个渲染流程中,首先是由应用侧响应消费者的屏幕点击等输入事件,由应用侧处理完成后再提交给Render Service,由Render Service协调GPU等资源处理后,再将最终的图像统一送到屏幕上进行显示。
- 应用侧(App)处理用户的屏幕点击等输入事件,生成当前界面描述的数据结构。其中,界面描述数据包括UI元素的位置,大小,资源,UI元素的绘制指令,动效属性等。
- Render Service(渲染服务部件)是图形栈中负责界面内容绘制的模块,其主要职责就是对接ArkUI框架,支撑ArkUI应用的界面显示,包括控件、动效等UI元素。Render Service的RenderThread线程在Vsync下触发UI绘制,绘制过程包含3个阶段:Animation动效,Draw描画和Flush提交。
- Display是显示屏幕的抽象概念,可以是实际的物理屏也可以是虚拟屏。
2、Vsync信号
由于屏幕刷新率是固定的,设备会以固定的频率发送vsync信号,以90Hz(1秒刷新90次)刷新率为例,每个Vsync周期是11.1ms(1000ms/90)。如果是120Hz,则每个Vsync的周期是8.3ms。如果数据处理时间过长或者组件过于复杂导致绘制时间过长就可能导致丢帧的问题。
3、ArkUI渲染管线结构与Frame性能打点
- Animation:动画阶段,在动画过程中会修改相应的FrameNode节点触发脏区标记,在特定场景下会执行用户侧ets代码实现自定义动画;
- Events:事件处理阶段,比如手势事件处理。在手势处理过程中也会修改FrameNode节点触发脏区标记,在特定场景下会执行用户侧ets代码实现自定义事件;
- UpdateUI:自定义组件(@Component)在首次创建挂载或者状态变量变更时会标记为需要rebuild状态,在下一次Vsync过来时会执行rebuild流程,rebuild流程会执行程序UI代码,通过调用View的方法生成相应的组件树结构和属性样式修改任务。
- Measure:布局包装器执行相关的大小测算任务。
- Layout:布局包装器执行相关的布局任务。
- Render:绘制任务包装器执行相关的绘制任务,执行完成后会标记请求刷新RSNode绘制
- SendMessage:请求刷新界面绘制。
4、UI更新流程
UI更新过程包含组件标脏过程以及布局过程,对应的元素会经历Build、Measure、Layout和Render等阶段。
- Build是执行组件创建和组件标脏的过程
- Measure是对组件的宽高进行测量的阶段
- Layout是对元素进行在屏幕上位置进行摆放的阶段
- Render则是根据测量和布局得到的大小位置等信息,进行提交绘制的过程。
而在界面更新时(如列表滑动,切换显示隐藏状态,触发页面元素内容样式位置大小等变化)等场景下,并不需要把页面所有的组件对象重新创建一遍,而是只需要对需要更新部分进行更新。
- UI线程处理过程会先将脏节点进行Build,Build的过程会按照组件id,依次更新组件设置的属性
- 如果属性发生改变,则进行组件标脏,其中布局属性(width、height、padding、margin等)发生改变会标记为布局脏,找到布局边界,进行子树更新
- 而非布局(Color、BackgroundColor、opacity等)属性仅会影响自身属性,不会进行子树查找。
- 确定实际的脏节点数组后,根据脏节点数组来拿到对应的脏节点对象,通过递归遍历children进行Measure过程,如果该对象布局参数没有发生变化,就会跳过对应的Measure阶段。当Measure执行完成后,进行layout阶段。
二、滑动流畅度
1、指标
- 滑动开始:滑动响应时延
- 滑动过程中:最大连续丢帧数+卡顿率
- 滑动结束:占位符加载完成时延
2、丢帧
- 最大连续丢帧数:指从页面开始有响应变化到页面结束刷新的过程中,由于显示器画面刷新频率低于预设的画面帧率而未能正常呈现的最大连续帧数。当连续值超过3时,用户可以明显感知到卡顿掉帧。
- 丢帧率(Janky Frames):表示一个时间周期内的丢帧比率,指一个时间周期内有问题的帧比例。
- 最大连续丢帧数(maximum successive frame dropping count):表示从页面开始有响应变化到页面结束刷新的过程中,由于显示器画面刷新频率低于预设的画面帧率而未能正常呈现的最大连续帧数
3、丢帧故障模型
应用侧和Render Service侧都有可能出现卡顿导致最终用户观测到丢帧的可能,我们分别将这两种情况命名为AppDeadlineMissed(App侧卡顿)和RenderDeadlineMissed(Render Service侧的卡顿)。
- AppDeadlineMissed可能是应用逻辑处理代码不够高效导致的
- RenderDeadlineMissed可能是界面结构过于复杂或者GPU负载过大等原因导致的
三、合理使用组件
1、精简节点数
- 移除冗余的节点
- 使用扁平化布局减少节点数。
2、尽量给定组件宽高固定值
原因:
- 首次绘制的情况下,无论是否设置宽高属性,都会对所有组件进行布局和测算的过程,得到最终的组件大小和位置。
- 触发重新绘制的情况下,在外层容器宽高发生变化时,对于未设置宽高以及设置百分比宽高的组件会触发重新进行Measure的过程。设置了固定值不会重新Measure, 会使用初次绘制保留的节点数据。
3、合理控制元素展示和隐藏
原因:
- 在初次加载时,无论visibility的值为Visibility.None还是Visibility.Visible都会创建对应组件内容。当visibility属性为Visibility.None时,对应的组件不参与Layout。
- 只有初始的一次渲染或者交互次数很少的情况下,建议使用if条件判断来控制元素的显示与隐藏效果,对于内存有较大提升;
- 如果会频繁响应显示与隐藏的交互效果,建议使用切换Visibility.None和Visibility.Visible来控制元素显示与隐藏,提高性能;
4、使用renderGroup
- 若组件被标记为启用renderGroup状态,将对组件及其子组件进行离屏绘制,将绘制结果合并保存到缓存中。
- 当需要重新绘制相同组件时,就会优先使用缓存而不必重新绘制了。
限制:
- 组件内容固定不变:组件及其子组件各属性保持固定,不发生变化
- 子组件无动效:由组件统一应用动效,其子组件均无动效
5、使用@builder函数代替自定义组件
- @Builder函数不会在后端FrameNode节点树上创建一个新的树节点。
6、Scroll嵌套List
- List使用ForEach加载子组件时,无论是否设置List的宽高,都会加载所有子组件。
- List使用LazyForEach加载子组件时,没有设置List的宽高,会加载所有子组件,设置了List的宽高,会加载List显示区域内的子组件。
7、使用Column/Row替代Flex
- 由于Flex容器组件默认情况下存在shrink导致二次布局,这会在一定程度上造成页面渲染上的性能劣化。
8、可变帧率displaySync
由于一次性加载大量数据、刷新大量组件会导致卡顿丢帧,需要加载的数据总量和绘制的组件数量是不能减少的,那么只能想办法将数据进行拆分
- 将和数据相关的组件分成多次进行绘制。
- ArkTS中提供了DisplaySync(可变帧率),支持开发者设置回调监听,可以在回调里做一些数据的处理,在每一帧中加载少量的数据,减少卡顿或者滑动动画的掉帧现象。
9、全局自定义组件复用
- 将要生成自定义组件地方用NodeContainer占位,将NodeContainer内部的NodeController按照组件类型分别存储在NodePool中。
- 每次需要创建子组件时,优先从NodePool中取出一个组件,如果NodePool中没有可复用的组件则重新创建一个,否则就更新一下数据。当NodeController销毁时,回收到NodePool中,供下次使用。
四、长列表性能优化
- 懒加载
- 缓存列表项
- 动态预加载
- 组件复用
1、LazyForEach
- LazyForEach会根据屏幕可视区能够容纳显示的组件数量按需加载数据。
- 根据加载的数据量创建组件,挂载在组件树上,构建出一棵短小的组件树。
- 屏幕可视区只展示部分组件。当可视区外的组件需要在屏幕内显示时,需要从头完成数据加载、组件创建、挂载组件树这一过程,直至渲染到屏幕上。
2、cacheCount
- 可以通过设置cachedCount来指定缓存数量,cachedCount默认为1。
- 缓存列表项仅在使用LazyForEach懒加载时有效。
3、动态预加载
通过IDataSourcePrefetching实现数据预取,预取更多的数据
- 过小的cachedCount值会导致列表预取的Item数量不足。当用户滑动列表时,后台可能来不及准备好足够多的预取项,特别是内容数据量大或网络条件特别差的时候,列表滑动过程就容易出现很多白块。
- 较大的cachedCount值可以缓解缺少预取项的情况,但在列表没有滑动时,过大的cachedCount值可能导致可见区域的加载时间过长
4、组件复用
- 标记为@Reusable的组件从组件树上被移除时,组件和其对应的JSView对象都会被放入复用缓存中。
- 当列表滑动新的ListItem将要被显示,List组件树上需要新建节点时,将会从复用缓存中查找可复用的组件节。
- 找到可复用节点并对其进行更新后添加到组件树中。从而节省了组件节点和JSView对象的创建时间。
5、keyGenerator
- 避免在LazyForEach的keyGenerator中执行耗时操作,尽可能指定key生成规则,默认的键值生成函数为(item: T, index: number) => { return index + ‘__’ + JSON.stringify(item); }
五、合理使用状态管理
1、避免不必要的状态变量的使用
- 没有关联任何UI组件的状态变量不应该定义为状态变量
- 没有修改过的状态变量不应该定义为状态变量
2、使用临时变量替换状态变量
通过使用临时变量的计算代替直接操作状态变量,仅在最后一次状态变量变更时查询并渲染组件,减少不必要的行为
3、最小化状态共享范围
- @Prop会进行深拷贝,增加状态变量创建时间及占用大量内存,尽量避免使用
- 减少不必要的参数层层传递:当共享状态的组件间跨层级较深时,选择@Provide+@Consume的装饰器组合代替层层传递的方式
- 避免滥用@Provide+@Consume: 在父子组件关联的场景下,@Provide+@Consume开销要大于@State+@Prop/@Link,因此在该场景下推荐使用@State+@Prop/@Link的组合
- 精准控制状态变量关联组件数量:如果一个状态关联过多的组件,当这个变量更新时会引起过多的组件重新绘制渲染
- 控制对象级状态变量成员数量
- 避免不必要的创建和读取状态变量
- 避免在For/while等循环函数中重复读取状态变量
六、CodeLinter性能规则
- 不建议在高频函数中使用Hilog。
- 避免设置空的系统回调监听
- 避免在for、while等循环逻辑中频繁读取状态变量。
- 推荐在ResourceManager获取资源时传入资源id作为参数
- 建议尽量减少视图嵌套层次
- 避免冗余的嵌套。
- 建议移除不关联UI组件的状态变量设置。
- 建议移除未改变的状态变量设置。
- 建议使用@Builder替代嵌套的自定义组件。
- 建议在Grid下使用LazyForEach时设置合理的cacheCount。
- 建议使用@ObjectLink代替@Prop减少不必要的深拷贝。
- 建议使用Column/Row替代Flex。
以上就是本篇文章所带来的鸿蒙开发中一小部分技术讲解;想要学习完整的鸿蒙全栈技术。可以在结尾找我可全部拿到!
下面是鸿蒙的完整学习路线,展示如下:
除此之外,根据这个学习鸿蒙全栈学习路线,也附带一整套完整的学习【文档+视频】,内容包含如下:
内容包含了:(ArkTS、ArkUI、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、鸿蒙南向开发、鸿蒙项目实战)等技术知识点。帮助大家在学习鸿蒙路上快速成长!
鸿蒙【北向应用开发+南向系统层开发】文档
鸿蒙【基础+实战项目】视频
鸿蒙面经
为了避免大家在学习过程中产生更多的时间成本,对比我把以上内容全部放在了↓↓↓想要的可以自拿喔!谢谢大家观看!