HarmonyOS应用性能提升——组件复用

打印 上一主题 下一主题

主题 972|帖子 972|积分 2916

组件复用总览

组件复用是优化用户界面性能,提升应用流畅度的一种焦点计谋,它通过复用已存在的组件节点而非创建新的节点,大幅度低沉了因频仍创建与销毁组件带来的性能消耗,从而确保UI线程的流畅性与响应速度。组件复用针对的是自定义组件,只要发生了类似自定义组件销毁和再创建的场景,都可以使用组件复用。
本文体系地形貌了六种复用类型及其应用场景,帮助开发者更好地理解和实施组件复用计谋以优化应用性能。
关于组件复用的原理机制可以参考资料组件复用原理机制,便于理解本文内容。
复用类型总览

复用类型形貌复用思路参考文档尺度型复用组件之间布局完全类似尺度复用组件复用实践有限变化型复用组件之间有不同,但是类型有限使用reuseId或者独立成两个自定义组件组件复用四板斧组合型复用组件之间有不同,情况非常多,但是拥有共同的子组件将复用组件改为Builder,让内部子组件相互之间复用组合型组件复用引导全局型组件可在不同的父组件中复用,而且不适合使用@Builder使用BuilderNode自定义复用组件池,在整个应用中自由流转全局自定义组件复用实现嵌套型复用组件的子组件的子组件存在差别采用化归思想将嵌套题目转化为上面四种尺度类型来解决/无法复用型组件之间差别很大,规律性不强,子组件也不类似不发起使用组件复用/ 各个复用类型详解

下文为了方便形貌,以一个滑动列表的场景为例,将要复用的自定义组件如ListItem的内容组件,叫做复用组件,把它子级的自定义组件叫做子组件,把复用组件上层的自定义组件叫做父组件。为了更直观,下面每一种复用类型都会通过简易的图形展示组件的布局方式,而且为了便于分别,布局类似的子组件使用同一种外形表示。
尺度型


这是一个尺度的组件复用场景,一个滚动容器内的复用组件布局类似,只有数据不同。这种类型的组件复用可以直接参考资料组件复用实践。
应用场景案例

有限变化型


这种类型中复用组件之间存在不同,但是类型有限。如上图所示,容器内的复用组件内部的子组件不一样,但可总结为两种类型,类型 1由三个子组件 A 举行布局拼接而成,类型 2由子组件 B、子组件 C 和子组件 D 举行布局拼接而成。
此时存在以下两种应对步伐:


  • 类型1和类型2业务逻辑不同:发起将两种类型的组件使用两个不同的自定义组件,分别举行复用。此时组件复用池内的状态如下图所示,复用组件 1 和复用组件 2 处于不同的复用 list 中。

实现方式可参考以下示例代码:
  1. class MyDataSource implements IDataSource {
  2.   ...
  3. }
  4. @Entry
  5. @Component
  6. struct Index {
  7.   private data: MyDataSource = new MyDataSource();
  8.   aboutToAppear() {
  9.     for (let i = 0; i < 1000; i++) {
  10.       this.data.pushData(i);
  11.     }
  12.   }
  13.   build() {
  14.     Column() {
  15.       List({ space: 10 }) {
  16.         LazyForEach(this.data, (item: number) => {
  17.           ListItem() {
  18.             if (item % 2 === 0) {
  19.               ReusableComponentOne({ item: item.toString() })
  20.             } else {
  21.               ReusableComponentTwo({ item: item.toString() })
  22.             }
  23.           }
  24.           .backgroundColor(Color.Orange)
  25.           .width('100%')
  26.         }, (item: number) => item.toString())
  27.       }
  28.       .cachedCount(2)
  29.     }
  30.   }
  31. }
  32. @Reusable
  33. @Component
  34. struct ReusableComponentOne {
  35.   @State item: string = '';
  36.   aboutToReuse(params: ESObject) {
  37.     this.item = params.item;
  38.   }
  39.   build() {
  40.     Column() {
  41.       Text(`Item ${this.item} ReusableComponentOne`)
  42.         .fontSize(20)
  43.         .margin({ left: 10 })
  44.     }.margin({ left: 10, right: 10 })
  45.   }
  46. }
  47. @Reusable
  48. @Component
  49. struct ReusableComponentTwo {
  50.   @State item: string = '';
  51.   aboutToReuse(params: ESObject) {
  52.     this.item = params.item;
  53.   }
  54.   build() {
  55.     Column() {
  56.       Text(`Item ${this.item} ReusableComponentTwo`)
  57.         .fontSize(20)
  58.         .margin({ left: 10 })
  59.     }.margin({ left: 10, right: 10 })
  60.   }
  61. }
