三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

HarmonyOS 7.0 / API 26 LazyForEach 卡顿复现:用构建计数器抓出断点切换后的重复渲染

HarmonyOS 7.0 / API 26 LazyForEach 卡顿复现:用构建计数器抓出断点切换后的重复渲染

HarmonyOS 7.0 / API 26 LazyForEach 卡顿复现:用构建计数器抓出断点切换后的重复渲染

先看开发现场

HarmonyOS 7.0 / API 26 做多设备页面时,折叠屏展开或平板窗口拖拽以后,LazyForEach 列表突然闪一下,图片重新加载,滚动位置也有概率跳动。这个问题最烦的地方是:本地看起来只是“偶尔卡一下”,但真到用户手里就是体验不稳。

我不建议一上来就猜图片缓存、网络慢、组件性能差。更直接的办法是加一个构建计数器,先证明列表项是不是在断点切换后被重新构建了。只要构建次数异常上涨,根因就往 key、数据源引用、父容器重建这三个方向查。

适用范围

这篇按 **HarmonyOS 7.0 / API 26** 的多设备适配场景来写,重点是 ArkUI 列表在 DynamicLayout、折叠屏、平板分屏和窗口态变化下的稳定性。示例代码用 ArkTS/TypeScript 风格表达,核心逻辑可以直接放进调试工具类或测试页里验证。

先建一个构建计数器

不要只靠肉眼看闪不闪。列表项有没有重复构建,应该能打印出来。

interface BuildRecord { key: string; count: number; lastReason: string; updatedAt: number; } export class ListItemBuildCounter { private records = new Map<string, BuildRecord>(); markBuild(key: string, reason: string): void { const old = this.records.get(key); const next: BuildRecord = { key, count: old ? old.count + 1 : 1, lastReason: reason, updatedAt: Date.now() }; this.records.set(key, next); } snapshot(): BuildRecord[] { return Array.from(this.records.values()).sort((a, b) => b.count - a.count); } findHotKeys(limit: number = 5): BuildRecord[] { return this.snapshot().filter(item => item.count > 1).slice(0, limit); } }

这个计数器很简单,但它能把问题说清楚:是所有 item 都重新构建,还是只有部分 item 抖动;是切断点时构建,还是数据刷新时构建。

错误复现:把布局模式拼进 key

type LayoutMode = 'singleColumn' | 'masterDetail'; interface ArticleRow { id: string; title: string; cover: string; } class UnstableKeyBuilder { build(item: ArticleRow, mode: LayoutMode): string { return mode + ':' + item.id; } } const rows: ArticleRow[] = [ { id: 'a1001', title: 'ArkUI 列表性能排查', cover: 'cover-a.png' }, { id: 'a1002', title: 'DynamicLayout 多设备适配', cover: 'cover-b.png' } ]; const badKey = new UnstableKeyBuilder(); const before = rows.map(item => badKey.build(item, 'singleColumn')); const after = rows.map(item => badKey.build(item, 'masterDetail')); console.info(before[0]); // singleColumn:a1001 console.info(after[0]); // masterDetail:a1001 console.info(before[0] === after[0]); // false

这里已经复现了根因:同一条数据,断点切换前后 key 不一样。LazyForEach 无法确认它是同一个 item,就会倾向于重新构建。

正确做法:key 只认业务身份

class StableKeyBuilder { build(item: ArticleRow): string { return item.id; } } interface RenderPolicy { mode: LayoutMode; lanes: number; imageRatio: number; titleMaxLines: number; } class LayoutRenderPolicyResolver { resolve(widthVp: number): RenderPolicy { if (widthVp >= 900) { return { mode: 'masterDetail', lanes: 2, imageRatio: 16 / 10, titleMaxLines: 2 }; } return { mode: 'singleColumn', lanes: 1, imageRatio: 16 / 9, titleMaxLines: 2 }; } }

业务身份和布局策略必须分开。key 只用 item.id;单双栏、图片比例、列数、标题行数都放进 RenderPolicy。这样断点切换时变的是展示策略,不是数据身份。

案例一:用计数器验证 key 是否稳定

