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 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 309 310如图2所示,FlushDirtyNodeUpdate里可以看到执行了832个CustomNodeUpdate NotUseFreezeItem(自定义组件节点刷新)任务,这里的832个自定义组件节点指的是屏幕内可见的32个GridItem节点和不可见的800个缓存GridItem节点。 311 312图2 不开启冻结功能后CustomNodeUpdate耗时 313 314 315 316### 开启冻结功能 317 318如图3所示,在Grid预加载GridItem数量设置200的情况下,开启组件冻结功能,抓取长按图片显示复选框的trace。可以看出显示复选框的UIVsyncTask耗时仅为32ms。其中FlushDirtyNodeUpdate耗时7ms,UITaskScheduler::FlushTask耗时14ms。和不开启冻结功能相比耗时减少了约80%(性能耗时数据因设备型号版本而异,以实测为准)。 319 320图3 开启自定义组件冻结功能 321 322 323 324如图4所示,FlushDirtyNodeUpdate里执行了32个CustomNodeUpdate UseFreezeItem(自定义组件节点刷新)任务,这里的32个自定义组件节点指的是屏幕内可见的所有GridItem节点。和图2相比,可以发现开启冻结功能比不开启冻结功能少执行了800个自定义组件节点的刷新任务,大大缩短了渲染耗时。 325 326图4 开启冻结功能后CustomNodeUpdate耗时 327 328 329 330图5为Grid懒加载场景下,设置不同预加载缓存GridItem数量(cachedCount)的UIVsyncTask耗时对比图。可以看出懒加载中设置的预加载缓存GridItem的数量越大,UIVsyncTask耗时越长。 331 332图5 UIVsyncTask耗时对比(性能耗时数据因设备型号版本而异,以实测为准) 333 334 335 336通过上述对比可以发现,懒加载场景下开启冻结功能后,仅会刷新屏幕可见的GridItem,屏幕外不可见的缓存GridItem不会刷新,相比不开启冻结功能,大大减少了需要刷新的自定义组件节点数量,有效降低页面重新渲染的耗时。在实际业务场景中,自定义组件布局更为复杂,需要更新的状态变量更多,而合理使用自定义组件冻结功能能有效减少渲染耗时和操作卡顿,提升页面性能,给用户带来更好的体验。