复制代码


  • 类型1和类型2布局不同,但是很多业务逻辑类似:在这种情况下,假如将组件分为两个自定义组件举行复用,会存在代码冗余题目。根据体系组件复用原理可知,复用组件是依据 reuseId 来区分复用缓存池的,而自定义组件的名称就是默认的 reuseId。因此,为复用组件显式设置两个 reuseId 与使用两个自定义组件举行复用,对于 ArkUI 而言,复用逻辑完全类似。此时组件复用池内的状态如下图所示。

具体实现方式可以参考以下示例:
  1. class MyDataSource implements IDataSource {
  2.   ...
  3. }
  4. @Entry
  5. @Component
  6. struct Index {
  7.   private data: MyDataSource = new MyDataSource();
  8.   aboutToAppear() {
  9.     for (let i = 0; i < 1000; i++) {
  10.       this.data.pushData(i);
  11.     }
  12.   }
  13.   build() {
  14.     Column() {
  15.       List({ space: 10 }) {
  16.         LazyForEach(this.data, (item: number) => {
  17.           ListItem() {
  18.             ReusableComponent({ item: item })
  19.               .reuseId(item % 2 === 0 ? 'ReusableComponentOne' : 'ReusableComponentTwo')
  20.           }
  21.           .backgroundColor(Color.Orange)
  22.           .width('100%')
  23.         }, (item: number) => item.toString())
  24.       }
  25.       .cachedCount(2)
  26.     }
  27.   }
  28. }
  29. @Reusable
  30. @Component
  31. struct ReusableComponent {
  32.   @State item: number = 0;
  33.   aboutToReuse(params: ESObject) {
  34.     this.item = params.item;
  35.   }
  36.   build() {
  37.     Column() {
  38.       if (this.item % 2 === 0) {
  39.         Text(`Item ${this.item} ReusableComponentOne`)
  40.           .fontSize(20)
  41.           .margin({ left: 10 })
  42.       } else {
  43.         Text(`Item ${this.item} ReusableComponentTwo`)
  44.           .fontSize(20)
  45.           .margin({ left: 10 })
  46.       }
  47.     }.margin({ left: 10, right: 10 })
  48.   }
  49. }
复制代码
应用场景案例

组合型


这种类型中复用组件之间存在不同,而且情况非常多,但拥有共同的子组件。假如使用有限变化型的组件复用方式,将所有类型的复用组件写成自定义组件分别复用,那么不同复用组件的复用 list 中类似的子组件之间不能互相复用。对此可以将复用组件变化为 Builder 函数,使复用组件内部共同的子组件的缓存池在父组件上共享。此时组件复用池内的状态如下图所示。