const counter = new ListItemBuildCounter(); const stableKey = new StableKeyBuilder(); const policyResolver = new LayoutRenderPolicyResolver(); function renderRows(widthVp: number, reason: string): void { const policy = policyResolver.resolve(widthVp); rows.forEach(item => { const key = stableKey.build(item); counter.markBuild(key, reason + ':' + policy.mode); }); } renderRows(390, 'first-render'); renderRows(980, 'foldable-expanded'); console.info(counter.snapshot()); // 期望输出:a1001 和 a1002 的 count 都是 2,但 key 没有变化 // 如果 key 变了,计数器里会出现 singleColumn:a1001 和 masterDetail:a1001 两条记录

注意,这里 count 变成 2 不一定是问题。关键要看 key 是否稳定。如果同一个业务 ID 变成了多个 key,才是列表复用失败的信号。

案例二:断点拖拽时合并布局事件

窗口拖拽时,如果每个 width 变化都触发列表重排,构建次数也会暴涨。需要把高频变化合并。

interface BreakpointEvent { widthVp: number; reason: string; timestamp: number; } export class BreakpointEventMerger { private lastBucket: string = 'compact'; resolveBucket(widthVp: number): string { if (widthVp >= 1200) return 'expanded'; if (widthVp >= 900) return 'medium'; return 'compact'; } shouldApply(event: BreakpointEvent): boolean { const bucket = this.resolveBucket(event.widthVp); if (bucket === this.lastBucket) { return false; } this.lastBucket = bucket; return true; } } const merger = new BreakpointEventMerger(); const events: BreakpointEvent[] = [ { widthVp: 910, reason: 'dragging', timestamp: 1 }, { widthVp: 930, reason: 'dragging', timestamp: 2 }, { widthVp: 960, reason: 'dragging', timestamp: 3 }, { widthVp: 1210, reason: 'dragging', timestamp: 4 } ]; const applied = events.filter(event => merger.shouldApply(event)); console.info(applied.map(item => item.widthVp)); // 期望输出:910、1210。同一断点内的 930、960 不触发完整布局更新

这个例子解决的是拖拽窗口时的抖动。宽度一直变,但断点没有变,就不要让列表跟着完整刷新。

ArkUI 页面里怎么接

@Component struct StableLazyListPage { @Prop rows: ArticleRow[]; @State widthVp: number = 390; private keyBuilder = new StableKeyBuilder(); private policyResolver = new LayoutRenderPolicyResolver(); private counter = new ListItemBuildCounter(); build() { const policy = this.policyResolver.resolve(this.widthVp); List() { LazyForEach(this.rows, (item: ArticleRow) => { ListItem() { ArticleCard({ item, imageRatio: policy.imageRatio, titleMaxLines: policy.titleMaxLines }) } .onAppear(() => { this.counter.markBuild(this.keyBuilder.build(item), 'onAppear:' + policy.mode); }) }, (item: ArticleRow) => this.keyBuilder.build(item)) } .lanes(policy.lanes) } }

这段页面代码有两个检查点:LazyForEach 的 key 稳定,onAppear 里能记录构建情况。上线前可以把 counter 输出接到日志里,确认断点切换没有把列表全部打散。

排查顺序

顺序检查点通过标准
1item key只使用业务 ID,不拼 layoutMode
2数据源引用断点变化不重新 new 一份列表
3图片缓存 key不把 widthBucket 拼进同一图片资源
4断点事件同一断点内拖拽不触发完整刷新
5构建计数器热点 key 数量可解释,不出现重复业务身份

这张表适合收藏,因为后面遇到列表闪烁时可以直接按顺序查。

最后给一个判断标准

如果列表卡顿只发生在折叠屏展开、平板分屏和窗口拖拽时,优先查断点切换,不要先查网络。只要 key 稳定、数据源稳定、断点事件合并,LazyForEach 的复用能力才有发挥空间。

如果你也遇到过“手机正常,大屏一切就闪”的问题,可以先把设备形态、窗口宽度和 key 输出贴出来,基本能很快判断是不是断点切换把列表打散了。

← 返回列表