HarmonyOS 鸿蒙Next LazyForEach + Lottie动画 内存占用过大

发布于 1周前 作者 ionicwang 来自 鸿蒙OS

HarmonyOS 鸿蒙Next LazyForEach + Lottie动画 内存占用过大
参考 [如何定位列表中Canvas加载Lottie动画卡顿的问题-测试与调优-常见问题手册 - 华为HarmonyOS开发者 (huawei.com)] 编码。

用profiler观察滑动时的实时内存变化,发现内存占用过大。搞不清楚是代码原因还是GC不足。

根据官方文档编写如下两个文件代码于pages目录,lottie动画路径请自行添加:

//Index.ets

/**
* BasicDataSource代码见文档末尾附件: string类型数组的BasicDataSource代码
*/
import { NineListItem } from './NineListItem';

// BasicDataSource实现了IDataSource接口,用于管理listener监听,以及通知LazyForEach数据更新
class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private originDataArray: string[] = [];

  public totalCount(): number {
    return 0;
  }

  public getData(index: number): string {
    return this.originDataArray[index];
  }

  // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      console.info('add listener');
      this.listeners.push(listener);
    }
  }

  // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      console.info('remove listener');
      this.listeners.splice(pos, 1);
    }
  }

  // 通知LazyForEach组件需要重载所有子组件
  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    })
  }

  // 通知LazyForEach组件需要在index对应索引处添加子组件
  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
      // 写法2:listener.onDatasetChange([{type: DataOperationType.ADD, index: index}]);
    })
  }

  // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index);
      // 写法2:listener.onDatasetChange([{type: DataOperationType.CHANGE, index: index}]);
    })
  }

  // 通知LazyForEach组件需要在index对应索引处删除该子组件
  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
      // 写法2:listener.onDatasetChange([{type: DataOperationType.DELETE, index: index}]);
    })
  }

  // 通知LazyForEach组件将from索引和to索引处的子组件进行交换
  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to);
      // 写法2:listener.onDatasetChange(
      //         [{type: DataOperationType.EXCHANGE, index: {start: from, end: to}});
    })
  }

  notifyDatasetChange(operations: DataOperation[]): void {
    this.listeners.forEach(listener => {
      listener.onDatasetChange(operations);
    })
  }
}

class MyDataSource extends BasicDataSource {
  private dataArray: string[] = [];

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): string {
    return this.dataArray[index];
  }

  public pushData(data: string): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

declare class ArkTools {
  static hintGC(): void;
}

@Entry
@Component
struct MyComponent {
  private data: MyDataSource = new MyDataSource();

  aboutToAppear() {
    for (let i = 0; i <= 50; i++) {
      this.data.pushData(`Hello ${i}`)
    }
  }

  build() {

    Column(){
      Button("触发HintGC").onClick((event: ClickEvent) => {
      ArkTools.hintGC();  //方法内直接调用
    })

      List({ space: 3 }) {
        LazyForEach(this.data, (item: string) => {

          NineListItem({index:item})

        }, (item: string) => item)
      }.cachedCount(2)
    }


  }
}

//NineListItem.ets

import Lottie, { AnimationItem } from '@ohos/lottie'

@Reusable
@Component
export struct NineListItem {
  @State @Require index: string = 'a';
  @State isPlaying: boolean = false;
  private mainRenderingSettings: RenderingContextSettings = new RenderingContextSettings(true)
  private mainCanvasRenderingContext: CanvasRenderingContext2D =
    new CanvasRenderingContext2D(this.mainRenderingSettings)
  private animateName: string = "homeLiveFeedAnimate" + (Math.random() * 1000).toFixed()

  private animateItem: AnimationItem | null = null;

  aboutToDisappear(): void {
    // 组件销毁时先释放动画对象
    this.animateItem?.destroy();
    this.animateItem = null;
    Lottie.destroy(this.animateName)
  }

