1# 合理使用自定义组件冻结功能
2
3## 概述
4自定义组件冻结功能是指通过设置[freezeWhenInactive](../quick-start/arkts-create-custom-components.md#freezewheninactive11)属性为true,对非激活页面或者自定义组件进行冻结,使其不响应状态变量引起的UI刷新。当状态变量改变时,处于非激活状态的页面或自定义组件的状态变量将不响应更新,状态变量的@Watch函数不会调用,关联的节点不会刷新。只有当页面或者自定义组件重新激活或者可见时,才会去更新状态变量。本文将介绍冻结功能的原理机制和使用场景,并且通过懒加载场景下使用冻结功能前后的性能对比,帮忙开发者优化页面性能,减少页面渲染的时间,提升用户体验。
5
6## 原理机制
7- 组件的激活状态和非激活状态并非等同于可见和不可见。比如堆叠,堆叠在下面的组件虽然不可见,但是是激活状态。
8- 需要注意,组件冻结不等于不更新,而是延迟更新,从而来降低某些场景下的UI刷新复杂度。当页面重新可见或者恢复到激活状态时,将对延迟刷新的组件进行刷新。举例而言,对于Navigation,对当前不可见的页面进行冻结,不会触发组件的更新。当返回该页面时,触发@Watch回调进行刷新。所以在pop返回上一个页面时,会刷新在冻结中没有更新的组件,从而可能导致pop的负载上升。
9- 所有的冻结都是以自定义组件为最小单位。使用冻结功能后,自定义组件及其所属的系统组件是可以被冻结的,只是设置冻结的最小单位是自定义组件,不能单独冻结某个系统组件。
10- @Component({ freezeWhenInactive: true })中freezeWhenInactive的值后端只支持常量,不支持变量形式。
11
12## 适用场景
13目前自定义组件冻结功能支持以下四种场景。更多自定义组件冻结的信息,请参考[自定义组件冻结功能](../quick-start/arkts-custom-components-freeze.md)。
14
15**页面路由**:当页面A调用router.pushUrl接口跳转到页面B时,页面A为隐藏不可见状态,此时如果更新页面A中的状态变量,不会触发页面A刷新。
16
17**TabContent**:对Tabs中当前不可见的TabContent进行冻结,不会触发组件的更新。当切换TabContent后,会对需要刷新的组件进行刷新。
18
19**LazyforEach**:对LazyforEach中缓存的自定义组件进行冻结,不会触发组件的更新。
20
21**Navigation**:对当前不可见的页面进行冻结,不会触发组件的更新。当返回该页面时,节点重新变成激活状态,才会进行刷新。
22
23## 场景示例
24
25![](./figures/custom_component_freeze_scene.gif)
26
27上图是一个使用Grid懒加载的仿图库长按图片显示复选框的场景。下面将基于该场景对比分析冻结能力使用前后的性能差异。
28
29下面是一个仿图库长按图片显示复选框的示例。为了方便对比,示例中使用isUseFreezeWhenInactive显隐控制是否使用自定义组件冻结功能。通过在自定义组件UseFreezeItem上设置@Component({ freezeWhenInactive: true }),即使用组件冻结功能。自定义组件NotUseFreezeItem不设置freezeWhenInactive属性,则不使用组件冻结功能。
30
31```ts
32// 图片类
33@Observed
34class ImageInfo {
35  id: number; // 图片id
36  icon: ResourceStr; // 图片
37  isSelected: boolean; // 是否选中图片
38
39  constructor(id: number = 0, icon: ResourceStr = '', isSelected: boolean = false) {
40    this.id = id;
41    this.icon = icon;
42    this.isSelected = isSelected;
43  }
44}
45
46// MyDataSource类实现IDataSource接口
47class MyDataSource implements IDataSource {
48  private dataArray: ImageInfo[] = [];
49
50  public pushData(data: ImageInfo): void {
51    this.dataArray.push(data);
52  }
53
54  // 数据源的数据总量
55  public totalCount(): number {
56    return this.dataArray.length;
57  }
58
59  // 返回指定索引位置的数据
60  public getData(index: number): ImageInfo {
61    return this.dataArray[index];
62  }
63
64  registerDataChangeListener(listener: DataChangeListener): void {
65  }
66
67  unregisterDataChangeListener(listener: DataChangeListener): void {
68  }
69}
70
71// 预加载的GridItem的数量
72const CACHED_COUNT: number = 200;
73
74@Entry
75@Component
76struct ImitationGallery {
77  // 数据源
78  @State data: MyDataSource = new MyDataSource();
79  // 选择模式标志位
80  @State isSelectedMode: boolean = false;
81  // 全选标志位。
82  @State isSelectedAll: boolean = false;
83  // 选择模式下选中的图片个数
84  @State selectCount: number = 0;
85  // 图片选中列表。存放选中后的图片id。
86  @State @Watch('onSelectChange') selectedList: number[] = [];
87  // 是否启用组件冻结功能
88  @State isUseFreezeWhenInactive: boolean = false;
89
90  // 监听选中图片的数量
91  private onSelectChange() {
92    this.selectCount = this.selectedList.length;
93  }
94
95  aboutToAppear() {
96    // 添加图片数据源
97    for (let i = 0; i < 2000; i++) {
98      this.data.pushData(new ImageInfo(i, $r('app.media.custom_component_freeze_photo'), false));
99    }
100  }
101
102  build() {
103    Column() {
104      Row() {
105        if (!this.isSelectedMode) {
106          Text("所有照片").fontSize(25).padding({ left: 15 }).fontWeight(FontWeight.Medium)
107          Blank()
108          Text("是否开启组件冻结").fontSize(15)
109          Toggle({ type: ToggleType.Switch, isOn: this.isUseFreezeWhenInactive })
110            .margin({ right: 20 })
111            .onChange((isOn: boolean) => {
112              this.isUseFreezeWhenInactive = isOn ? true : false;
113            })
114        } else {
115          Row() {
116            Image($r('app.media.custom_component_freeze_cancel'))
117              .margin({ left: 15, right: 15 })
118              .width(25)
119              .onClick((): void => {
120                if (this.isSelectedMode) {
121                  // 取消选择模式
122                  this.isSelectedMode = false;
123                  // 取消所有选中状态
124                  for (let i = 0; i < this.selectedList.length; i++) {
125                    this.data.getData(this.selectedList[i]).isSelected = false;
126                  }
127                  // 取消全选
128                  this.isSelectedAll = false;
129                  this.selectedList = [];
130                }
131              })
132            Text(`已选择${this.selectCount}项`).fontSize(20)
133            Blank()
134            Image(this.isSelectedAll ? $r('app.media.custom_component_freeze_select_all') : $r('app.media.custom_component_freeze_no_select_all'))
135              .width(30)
136              .height(30)
137              .margin({ right: 20 })
138              .onClick(() => {
139                if (!this.isSelectedMode) {
140                  return;
141                }
142                // 全选
143                if (this.isSelectedAll) {
144                  this.isSelectedAll = false;
145                  // 取消全选,修改选中状态
146                  for (let i = 0; i < this.selectedList.length; i++) {
147                    this.data.getData(this.selectedList[i]).isSelected = false;
148                  }
149                  this.selectedList = [];
150                } else {
151                  // 全选,添加到图片选中列表
152                  this.isSelectedAll = true;
153                  this.selectedList = [];
154                  for (let i = 0; i < this.data.totalCount(); i++) {
155                    this.data.getData(i).isSelected = true;
156                    this.selectedList.push(i);
157                  }
158                }
159              })
160          }.width('100%')
161        }
162      }.width('100%').height(48)
163
164      if (this.isUseFreezeWhenInactive) {
165        Grid() {
166          LazyForEach(this.data, (imageItem: ImageInfo) => {
167            GridItem() {
168              UseFreezeItem({
169                imageItem: imageItem,
170                isSelectedMode: this.isSelectedMode,
171                selectedList: this.selectedList
172              })
173            }
174          }, (item: string) => item)
175        }
176        .cachedCount(CACHED_COUNT) // 设置GridItem的缓存数量
177        .columnsTemplate('1fr 1fr 1fr 1fr')
178        .columnsGap(2)
179        .rowsGap(2)
180      } else {
181        Grid() {
182          LazyForEach(this.data, (imageItem: ImageInfo) => {
183            GridItem() {
184              NotUseFreezeItem({
185                imageItem: imageItem,
186                isSelectedMode: this.isSelectedMode,
187                selectedList: this.selectedList
188              })
189            }
190          }, (item: string) => item)
191        }
192        .cachedCount(CACHED_COUNT) // 设置GridItem的缓存数量
193        .columnsTemplate('1fr 1fr 1fr 1fr')
194        .columnsGap(2)
195        .rowsGap(2)
196      }
197    }
198  }
199}
200
201// 使用组件冻结
202@Component({ freezeWhenInactive: true })
203struct UseFreezeItem {
204  @ObjectLink imageItem: ImageInfo; // 图片信息对象
205  @Link isSelectedMode: boolean; // 是否选择模式
206  @Link selectedList: number[]; // 图片选中列表。存放选中后的图片id。
207
208  build() {
209    Stack() {
210      Image(this.imageItem.icon).aspectRatio(1).draggable(false)
211      if (this.isSelectedMode) {
212        Image(this.imageItem.isSelected ? $r('app.media.custom_component_freeze_checkbox_on') : $r('app.media.custom_component_freeze_checkbox_off'))
213          .height(24)
214          .width(24)
215          .position({ x: '100%', y: '100%' })
216          .markAnchor({
217            x: 28,
218            y: 28
219          })
220      }
221    }
222    .width('100%')
223    .aspectRatio(1)
224    .gesture(LongPressGesture().onAction((event: GestureEvent): void => {
225      // 长按进入选择模式。同时选中当前长按的图片
226      if (!this.isSelectedMode) {
227        this.isSelectedMode = true;
228        this.imageItem.isSelected = true;
229        this.selectedList.push(this.imageItem.id);
230      }
231    }))
232    .onClick((): void => {
233      if (!this.isSelectedMode) {
234        return;
235      }
236      if (this.imageItem.isSelected) {
237        // 取消选中,从图片选中列表中删除
238        this.imageItem.isSelected = false;
239        this.selectedList = this.selectedList.filter(num => num !== this.imageItem.id);
240      } else {
241        // 选中图片,添加到图片选中列表
242        this.imageItem.isSelected = true;
243        this.selectedList.push(this.imageItem.id);
244      }
245    })
246  }
247}
248
249// 不使用组件冻结
250@Component
251struct NotUseFreezeItem {
252  @ObjectLink imageItem: ImageInfo; // 图片信息对象
253  @Link isSelectedMode: boolean; // 是否选择模式
254  @Link selectedList: number[]; // 图片选中列表。存放选中后的图片id。
255
256  build() {
257    Stack() {
258      Image(this.imageItem.icon).aspectRatio(1).draggable(false)
259      if (this.isSelectedMode) {
260        Image(this.imageItem.isSelected ? $r('app.media.custom_component_freeze_checkbox_on') : $r('app.media.custom_component_freeze_checkbox_off'))
261          .height(24)
262          .width(24)
263          .position({ x: '100%', y: '100%' })
264          .markAnchor({
265            x: 28,
266            y: 28
267          })
268      }
269    }
270    .width('100%')
271    .aspectRatio(1)
272    .gesture(LongPressGesture().onAction((event: GestureEvent): void => {
273      // 长按进入选择模式。同时选中当前长按的图片
274      if (!this.isSelectedMode) {
275        this.isSelectedMode = true;
276        this.imageItem.isSelected = true;
277        this.selectedList.push(this.imageItem.id);
278      }
279    }))
280    .onClick((): void => {
281      if (!this.isSelectedMode) {
282        return;
283      }
284      if (this.imageItem.isSelected) {
285        // 取消选中,从图片选中列表中删除
286        this.imageItem.isSelected = false;
287        this.selectedList = this.selectedList.filter(num => num !== this.imageItem.id);
288      } else {
289        // 选中图片,添加到图片选中列表
290        this.imageItem.isSelected = true;
291        this.selectedList.push(this.imageItem.id);
292      }
293    })
294  }
295}
296```
297
298## 效果对比
299
300下面将通过控制示例页面上“是否开启组件冻结”的开关,使用SmartPerf工具抓取trace来分析自定义组件冻结功能使用前后长按图片显示复选框的性能差异。
301
302### 不开启冻结功能
303
304如图1所示,在Grid预加载GridItem数量设置200的情况下,不开启组件冻结功能,抓取长按图片显示复选框的trace。可以看出显示复选框的UIVsyncTask(执行布局任务、执行渲染任务并通知图形进行渲染)耗时为162ms。其中FlushDirtyNodeUpdate(更新脏节点)耗时104ms,UITaskScheduler::FlushTask(主要是对懒加载的GridItem进行重新布局)耗时28ms。
305
306图1 不开启自定义组件冻结功能
307
308![](./figures/custom_component_freeze_not_freeze_duration.png)
309
310如图2所示,FlushDirtyNodeUpdate里可以看到执行了832个CustomNodeUpdate NotUseFreezeItem(自定义组件节点刷新)任务,这里的832个自定义组件节点指的是屏幕内可见的32个GridItem节点和不可见的800个缓存GridItem节点。
311
312图2 不开启冻结功能后CustomNodeUpdate耗时
313
314![](./figures/custom_component_freeze_not_freeze_item.png)
315
316### 开启冻结功能
317
318如图3所示,在Grid预加载GridItem数量设置200的情况下,开启组件冻结功能,抓取长按图片显示复选框的trace。可以看出显示复选框的UIVsyncTask耗时仅为32ms。其中FlushDirtyNodeUpdate耗时7ms,UITaskScheduler::FlushTask耗时14ms。和不开启冻结功能相比耗时减少了约80%(性能耗时数据因设备型号版本而异,以实测为准)。
319
320图3 开启自定义组件冻结功能
321
322![](./figures/custom_component_freeze_freeze_duration.png)
323
324如图4所示,FlushDirtyNodeUpdate里执行了32个CustomNodeUpdate UseFreezeItem(自定义组件节点刷新)任务,这里的32个自定义组件节点指的是屏幕内可见的所有GridItem节点。和图2相比,可以发现开启冻结功能比不开启冻结功能少执行了800个自定义组件节点的刷新任务,大大缩短了渲染耗时。
325
326图4 开启冻结功能后CustomNodeUpdate耗时
327
328![](./figures/custom_component_freeze_freeze_item.png)
329
330图5为Grid懒加载场景下,设置不同预加载缓存GridItem数量(cachedCount)的UIVsyncTask耗时对比图。可以看出懒加载中设置的预加载缓存GridItem的数量越大,UIVsyncTask耗时越长。
331
332图5 UIVsyncTask耗时对比(性能耗时数据因设备型号版本而异,以实测为准)
333
334![](./figures/custom_component_freeze_duration.png)
335
336通过上述对比可以发现,懒加载场景下开启冻结功能后,仅会刷新屏幕可见的GridItem,屏幕外不可见的缓存GridItem不会刷新,相比不开启冻结功能,大大减少了需要刷新的自定义组件节点数量,有效降低页面重新渲染的耗时。在实际业务场景中,自定义组件布局更为复杂,需要更新的状态变量更多,而合理使用自定义组件冻结功能能有效减少渲染耗时和操作卡顿,提升页面性能,给用户带来更好的体验。