反例
下面是使用有限变化型组件复用的一段示例代码:
  1. class MyDataSource implements IDataSource {
  2.   ...
  3. }
  4. @Entry
  5. @Component
  6. struct MyComponent {
  7.   private data: MyDataSource = new MyDataSource();
  8.   aboutToAppear() {
  9.     for (let i = 0; i < 1000; i++) {
  10.       this.data.pushData(i.toString());
  11.     }
  12.   }
  13.   build() {
  14.     List({ space: 40 }) {
  15.       LazyForEach(this.data, (item: string, index: number) => {
  16.         ListItem() {
  17.           if (index % 3 === 0) {
  18.             ReusableComponentOne({ item: item })
  19.           } else if (index % 5 === 0) {
  20.             ReusableComponentTwo({ item: item })
  21.           } else {
  22.             ReusableComponentThree({ item: item })
  23.           }
  24.         }
  25.         .backgroundColor('#cccccc')
  26.         .width('100%')
  27.         .onAppear(()=>{
  28.           console.log(`ListItem ${index} onAppear`);
  29.         })
  30.       })
  31.     }
  32.     .width('100%')
  33.     .height('100%')
  34.     .cachedCount(0)
  35.   }
  36. }
  37. @Reusable
  38. @Component
  39. struct ReusableComponentOne {
  40.   @State item: string = '';
  41.   // 组件的生命周期回调,在可复用组件从复用缓存中加入到组件树之前调用
  42.   aboutToReuse(params: ESObject) {
  43.     console.log(`ReusableComponentOne ${params.item} Reuse ${this.item}`);
  44.     this.item = params.item;
  45.   }
  46.   // 组件的生命周期回调,在可复用组件从组件树上被加入到复用缓存之前调用
  47.   aboutToRecycle(): void {
  48.     console.log(`ReusableComponentOne ${this.item} Recycle`);
  49.   }
  50.   build() {
  51.     Column() {
  52.       ChildComponentA({ item: this.item })
  53.       ChildComponentB({ item: this.item })
  54.       ChildComponentC({ item: this.item })
  55.     }
  56.   }
  57. }
  58. @Reusable
  59. @Component
  60. struct ReusableComponentTwo {
  61.   @State item: string = '';
  62.   aboutToReuse(params: ESObject) {
  63.     console.log(`ReusableComponentTwo ${params.item} Reuse ${this.item}`);
  64.     this.item = params.item;
  65.   }
  66.   aboutToRecycle(): void {
  67.     console.log(`ReusableComponentTwo ${this.item} Recycle`);
  68.   }
  69.   build() {
  70.     Column() {
  71.       ChildComponentA({ item: this.item })
  72.       ChildComponentC({ item: this.item })
  73.       ChildComponentD({ item: this.item })
  74.     }
  75.   }
  76. }
  77. @Reusable
  78. @Component
  79. struct ReusableComponentThree {
  80.   @State item: string = '';
  81.   aboutToReuse(params: ESObject) {
  82.     console.log(`ReusableComponentThree ${params.item} Reuse ${this.item}`);
  83.     this.item = params.item;
  84.   }
  85.   aboutToRecycle(): void {
  86.     console.log(`ReusableComponentThree ${this.item} Recycle`);
  87.   }
  88.   build() {
  89.     Column() {
  90.       ChildComponentA({ item: this.item })
  91.       ChildComponentB({ item: this.item })
  92.       ChildComponentD({ item: this.item })
  93.     }
  94.   }
  95. }
  96. @Component
  97. struct ChildComponentA {
  98.   @State item: string = '';
  99.   aboutToReuse(params: ESObject) {
  100.     console.log(`ChildComponentA ${params.item} Reuse ${this.item}`);
  101.     this.item = params.item;
  102.   }
  103.   aboutToRecycle(): void {
  104.     console.log(`ChildComponentA ${this.item} Recycle`);
  105.   }
  106.   build() {
  107.     Column() {
  108.       Text(`Item ${this.item} Child Component A`)
  109.         .fontSize(20)
  110.         .margin({ left: 10 })
  111.         .fontColor(Color.Blue)
  112.       Grid() {
  113.         ForEach((new Array(20)).fill(''), (item: string,index: number) => {
  114.           GridItem() {
  115.             Image($r('app.media.startIcon'))
  116.               .height(20)
  117.           }
  118.         })
  119.       }
  120.       .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
  121.       .rowsTemplate('1fr 1fr 1fr 1fr')
  122.       .columnsGap(10)
  123.       .width('90%')
  124.       .height(160)
  125.     }
  126.     .margin({ left: 10, right: 10 })
  127.     .backgroundColor(0xFAEEE0)
  128.   }
  129. }
  130. @Component
  131. struct ChildComponentB {
  132.   @State item: string = '';
  133.   aboutToReuse(params: ESObject) {
  134.     this.item = params.item;
  135.   }
  136.   build() {
  137.     Row() {
  138.       Text(`Item ${this.item} Child Component B`)
  139.         .fontSize(20)
  140.         .margin({ left: 10 })
  141.         .fontColor(Color.Red)
  142.     }.margin({ left: 10, right: 10 })
  143.   }
  144. }
  145. @Component
  146. struct ChildComponentC {
  147.   @State item: string = '';
  148.   aboutToReuse(params: ESObject) {
  149.     this.item = params.item;
  150.   }
  151.   build() {
  152.     Row() {
  153.       Text(`Item ${this.item} Child Component C`)
  154.         .fontSize(20)
  155.         .margin({ left: 10 })
  156.         .fontColor(Color.Green)
  157.     }.margin({ left: 10, right: 10 })
  158.   }
  159. }
  160. @Component
  161. struct ChildComponentD {
  162.   @State item: string = '';
  163.   aboutToReuse(params: ESObject) {
  164.     this.item = params.item;
  165.   }
  166.   build() {
  167.     Row() {
  168.       Text(`Item ${this.item} Child Component D`)
  169.         .fontSize(20)
  170.         .margin({ left: 10 })
  171.         .fontColor(Color.Orange)
  172.     }.margin({ left: 10, right: 10 })
  173.   }
  174. }