  build() {
    Row() {

            Canvas(this.mainCanvasRenderingContext)
              .width(200)
              .height(200)
              .backgroundColor(Color.Gray)
              .onReady(() => {
                this.animateItem?.destroy();
                // 加载动画
                if (this.animateItem != null) {
                  // 可在此生命回调周期中加载动画,可以保证动画尺寸正确
                  this.animateItem?.resize();
                } else {
                  // 抗锯齿的设置
                  this.mainCanvasRenderingContext.imageSmoothingEnabled = true;
                  this.mainCanvasRenderingContext.imageSmoothingQuality = 'medium'
                  this.animateItem = Lottie.loadAnimation({
                    container: this.mainCanvasRenderingContext,
                    renderer: 'canvas', // canvas 渲染模式
                    loop: true,
                    autoplay: false,
                    name: this.animateName,
                    contentMode: 'Contain',
                    path: "pages/lottie/animation.json",
                  })
                  this.animateItem.changeColor([225, 25, 100, 1]);
                  this.animateItem.play()
                }})
      Text("Hello!!")


    }
  }
}

更多关于HarmonyOS 鸿蒙Next LazyForEach + Lottie动画 内存占用过大的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html

4 回复

治标不治本思路:

该思路停止了不可见动画的后台播放,但是没有销毁掉对应item,不过也应该够用了?

参考 Lottie动画在列表中滚动卡顿如何优化-ArkUI开发框架-开发-常见问题手册 - 华为HarmonyOS开发者 (huawei.com)

添加如下代码至 NineListItem.ets 的 Canvas组件。当不可见时停止动画播放。

Canvas(this.mainCanvasRenderingContext)
  .onVisibleAreaChange([0.0, 1.0],
    (isVisible: boolean, currentRatio: number) =&gt; {
      if (isVisible) {
        console.log('onVisibleAreaChange:' + 'play' + ' index: ' + this.index)
        this.animateItem?.play();
      } else {
        console.log('onVisibleAreaChange:' + 'stop' + ' index: ' + this.index)
        this.animateItem?.stop();

      }
    })

更多关于HarmonyOS 鸿蒙Next LazyForEach + Lottie动画 内存占用过大的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


使用LazyForEach的list 要指定高度,比如.height(‘100%’),否则懒加载不会生效的。

我认为关键点不在 .height(100%") 设置上。

我的观察是提问中的程序还是执行了懒加载的,应用冷启动后初始内存90MB左右。在50个item时内存占用 420MB左右。

不断上下滑动屏幕,峰值内存会飙到500多MB,但最后常驻会在420左右。

我的感觉是懒加载后,屏幕不可见部分的item没有被销毁。

提问:1. 懒加载把列表加载全后不会销毁之间加载过但是不可见的items吗? 2. 如何成功销毁掉不可见部分的lottie items?

针对HarmonyOS鸿蒙系统中使用Next LazyForEach与Lottie动画导致的内存占用过大问题,可能的原因及解决方案简述如下:

在鸿蒙系统中,Next LazyForEach用于高效渲染列表项,而Lottie动画则负责动态效果展示。当这两者结合使用时,若处理不当,确实可能导致内存占用显著增加。

内存占用过大可能源于多个方面:

  1. 动画资源过大:Lottie动画文件若过于复杂或尺寸过大,会直接导致内存消耗增加。优化动画文件,减少不必要的细节和帧数,有助于降低内存占用。

  2. 列表项缓存:Next LazyForEach虽然旨在按需加载列表项,但若未正确管理缓存,可能导致已渲染项在内存中持续占用空间。检查并确保列表项的缓存策略合理,避免不必要的内存泄漏。

  3. 内存泄漏:代码中的内存泄漏同样会导致内存占用不断攀升。使用鸿蒙提供的内存分析工具,定期检查并修复潜在的内存泄漏问题。

综上所述,针对内存占用过大问题,建议从优化动画资源、管理列表项缓存以及排查内存泄漏等方面入手。若问题依旧无法解决,请联系官网客服,官网地址是:https://www.itying.com/category-93-b0.html

回到顶部