概述
在大型业务场景开发过程中,为了提升产品的视觉效果,经常大量使用属性动画和转场动画,当业务场景复杂度达到一定程度之后,就有可能出现卡顿的情况。本文推荐在单一页面上存在大量应用动效的组件时,使用renderGroup方法来解决卡顿问题,从而提升绘制性能。
renderGroup是组件通用方法,它代表了渲染绘制的一个组合。其核心功能就是标记组件,在绘制阶段将组件和其子组件的绘制结果进行合并并缓存,以达到复用的效果,从而降低绘制负载。renderGroup方法通过传参,主动标记组件是否开启缓存复用,其参数说明如下:
参数 | 类型 | 说明 |
---|---|---|
value | boolean | false:关闭,true:开启。若不调用接口,组件标记默认值为false |
renderGroup本质上使用了用空间换时间的思想,如果缓存能够一直复用,那么就能一直节约绘制时间。要想达到上述的效果,组件每一帧的绘制结果都必须是相同的,也就是说如果组件内部的内容是固定的、不变的、静止的,只有这样,使用renderGroup才能生效。本文基于在内容固定的组件上添加动效这个场景,对组件通用方法renderGroup进行案例分析和性能对比。
原理说明
首次绘制组件时,若组件被标记为启用renderGroup状态,将对组件和其子组件进行离屏绘制,将绘制结果进行缓存。此后当需要重新绘制组件时,就会优先使用缓存而不必重新绘制了。从而降低绘制负载,优化渲染性能。
以下流程图展示了单个组件的渲染流程,涉及了缓存管理和使用,当组件树进入渲染管线开始渲染流程时,会对组件树上标脏的组件和其子组件进行递归渲染,若组件缓存存在,则将直接使用缓存进行绘制;若组件被标记为开启renderGroup时,则将进入绘制逻辑,递归绘制其所有子组件,并将绘制结果进行缓存。
图1 组件渲染流程
以下流程图展示了缓存管理上的流程细节。
当同时满足以下三个条件时,将进行缓存更新。
- 组件在当前组件树上
- 组件renderGroup被标记为true
- 组件内容被标脏
当满足以下任意条件时,将进行缓存清理。
- 组件不存在于组件树上
- 组件renderGroup被标记为false
图2 缓存管理流程
使用约束
结合上述原理,为了能使renderGroup功能生效,组件存在以下限制。
-
组件内容固定不变
组件和其子组件各属性保持固定,不发生变化。如果组件内容不是固定的,也就是说其子组件中上存在某些属性变化或者样式变化的组件,此时如果使用renderGroup,那么缓存的利用率将大大下降,并且有可能需要不断执行缓存更新逻辑,在这种情况下,不仅不能优化卡顿效果,甚至还可能使卡顿恶化。例如:文本内容使用双向绑定的动态数据;图片资源使用gif格式;使用video组件播放视频。
-
子组件无动效
由组件统一应用动效,其子组件均无动效。如果子组件上也应用动效,那么子组件相对父组件就不再是静止的,每一帧都有可能需要更新缓存,更新逻辑同样需要消耗系统资源。
使用场景
当在单一页面上存在大量应用动效的组件,并且这些组件均满足上述约束时,推荐使用renderGroup。
以下展示了一个使用场景的示例,首先场景中每个组件内部使用固定图片和文本内容,其次在每个组件上统一应用旋转和缩放动效,最后在场景中添加60个这样的组件。
图3 使用场景示例
推荐示例
以下展示了推荐场景的示例代码,分别是组件树结构以及自定义组件IconItem,场景采用grid布局,将多个IconItem放置在组件树上,每个IconItem内部使用固定图片和固定文本表示固定内容的组件。renderGroup方法在自定义组件IconItem内调用,通过开关按钮切换来关闭和开启renderGroup,通过Profiler Frame工具进行数据收集,从丢帧率、CPU使用率和GPU使用率三个方面,对比场景示例在关闭和开启renderGroup时的性能差异。
// Index.ets
import {
IconItem } from './IconItem'
// IconItem相关数据
class IconItemSource {
image: string | Resource = ''
text: string | Resource = ''
constructor(image: string | Resource = '', text: string | Resource = '') {
this.image = image;
this.text = text;
}
}
@Entry
@Component
struct Index {
// renderGroup接口是否开启
@State renderGroupFlag: boolean = false;
private iconItemSourceList: IconItemSource[] = [];
aboutToAppear() {
// 遍历添加60个IconItem的数据
for (let index = 0; index < 20; index++) {
const numStart: number = index * 3;
// 此处循环使用三张图片资源
this.iconItemSourceList.push(
new IconItemSource($r('app.media.album'), `item${
numStart + 1}`),
new IconItemSource($r('app.media.applet'), `item${
numStart +