列表里有 10 万条数据,难道真的需要创建 10 万个 UI 节点?

文章封面

做长列表的时候,最开始的思路很简单:数据有多少条,就创建多少个节点。

结果真做的时候发现不对:10 万条数据,全部创建节点,内存直接爆了。滚动也卡得不行。

这时候才意识到:不是所有节点都要创建。屏幕上只能看到十几条,为什么要创建 10 万个?

一、先想清楚:屏幕上能看到多少

先做个简单的计算。屏幕高度是 800px,每个列表项高度是 80px。那屏幕上最多能看到多少条?

概念数值
屏幕高度800px
列表项高度80px
一屏能看到约 10 条

也就是说,不管你有 100 条还是 10 万条,屏幕上同时显示的就 10 条左右。

那剩下的 99990 条,为什么要创建节点?

二、LazyForEach 和 NodeAdapter 是什么

LazyForEach 是 ArkTS 自带的懒加载列表。它会根据可视区域,只创建你能看到的那几条。

NodeAdapter 是更底层的方式。它直接管理 FrameNode,你自己决定什么时候创建节点、什么时候销毁节点。

方案层级灵活性
LazyForEachArkTS 声明式高,自动管理
NodeAdapterNative 节点级更高,自己管理

NodeAdapter 适合什么场景?比如自定义列表项很复杂、需要 Native 层面优化的情况。

三、一个 FrameNode 的一生

跟踪一个列表节点,它的一生是这样的:

  1. 列表滚动到这个位置,需要显示这个 item;
  2. 列表向 Adapter 请求这个位置的节点;
  3. Adapter 创建 FrameNode,设置数据;
  4. 节点挂到列表上,显示出来;
  5. 用户继续滚动,这个 item 滑出屏幕;
  6. 节点从列表上移除,进入缓存池;
  7. 用户往回滚,又滑到这个位置;
  8. 从缓存池拿出来,复用,重新设置数据;
  9. 一直不用了,缓存池满了,真正销毁。

这段代码解决什么问题: 实现 NodeAdapter。
文件: list/MyAdapter.ets
用途: 长列表节点管理
接入位置: 自定义长列表

import { NodeAdapter, FrameNode } from '@kit.ArkUI';

class MyAdapter extends NodeAdapter {
  private data: string[] = [];
  
  // 总共有多少个节点
  totalNodeCount(): number {
    return this.data.length;
  }
  
  // 请求创建节点
  onCreateNode(index: number): FrameNode {
    let node = new FrameNode();
    node.setData(this.data[index]);
    return node;
  }
  
  // 节点滑出屏幕
  onRemoveNode(index: number, node: FrameNode): void {
    // 不是自动释放,要自己处理
    // 可以放进缓存池复用
  }
}

这里最关键的是:节点滑出屏幕,不是自动释放。你要自己决定是放缓存池还是直接销毁。

系统架构图

四、缓存池是什么

缓存池就是把滑出屏幕的节点存起来,等需要的时候再拿出来用,不用重新创建。

为什么要缓存?因为创建节点是有开销的。每次滚动都重新创建,滚动就卡。

缓存大小效果
0每次都新建,卡
合适复用节点,流畅
太大内存占用高

缓存不是越大越好。缓存太多,内存就上去了。合适就好。

五、节点复用的时候要注意什么

节点从缓存池拿出来复用,不是直接用就行。它里面还是旧数据,要重新设置。

很多人出问题就是:节点复用了,但数据没更新,列表显示错了。

还有一个问题:节点里的状态。比如这个列表项有个开关,上次用户打开了,这次复用的时候,开关状态还在。不对,这是新的数据,状态要重置。

运行效果图

六、几个容易踩的坑

第一个坑:把"移出可视区域"理解成节点已经释放。不是,只是从列表上移除了,对象还在内存里。

第二个坑:所有滑出节点无限塞进 cachePool。缓存太大,内存爆了。

第三个坑:节点缓存回来以后仍保留旧业务状态。开关、输入框内容这些,都要重置。

第四个坑:Adapter 已接管子节点后继续直接 addChild。节点已经归 Adapter 管了,你别自己再加。

第五个坑:totalNodeCount 与真实数据数量不同步。数据变了,Adapter 不知道,列表就错了。

第六个坑:每次滚动都重新创建重量级 Native 资源。图片、Shader 这些,要复用,别每次新建。

第七个坑:为了减少创建次数把缓存开得无限大。内存代价太大。

七、和 LazyForEach 的区别

很多人以为 NodeAdapter 就是换一种写法的 LazyForEach。不对。

对比LazyForEachNodeAdapter
层级ArkTS 声明式Native 节点级
管理系统自动你自己管
灵活性够用更高
适合普通列表复杂自定义列表

NodeAdapter 不是替代 LazyForEach,是给你更底层的控制力。普通列表用 LazyForEach 就够了。

业务流程图

这次做长列表最大的体会是:长列表的核心不是"懒加载"这三个字,是节点生命周期管理。什么时候创建、什么时候缓存、什么时候复用、什么时候销毁,每一步都要想清楚。

真正做的时候,最容易忽略的不是怎么创建节点,而是节点复用和缓存管理。创建节点很简单,难的是怎么高效地复用,不浪费内存,也不浪费创建开销。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