公司动态
HarmonyOS Repeat 虚拟滚动实战:模板分流、缓存数量与懒加载
HarmonyOS Repeat 虚拟滚动实战模板分流、缓存数量与懒加载长列表卡顿往往不是“数据太多”这一句能解释。一次创建过多子组件、键值不稳定导致整批重建、不同卡片共用错误的复用池、缓存数量按总数据量设置都会让滑动过程产生额外构建和内存压力。Repeat的虚拟滚动把可见窗口、组件复用和按需补数放在同一条链路中但前提是开发者给出可靠的数据身份与组件分类。本文实现一个包含文字、图片和视频卡片的动态信息流。读完后你可以明确回答三个问题哪一项能复用、复用到哪个组件类型、数据尚未到达时由谁补齐。1. 先把卡顿拆成构建、复用和补数列表问题可按发生时机分类。首屏就慢通常是一次构建太多节点或图片解码过重滑动一段后才掉帧常见原因是key变化、复用失败或缓存过大滑到数据尾部出现空白则要检查totalCount与懒加载回调是否一致。首屏创建慢 - 缩小可见构建范围检查复杂子树 往返滑动重建 - 检查稳定key与组件分类 内存持续增长 - 按卡片成本设置cachedCount 尾部出现空洞 - 核对totalCount、占位项和补数回调不要只观察平均帧率。组件创建次数、数据请求次数、图片内存和长帧位置更能说明是哪一层失效。2. API版本决定可采用的策略Repeat、each、key、virtualScroll和template从API 12起可用VirtualScrollOptions.reusable从API 18起提供按需补数相关的onLazyLoading与onTotalCount从API 19起提供。项目的compatibleSdkVersion低于这些版本时需要分别准备兼容分支。能力起始版本作用Repeat虚拟滚动API 12只构建可见范围及缓存项reusable开关API 18控制被移出窗口的节点是否进入复用懒加载回调API 19数据未就绪时按索引补数版本表不是形式信息。若团队设备跨度较大应先用API 12基础链路跑通再逐步开启高版本能力。3. 数据身份不能由数组下标代替删除、插入、置顶会改变数组下标。如果key使用index原来的组件状态可能错误地落到另一条数据上。稳定key应来自业务主键并且在同一个列表生命周期中保持唯一。typeFeedKindtext|image|video;interfaceFeedItem{id:string;kind:FeedKind;title:string;coverUrl?:string;durationSec?:number;}functionfeedKey(item:FeedItem):string{return${item.kind}:${item.id};}把kind放进key并不是替代分类而是避免不同数据域的主键碰撞。组件进入哪个复用池仍由后面的templateId决定。4. 虚拟滚动链路的五个环节数据先被准备为稳定对象随后根据类型选择渲染分支只创建可见项和缓存项。离开窗口的节点若允许复用就进入对应缓存池接近未加载区域时再由数据层补齐。任何一环失去边界都会退化成“边滑边重建”或“提前加载全部”。5. 先实现单一类型的最小链路下面的例子只渲染文本卡片用于确认key和虚拟窗口本身工作正常。totalCount必须表示列表逻辑总数而不是随意写一个比数据大很多的常量。Componentstruct TextFeedList{Stateitems:FeedItem[][];build(){List({space:12}){RepeatFeedItem(this.items).each((repeatItem:RepeatItemFeedItem){ListItem(){Text(repeatItem.item.title).fontSize(18).padding(16)}}).key((item:FeedItem)feedKey(item)).virtualScroll({totalCount:this.items.length})}.cachedCount(2)}}先用插入、删除、排序和往返滑动验证组件身份。若状态串行再增加卡片类型只会让问题更隐蔽。6. 不同卡片必须进入不同复用池文字、图片和视频卡片的组件树与资源成本不同。使用templateId按数据类型分流再为每个template设置缓存数量可以避免视频节点被当作文本节点复用。RepeatFeedItem(this.items).each((repeatItem:RepeatItemFeedItem){ListItem(){Text(repeatItem.item.title)}}).key((item:FeedItem)feedKey(item)).virtualScroll({totalCount:this.items.length,reusable:true}).templateId((item:FeedItem)item.kind).template(text,(repeatItem:RepeatItemFeedItem){ListItem(){TextCard({item:repeatItem.item})}},{cachedCount:4}).template(image,(repeatItem:RepeatItemFeedItem){ListItem(){ImageCard({item:repeatItem.item})}},{cachedCount:2}).template(video,(repeatItem:RepeatItemFeedItem){ListItem(){VideoCoverCard({item:repeatItem.item})}},{cachedCount:1})默认each分支仍应保留以处理未知类型或灰度数据。不要为每个业务状态创建一个类型例如“已读图片”“未读图片”若组件结构相同应在同一分支内改变样式。7. 缓存数量应按组件成本计算cachedCount不是数据预加载条数也不应等于服务端分页大小。它描述复用池愿意保留的组件数量。轻量文本卡片可多留几项包含大图或媒体资源的卡片要更克制。interfaceCacheBudget{text:number;image:number;video:number;}constcacheBudget:CacheBudget{text:4,image:2,video:1};调参时从小值开始记录往返滑动的创建次数和内存峰值。如果增加缓存只减少极少构建却显著提高图片或解码器驻留就应回退。8. 数据更新要保留未变化对象的身份收到分页数据后不要先深拷贝整个数组再全部替换。只追加新对象更新已有项时也只替换确实变化的数据。这样Repeat能根据key识别未变化项。functionmergePage(current:FeedItem[],incoming:FeedItem[]):FeedItem[]{constbyId:Mapstring,FeedItemnewMap(current.map((item:FeedItem)[feedKey(item),item]));incoming.forEach((item:FeedItem){byId.set(feedKey(item),item);});returnArray.from(byId.values());}若服务端返回顺序有意义应额外按游标顺序合并不能依赖Map掩盖排序规则。关键是稳定身份而不是固定数据顺序。9. 懒加载要区分逻辑总数与已到达数据当API 19能力可用时totalCount可以表示服务端已知总数缺失索引由onLazyLoading触发补数。请求层必须合并相邻索引并防止同一页并发请求。classFeedPageLoader{privateloadingPages:SetnumbernewSet();asyncloadIndex(index:number,pageSize:number):Promisevoid{constpageMath.floor(index/pageSize);if(this.loadingPages.has(page)){return;}this.loadingPages.add(page);try{awaitthis.fetchPage(page,pageSize);}finally{this.loadingPages.delete(page);}}privateasyncfetchPage(page:number,pageSize:number):Promisevoid{// 调用仓储层并把结果写回数据源}}补数失败时保留可重试的占位状态不要反复增减totalCount制造列表跳动。若服务端不知道总数可以采用“当前数据量加一个加载占位项”的协议。10. 复用组件内部不能藏一次性状态组件被复用后会接收新的repeatItem.item。播放器、订阅、定时器或异步图片请求若只在首次创建时绑定就可能继续服务旧数据。重资源卡片应显式实现绑定与解绑。Componentstruct VideoCoverCard{Propitem:FeedItem;privateboundId:string;aboutToAppear():void{this.bind(this.item.id);}aboutToDisappear():void{this.unbind(this.boundId);}privatebind(id:string):void{this.boundIdid;}privateunbind(id:string):void{// 取消与该id相关的订阅或加载任务}build(){Text(this.item.title)}}实际项目还要关注属性变化时的重新绑定不能仅依赖出现与消失回调。最安全的做法是让卡片只保存与当前业务ID可核对的资源句柄。11. 三种错误会让虚拟列表退化key随位置变化插入一条数据后大量卡片被视为新节点。分类粒度过细每个状态一个分支复用池被切得过碎。缓存盲目增大屏外重组件长期驻留内存与解码压力上升。还要避免在卡片build路径中排序大数组、同步读取文件或创建网络请求。构建函数应消费准备好的视图数据不承担数据加工。12. 建立可重复的滑动验收场景准备固定的1000条混合数据包含稳定比例的三种卡片。冷启动后从顶部匀速滑到底再立即回到顶部重复三轮。记录首屏时间、组件创建次数、长帧、内存峰值和分页请求次数。[ ] 插入、删除和置顶后卡片状态未串行 [ ] 相同数据往返滑动时创建次数明显下降 [ ] 三种卡片进入各自的复用分支 [ ] 缓存增大不会导致内存持续上升 [ ] 同一分页不会并发请求两次 [ ] 补数失败可重试且列表位置不跳动13. Repeat虚拟滚动资料索引控制列表渲染范围最佳实践Repeat、RepeatItem、VirtualScrollOptions与TemplateOptions以本机HarmonyOS SDK API 23声明为准。Repeat优化的核心不是少写几个组件而是让数据身份、组件类型、可见窗口和补数协议保持一致。key决定“是不是同一条数据”templateId决定“能不能进入同一复用池”cachedCount决定“愿意为复用付出多少内存”。把这三层分开调列表性能才可解释、可复现。