接下来,需要创建示例数据。在自定义组件ChatListDisplayView中,创建一个ChatListData类型的局部变量chatList_Lazy,并在aboutToAppear()方法中创建示例数据。

@Component  
export struct ChatListDisplayView {  
    private chatList_Lazy: ChatListData = new ChatListData()  
    ......  
    async aboutToAppear(): Promise<void> {  
    await makeDataLocal(this.chatList_Lazy)  
    ......  
   }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

最后,在List组件容器中,使用LazyForEach接口遍历数据源this.chatList_Lazy循环生成ListItem列表项。其中,chatViewBuilder()方法用于布局页面列表项;代码行(msg: ChatModel) => msg.user.userId使用用户的编码作为列表项唯一的键值编码,用于区分不同的列表项。至此,使用懒加载代码实现完成。

build() {  
    Column() {  
        List() {  
        ......  
        LazyForEach(this.chatList_Lazy, (msg: ChatModel) => {  
        ListItem() {  
        ......  
        this.chatViewBuilder(msg)  
        ......  
        }
       }, (msg: ChatModel) => msg.user.userId)  
       ......  
    }  
  }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

效果对比

在聊天示例程序中,通过模拟10000条聊天数据,来对比测试在开启、关闭懒加载时的性能。测试项包含页面启动完成时间和列表滑动时帧率。

使用ForEach一次性加载时,页面启动完成时间为3530ms;开懒加载时,页面启动完成时间为752ms。开启懒加载后,启动完成时间缩短为开启前的21.3%。

使用ForEach一次性加载时,丢帧率为26.64%;开懒加载时,丢帧率降低到2.33%。

缓存列表项

原理机制

虽然需要尽量避免一次性加载全部列表数据项,但合理的预先缓存当前屏幕上下几页的列表项内容会给用户带来更好的体验,例如通过缓存避免“滑动白块”现象。

LazyForEach懒加载可以通过设置cachedCount属性来指定缓存数量。在设置cachedCount后,除屏幕内显示的ListItem组件外,还会预先将屏幕可视区外指定数量的列表项数据缓存起来。

详细过程如下:

  1. 当列表滑动,缓存列表项需要从屏幕可视区外进入可视区内时,只用创建、渲染组件即可,相比不设置cachedCount提升了显示效率。

  2. 当列表不断滑动,屏幕可视区外缓存的列表项数量少于cachedCount设置数量时,会触发列表项数据加载事件,继续预加载缓存列表项。比如,如果cachedCount设置为10,滑动到第10项数据展示在屏幕上时,会请求把第11~20列表项数据加载缓存起来。

  3. 当上滑下滑间隔进行时,列表数据两个方向的数据都会缓存起来。

  4. 如果不显式设置cachedCount,默认缓存1条数据。

使用场景和限制

缓存列表适合加载列表项数据比较耗时的场景。比如,需要从网络上获取视频数据、图片并通过ListItem展示。通过预先加载并缓存,缩短渲染前的准备时间,提升列表响应速度。

使用限制为:缓存列表项仅在使用LazyForEach懒加载时有效,ForEach循环渲染会一次性加载全量数据,不需要设置缓存列表项。

实现示例

List/Grid容器组件的cachedCount属性用于为LazyForEach懒加载设置列表项ListItem的最少缓存数量。应用可以通过增加cachedCount参数,调整屏幕外预加载项的数量。提供一个开关用于设置是否使能该属性,如下所示。在设置cachedCount后,当列表界面滑动时,除了获取屏幕上展示的数据,还会额外获取指定数量的列表项数据缓存起来。

build() {
  Column() {
    List() {
      ...
      ...
      LazyForEach(this.chatListData, (msg: ChatModel) => {
        ListItem() {
          ChatView({ chatItem: msg })
        }
      }, (msg: ChatModel) => msg.user.userId)
    }
    .backgroundColor(Color.White)
    .listDirection(Axis.Vertical)

    ...
    ...
    .cachedCount(this.list_cachedCount ? Constants.CACHED_COUNT : 0) // 缓存列表数量  
  }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

效果对比

在示例程序中,屏幕上每页展示9条数据。基于示例程序,测试了不同缓存数量对帧率的影响情况,不设置缓存数量时,丢帧率为7.79%,当逐渐增加缓存数量时,丢帧率降低。当设置当前屏幕展示数量的一半,即缓存5个列表项时,丢帧率最低。再增加缓存数量,丢帧率不再有显著的下降,增加缓存数量太多时,甚至会影响丢帧率。测试数据仅限于示例程序,不同的应用程序设置的最佳缓存数量不一致,需要针对应用程序测试得出最佳缓存数量。

应该如何根据实际场景,设置缓存数量的值呢? 例如列表项中需要显示网络数据,而网络数据加载较慢,为了提升列表信息的浏览效率和浏览体验,可以适当的多设置一些缓存数量;如果列表中需要加载一些大图或者视频等,这些数据占用的内存较大,为了减少内存占用,需要适当减少缓存数量的设置;因此,在实际场景中,需要不断尝试验证,设置适当的缓存数量,来达到体验和内存的平衡。

组件复用

原理机制

应用框架提供了组件复用能力,可复用组件从组件树上移除时,会进入到一个回收缓存区。后续创建新组件节点时,会复用缓存区中的节点,节约组件重新创建的时间。尤其在列表等场景下,其自定义子组件具有相同的组件布局结构,列表更新时仅有状态变量等数据差异。通过组件复用可以提高列表页面的加载速度和响应速度。

组件复用机制如下:

  1. 标记为@Reusable的组件从组件树上被移除时,组件和其对应的JSView对象都会被放入复用缓存中。

  2. 当列表滑动新的ListItem将要被显示,List组件树上需要新建节点时,将会从复用缓存中查找可复用的组件节点。

  3. 找到可复用节点并对其进行更新后添加到组件树中。从而节省了组件节点和JSView对象的创建时间。

@Reusable组件复用结合LazyForEach懒加载,可以进一步解决列表滑动场景的瓶颈问题,提供滑动场景下高性能创建组件的方式来提升滑动帧率。

使用场景和限制

若业务实现中存在以下场景,并成为UI线程的帧率瓶颈,推荐使用组件复用:

  1. 一帧内重复创建多个已经被销毁的自定义组件。

  2. 反复切换条件渲染的控制分支,且控制分支中的组件子树结构较重。

组件复用生效的条件是:

使用规则如下:

使用建议如下:

建议复用自定义组件时避免一切可能改变自定义组件的组件树结构和可能使可复用组件中产生重新布局的操作以将组件复用的性能提升到最高;

实现示例

在开发应用时,自定义组件被@Reusable装饰器修饰,表示该自定义组件可以复用。在自定义父组件下创建的可复用组件从组件树上移除后,会被加入父组件的可复用节点缓存里。在父组件再次创建可复用组件时,会通过更新可复用组件的方式,从缓存快速创建可复用组件。使用装饰器@Reusable标记一个组件属于可复用组件后,还需要实现自定义组件的生命周期回调函数aboutToReuse(),其参数为可复用组件的状态变量。调用可复用自定义组件时,父组件会给子组件传递构造数据。

示例代码如下所示:

/**
  * 可复用且优化布局的聊天页面组件
  */
@Reusable
@Component
struct ReusableOptLayoutChatView {
  @State chatItem: ChatModel = new ChatModel(new ChatContact('', ''), '', '', 0);

  aboutToReuse(params: Record<string, Object>): void {
    this.chatItem = params.chatItem as ChatModel;
    Logger.info(TAG, 'aboutToReuse=' + this.chatItem.toString());
  }

  build() {
    OptLayoutChatView({ chatItem: this.chatItem });
  }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

效果对比

在示例程序中,对列表项中的组件进行复用。经测试发现,因本示例复用组件的布局较简单,组件复用对本测试场景没有明显的性能提升效果。在实际场景中,应该如何用好组件复用这个特性呢?在列表项的布局复杂度更高时,组件复用的效果更好。因为更高复杂度的组件布局,初始化时需要消耗更多的系统资源,因此在使用较高复杂的列表布局时,建议使用组件复用这个特性。

布局优化

常用布局类型

当前ArkUI应用框架提供了以下两类常用的布局方式:

线性布局: 例如Stack、Column、Row和Flex等,会把布局中的组件按照线性方向进行排布,如横向、纵向、Z轴方向等;这种布局使用简单方便、易于理解,但是在复杂的场景下往往会使用更多的组件数和较深的嵌套层次,维护困难,同时也增加了系统的开销;

高级布局: 往往可以使用更少的节点数和布局层级,实现更加复杂的布局效果,具有扁平化的特性;包括List、Grid、RelativeContainer等,在列表、宫格和混排布局等场景提供了扁平化的布局方式,例如RelativeContainer可以根据锚点来进行低嵌套层级复杂布局,而List和Grid又支持懒加载等提升性能的方法,同时降低了维护成本;因此,高级布局是更加推荐的布局方法。

使用场景和问题

在开发页面时,我们往往会习惯使用线性布局来实现页面构造,这种布局方法可能会导致组件树和嵌套层数过多的问题,在创建和布局阶段产生较大的性能开销,如下列示例场景:

布局中存在冗余布局,如build()函数下第一层的Column布局;例如GridContainer下的嵌套结构,使用了多个线性布局Column嵌套,层级较深。 还有下面的场景示例中也存在频繁使用线性布局导致嵌套过深的情况:

构建了10、20、30、40、50层的嵌套组件作为列表项,在列表中插入100条该嵌套组件,测试这些嵌套组件在滑动场景下对内存的影响,数据如下所示:

嵌套组件的示意结构如下所示:

从内存数据可以得知,嵌套层级越深,会有更大的系统内存开销。因此在开发过程中,要尽可能减少布局嵌套,使布局更加扁平化。那么应该如何进行布局优化呢?

布局优化思路

对于这些常见问题,将通过优化一个聊天列表项的页面布局,来展示布局优化的方法和思路。使用DevEco Studio集成的ArkUI Inspector等工具可以查看视图布局,场景预览图和优化前的布局结构如下所示:

从场景预览图中可以知道,列表项中包含了图片、消息数、昵称、聊天信息、时间这5个部分的内容;使用线性布局的写法方式,就是一个横向布局Row,嵌套了3个纵向布局Column,由于红色消息字体需要和图片进行重叠,还使用了Stack布局进行堆叠,最终的布局方式就如上图所示,共使用组件10个,嵌套4层,源码如下所示:

build() {
  Row() {
    Column() {
      Stack({ alignContent: Alignment.TopEnd }) {
        Image(this.chatItem.user.userImage) // 用户头像  
        ...
        if (this.chatItem.unreadMsgCount > 0) { // 红点消息大于0条时渲染红点  
          Text(`${this.chatItem.unreadMsgCount}`) // 消息数  
          ...
        }
      }
    }
    .layoutWeight(1)
    .padding({ right: 12 })

    Column() {
      Text(this.chatItem.user.userName) // 昵称  
        .fontColor(Color.Black)
        .fontSize(16)
        .margin({ bottom: 3 })
      Text(this.chatItem.lastMsg) // 聊天消息  
        .fontColor("#999999")
        .maxLines(1)
        .fontSize(14)
        .margin({ top: 5 })
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(3)

    Column() {
      Text(this.chatItem.lastTime) // 时间  
        .width(50)
        .fontColor("#999999")
        .textAlign(TextAlign.End)
        .maxLines(1)
        .fontSize(12)
        .margin({ right: 6, bottom: 27 })
    }
    .padding({ left: 15 })
    .alignItems(HorizontalAlign.End)
    .layoutWeight(1)
  }

  ...
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

其实这4个部分是有明确的相对关系的,如图片和消息数位于列表最左侧,时间位于列表最右侧,而昵称和聊天信息是在图片的右侧,并且上下分布,因此,就可以使用RelativeContainer布局来进行优化,优化后可以把组件数减少到5个,嵌套2层,大大减少了系统开销,源码如下所示:

build() {
  RelativeContainer() { // 相对布局  
    Image(this.chatItem.user.userImage) // 用户头像  
    ...
    .alignRules({
      top: { anchor: '__container__', align: VerticalAlign.Top },
      left: { anchor: '__container__', align: HorizontalAlign.Start }
    })
      .syncLoad(this.img_syncLoad ? true : false)
      .id("figure")

    if (this.chatItem.unreadMsgCount > 0) { // 红点消息大于0条时渲染红点  
      Text(`${this.chatItem.unreadMsgCount}`) // 消息数  
      ...
      .alignRules({
        top: { anchor: '__container__', align: VerticalAlign.Top },
        left: { anchor: '__container__', align: HorizontalAlign.Start }
      })
        .opacity(this.chatItem.unreadMsgCount > 0 ? 1 : 0)
        .id("badge")
    }

    Text(this.chatItem.user.userName) // 昵称  
    ...
    .alignRules({
      top: { anchor: '__container__', align: VerticalAlign.Top },
      left: { anchor: '__container__', align: HorizontalAlign.Start }
    })
      .id("user")

    Text(this.chatItem.lastTime) // 时间  
    ...
    .alignRules({
      top: { anchor: '__container__', align: VerticalAlign.Top },
      right: { anchor: '__container__', align: HorizontalAlign.End }
    })
      .id("time")
    Text(this.chatItem.lastMsg) // 聊天信息  
    ...
    .alignRules({
      top: { anchor: '__container__', align: VerticalAlign.Top },
      left: { anchor: '__container__', align: HorizontalAlign.Start }
    })
      .id("msg")
  }
  ...
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

class="table-box">
优化情况组件总数视图嵌套层数
优化前,使用线性布局104
优化后,使用扁平化布局52

从上述案例中可以看到,选用正确的布局组件,不但去除了中间嵌套的组件层级,减少了组件数量,使代码更易于维护,也避免系统绘制更多的布局组件,达到优化性能、减少内存占用的目的,这就是扁平化布局改造的思路。

系统还提供了更多的扁平化布局方案,例如绝对定位、自定义布局、Grid、GridRow等,适用更多不同的场景,具体的使用方法可以参考官方文档

总结

本文的聊天列表场景,分析了列表滑动性能的优化方法,包含懒加载、缓存列表项、组件复用、页面布局优化。对每个优化方法详细介绍了原理、使用场景,并基于示例程序给出了优化效果和对比数据。在开发类似列表场景时,可以借鉴这些优化方法。

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:

如何快速入门:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

基于ArkTS 开发:https://qr21.cn/FV7h05

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ……

鸿蒙开发面试真题(含参考答案):https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

data-report-view="{"mod":"1585297308_001","spm":"1001.2101.3001.6548","dest":"https://blog.csdn.net/weixin_61845324/article/details/138132849","extend1":"pc","ab":"new"}">> id="blogExtensionBox" style="width:400px;margin:auto;margin-top:12px" class="blog-extension-box"> class="blog_extension blog_extension_type2" id="blog_extension"> class="extension_official" data-report-click="{"spm":"1001.2101.3001.6471"}" data-report-view="{"spm":"1001.2101.3001.6471"}"> class="blog_extension_card_left"> class="blog_extension_card_cont"> 鸿蒙开发学习资料领取!!! class="blog_extension_card_cont_r"> 微信名片
注:本文转载自blog.csdn.net的沧海一笑-dj的文章"https://blog.csdn.net/dengjin20104042056/article/details/94767389"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接

评论记录:

未查询到任何数据!