复制代码
上述代码中由四个子组件按不同的排列组合构成了三种类型的复用组件。为了方便观察组件的缓存和复用情况,将 List 的 cachedCount 设置为0,并在部分自定义组件的生命周期函数中添加日记输出。其中重点观察子组件 ChildComponentA 的缓存和复用。
示例运行效果图如下:

从上图可以看到,列表滑动到 ListItem 0 消失时,复用组件 ReusableComponentOne 和它的子组件 ChildComponentA 都加入了复用缓存。继续向上滑动时,由于 ListItem 4 与 ListItem 0 的复用组件不在同一个复用 list,因此 ListItem 4 的复用组件 ReusableComponentThree 和它的子组件依然会全部重新创建,不会复用缓存中的子组件 ChildComponentA。
此时 ListItem 4 中的子组件 ChildComponentA 的重新创建耗时 6ms387μs499ns。

正例
按照组合型的组件复用方式,将上述示例中的三种复用组件变化为 Builder 函数后,内部共同的子组件就处于同一个父组件 MyComponent 下。对这些子组件使用组件复用时,它们的缓存池也会在父组件上共享,节省组件创建时的消耗。
修改后的示例代码:
  1. class MyDataSource implements IDataSource {
  2.   ...
  3. }
  4. @Entry
  5. @Component
  6. struct MyComponent {
  7.   private data: MyDataSource = new MyDataSource();
  8.   aboutToAppear() {
  9.     for (let i = 0; i < 1000; i++) {
  10.       this.data.pushData(i.toString())
  11.     }
  12.   }
  13.   @Builder
  14.   itemBuilderOne(item: string) {
  15.     Column() {
  16.       ChildComponentA({ item: item })
  17.       ChildComponentB({ item: item })
  18.       ChildComponentC({ item: item })
  19.     }
  20.   }
  21.   @Builder
  22.   itemBuilderTwo(item: string) {
  23.     Column() {
  24.       ChildComponentA({ item: item })
  25.       ChildComponentC({ item: item })
  26.       ChildComponentD({ item: item })
  27.     }
  28.   }
  29.   @Builder
  30.   itemBuilderThree(item: string) {
  31.     Column() {
  32.       ChildComponentA({ item: item })
  33.       ChildComponentB({ item: item })
  34.       ChildComponentD({ item: item })
  35.     }
  36.   }
  37.   build() {
  38.     List({ space: 40 }) {
  39.       LazyForEach(this.data, (item: string, index: number) => {
  40.         ListItem() {
  41.           if (index % 3 === 0) {
  42.             this.itemBuilderOne(item)
  43.           } else if (index % 5 === 0) {
  44.             this.itemBuilderTwo(item)
  45.           } else {
  46.             this.itemBuilderThree(item)
  47.           }
  48.         }
  49.         .backgroundColor('#cccccc')
  50.         .width('100%')
  51.         .onAppear(() => {
  52.           console.log(`ListItem ${index} onAppear`);
  53.         })
  54.       }, (item: number) => item.toString())
  55.     }
  56.     .width('100%')
  57.     .height('100%')
  58.     .cachedCount(0)
  59.   }
  60. }
  61. @Reusable
  62. @Component
  63. struct ChildComponentA {
  64.   @State item: string = '';
  65.   aboutToReuse(params: ESObject) {
  66.     console.log(`ChildComponentA ${params.item} Reuse ${this.item}`);
  67.     this.item = params.item;
  68.   }
  69.   aboutToRecycle(): void {
  70.     console.log(`ChildComponentA ${this.item} Recycle`);
  71.   }
  72.   build() {
  73.     Column() {
  74.       Text(`Item ${this.item} Child Component A`)
  75.         .fontSize(20)
  76.         .margin({ left: 10 })
  77.         .fontColor(Color.Blue)
  78.       Grid() {
  79.         ForEach((new Array(20)).fill(''), (item: string,index: number) => {
  80.           GridItem() {
  81.             Image($r('app.media.startIcon'))
  82.               .height(20)
  83.           }
  84.         })
  85.       }
  86.       .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
  87.       .rowsTemplate('1fr 1fr 1fr 1fr')
  88.       .columnsGap(10)
  89.       .width('90%')
  90.       .height(160)
  91.     }
  92.     .margin({ left: 10, right: 10 })
  93.     .backgroundColor(0xFAEEE0)
  94.   }
  95. }
  96. @Reusable
  97. @Component
  98. struct ChildComponentB {
  99.   @State item: string = '';
  100.   aboutToReuse(params: ESObject) {
  101.     this.item = params.item;
  102.   }
  103.   build() {
  104.     Row() {
  105.       Text(`Item ${this.item} Child Component B`)
  106.         .fontSize(20)
  107.         .margin({ left: 10 })
  108.         .fontColor(Color.Red)
  109.     }.margin({ left: 10, right: 10 })
  110.   }
  111. }
  112. @Reusable
  113. @Component
  114. struct ChildComponentC {
  115.   @State item: string = '';
  116.   aboutToReuse(params: ESObject) {
  117.     this.item = params.item;
  118.   }
  119.   build() {
  120.     Row() {
  121.       Text(`Item ${this.item} Child Component C`)
  122.         .fontSize(20)
  123.         .margin({ left: 10 })
  124.         .fontColor(Color.Green)
  125.     }.margin({ left: 10, right: 10 })
  126.   }
  127. }
  128. @Reusable
  129. @Component
  130. struct ChildComponentD {
  131.   @State item: string = '';
  132.   aboutToReuse(params: ESObject) {
  133.     this.item = params.item;
  134.   }
  135.   build() {
  136.     Row() {
  137.       Text(`Item ${this.item} Child Component D`)
  138.         .fontSize(20)
  139.         .margin({ left: 10 })
  140.         .fontColor(Color.Orange)
  141.     }.margin({ left: 10, right: 10 })
  142.   }
  143. }
复制代码
示例运行效果图如下:

从效果图可以看出,每一个 ListItem 中的子组件 ChildComponentA 之间都可以触发组件复用。此时 ListItem 4 创建时,子组件 ChildComponentA 复用 ListItem 0 中的子组件 ChildComponentA ,复用仅耗时 864μs583ns。

应用场景案例

全局型


一些场景中组件必要在不同的父组件中复用,而且不适合改为Builder。如上图所示,偶然候应用在多个tab页之间切换,tab页之间结构类似,必要在tab页之间复用组件,提升页面切换性能。或者有些应用在组合型场景下,由于复用组件内部含有带状态的业务逻辑,不适合改为Builder函数。
针对这种类型的组件复用场景,可以通过BuilderNode自定义缓存池,将要复用的组件封装在BuilderNode中,将BuilderNode的NodeController作为复用的最小单元,自行管理复用池。具体实现可以参考资料全局自定义组件复用实现。
这种场景不适用体系自带的复用池,自行管理组件复用。
应用场景案例

嵌套型


复用组件的子组件的子组件之间存在差别。可以运行化归的思想,将复杂的题目转化为已知的、简单的题目。
嵌套型实际上是上面四种类型的组件,以上图为例,可以通过有限变化型的方案,将子组件B变为子组件B1/B2/B3,如许题目就变成了一个尺度的有限变化型。或者通过组合型的方案,将子组件B改为Builder,也可以将题目转化为一个尺度有限变化型或者组合型的题目。
无法复用型

组件之间差别很大,规律性不强,子组件也不类似的组件之间举行复用。复用的寄义就是重复使用类似布局的组件,布局完全不同的情况下,不发起使用组件复用。
摘自代码仓https://gitee.com/harmonyos-cases/cases/blob/master/docs/performance/component-reuse-overview.md,欢迎讨论~

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

魏晓东

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表