HarmonyOS ArkTS:自动"缓存池复用监控日志"怎么做
先说结论:ArkUI 的
@Reusable复用池本身不是一个你能直接拿到的对象 ,也没有一个官方 API 让你"读出当前池子里有多少个实例"。 所以所谓"监控复用池",本质上是: ✅ 通过组件生命周期回调 + 统一打点工具 ,去推断复用发生了多少次、哪些组件在复用、有没有状态没重置导致串数据。
下面给你一套我在项目里会用的"监控方案",特点是:
- 侵入性低(加几行就能看到复用情况)
- 日志清晰(创建 / 复用 / 退出 / 重置)
- 能按
reuseId归类统计 - 能定位"复用导致的脏状态"问题
1)监控目标:我们到底想看到什么?
我建议至少把这 4 类信息打出来:
- create:首次创建(不是复用池拿出来的)
- reuse:从复用池中取出再利用(关键指标)
- detach / disappear:从树上移除/不可见(进入复用池的前置条件)
- state reset:复用时你做了哪些重置(防串数据)
然后统计两个核心指标:
reuseRate = reuseCount / (createCount + reuseCount)activeCount:当前在树上的实例数(帮助判断列表滚动时是否稳定)
2)核心思路:做一个全局 ReusePoolMonitor(只管日志和计数)
新建一个文件:
typescript
entry/src/main/ets/utils/ReusePoolMonitor.ts
// entry/src/main/ets/utils/ReusePoolMonitor.ts
type ReuseEvent =
| 'create'
| 'reuse'
| 'appear'
| 'disappear'
| 'reset';
type MonitorKey = string; // 通常用: `${reuseId}:${componentName}`
interface InstanceInfo {
instanceId: string; // 你自己生成的 id
firstSeenAt: number;
lastEventAt: number;
lastItemKey?: string; // 业务数据 key(例如 item.id)
}
interface GroupStats {
createCount: number;
reuseCount: number;
appearCount: number;
disappearCount: number;
resetCount: number;
activeInstanceIds: Set<string>;
instances: Map<string, InstanceInfo>; // instanceId -> info
}
class ReusePoolMonitor {
private groups: Map<MonitorKey, GroupStats> = new Map();
private enabled: boolean = true;
enable(on: boolean) {
this.enabled = on;
}
private getGroup(key: MonitorKey): GroupStats {
let g = this.groups.get(key);
if (!g) {
g = {
createCount: 0,
reuseCount: 0,
appearCount: 0,
disappearCount: 0,
resetCount: 0,
activeInstanceIds: new Set<string>(),
instances: new Map<string, InstanceInfo>(),
};
this.groups.set(key, g);
}
return g;
}
mark(
groupKey: MonitorKey,
event: ReuseEvent,
payload: { instanceId: string; itemKey?: string; extra?: string } = { instanceId: 'unknown' }
) {
if (!this.enabled) return;
const now = Date.now();
const g = this.getGroup(groupKey);
// 维护实例信息
let info = g.instances.get(payload.instanceId);
if (!info) {
info = {
instanceId: payload.instanceId,
firstSeenAt: now,
lastEventAt: now,
lastItemKey: payload.itemKey,
};
g.instances.set(payload.instanceId, info);
} else {
info.lastEventAt = now;
if (payload.itemKey !== undefined) info.lastItemKey = payload.itemKey;
}
// 统计
switch (event) {
case 'create':
g.createCount++;
break;
case 'reuse':
g.re_toggleReuseCount ? null : null; // 占位避免你复制后误删结构
g.reuseCount++;
break;
case 'appear':
g.appearCount++;
g.activeInstanceIds.add(payload.instanceId);
break;
case 'disappear':
g.disappearCount++;
g.activeInstanceIds.delete(payload.instanceId);
break;
case 'reset':
g.resetCount++;
break;
}
// 日志输出(格式尽量"人能看懂")
const itemPart = payload.itemKey !== undefined ? ` itemKey=${payload.itemKey}` : '';
const extraPart = payload.extra ? ` ${payload.extra}` : '';
console.info(
`[ReusableMonitor] [${groupKey}] ${event} instance=${payload.instanceId}${itemPart} active=${g.activeInstanceIds.size}${extraPart}`
);
}
snapshot(groupKey?: MonitorKey) {
if (!this.enabled) return;
const keys = groupKey ? [groupKey] : Array.from(this.groups.keys());
keys.forEach((k) => {
const g = this.groups.get(k);
if (!g) return;
const total = g.createCount + g.reuseCount;
const reuseRate = total === 0 ? 0 : (g.reuseCount / total) * 100;
console.info(
`[ReusableMonitor] [${k}] SNAPSHOT create=${g.createCount} reuse=${g.reuseCount} reuseRate=${reuseRate.toFixed(
1
)}% appear=${g.appearCount} disappear=${g.disappearCount} reset=${g.resetCount} active=${g.activeInstanceIds.size} knownInstances=${g.instances.size}`
);
});
}
}
export const reuseMonitor = new ReusePoolMonitor();
说明:上面我用的是"推断式监控"。你只要在生命周期里调用
reuseMonitor.mark(...),就能看到复用是否发生、发生频率如何。
3)在 @Reusable 组件里接入监控(关键:实例 ID + itemKey)
3.1 一个可复用的列表项组件(带监控日志)
kotlin
// entry/src/main/ets/components/BookRow.ets
import { reuseMonitor } from '../utils/ReusePoolMonitor';
@Reusable
@Component
export struct BookRow {
// 业务入参:列表数据
@Prop itemId: string = ''
@Prop title: string = ''
// 给每个组件实例一个固定的 id(用于追踪同一个实例被复用多少次)
private instanceId: string = `BookRow#${Math.random().toString(16).slice(2)}`;
// 你组件里可能有 @State,这就是最容易"复用串数据"的地方
@State private highlight: boolean = false;
aboutToAppear() {
// 说明:aboutToAppear 更像"进树前",你可以当作 appear
reuseMonitor.mark('BookRow:default', 'appear', {
instanceId: this.instanceId,
itemKey: this.itemId,
});
}
aboutToDisappear() {
reuseMonitor.mark('BookRow:default', 'disappear', {
instanceId: this.instanceId,
itemKey: this.itemId,
});
}
// 复用发生时会走这里(重点)
aboutToReuse(params: Record<string, object>) {
// params 里通常会带新的入参(不同版本/写法会有差异,你按你项目实际来取)
// 这里我们只演示:复用时重置状态 + 打日志
reuseMonitor.mark('BookRow:default', 'reuse', {
instanceId: this.instanceId,
itemKey: this.itemId,
extra: '(before reset)',
});
// ✅ 复用最关键的一步:重置本地状态,避免串数据
this.highlight = false;
reuseMonitor.mark('BookRow:default', 'reset', {
instanceId: this.instanceId,
itemKey: this.itemId,
extra: 'highlight=false',
});
}
// 首次创建时记录 create(你可以放在 aboutToAppear 的第一次触发里,这里简单起见写在构造逻辑附近不太好判断)
// 实际上:我们用一个小技巧 ------ instanceId 初次出现时在 monitor 内会新增实例,你也可以把 "firstSeenAt" 当 create
// 如果你一定要明确 create,可以在 aboutToAppear 里判断一个 flag。
private createdOnce: boolean = false;
build() {
if (!this.createdOnce) {
this.createdOnce = true;
reuseMonitor.mark('BookRow:default', 'create', {
instanceId: this.instanceId,
itemKey: this.itemId,
});
}
Row() {
Text(this.title)
.fontSize(16)
.fontColor(this.highlight ? '#2F7BFF' : '#222')
Blank()
Text(this.itemId)
.fontSize(12)
.fontColor('#999')
}
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.backgroundColor('#FFF')
.onClick(() => {
this.highlight = !this.highlight;
})
}
}
你会发现我特别强调
@State的 reset。 因为复用的 bug,十有八九就是:上一个 item 的状态残留到了下一个 item。
4)在列表里使用,并且定期打 Snapshot(更像真实排查)
typescript
// entry/src/main/ets/pages/ReuseListDemo.ets
import { reuseMonitor } from '../utils/ReusePoolMonitor';
import { BookRow } from '../components/BookRow';
@Entry
@Component
struct ReuseListDemo {
@State items: Array<{ id: string; title: string }> = []
aboutToAppear() {
this.items = new Array(200).fill(0).map((_, i) => ({
id: `ID-${i}`,
title: `第 ${i + 1} 本书:示例标题`,
}));
// 每隔 2 秒输出一次快照(调试用,上线前关掉)
setInterval(() => {
reuseMonitor.snapshot('BookRow:default');
}, 2000);
}
build() {
List() {
ForEach(this.items, (it) => {
ListItem() {
BookRow({ itemId: it.id, title: it.title })
.reuseId('BookRow:default') // 复用分组:同组才能复用
}
}, (it) => it.id) // key 很重要:帮助框架稳定识别 item
}
.width('100%')
.height('100%')
.backgroundColor('#F6F7F9')
}
}
5)你会看到怎样的日志?怎么读才"有用"?
你滚动列表时,大致会看到类似:
ini
[ReusableMonitor] [BookRow:default] create instance=BookRow#9af3 itemKey=ID-0 active=1
[ReusableMonitor] [BookRow:default] appear instance=BookRow#9af3 itemKey=ID-0 active=1
...滚动...
[ReusableMonitor] [BookRow:default] disappear instance=BookRow#9af3 itemKey=ID-0 active=10
[ReusableMonitor] [BookRow:default] reuse instance=BookRow#9af3 itemKey=ID-20 active=10 (before reset)
[ReusableMonitor] [BookRow:default] reset instance=BookRow#9af3 itemKey=ID-20 active=10 highlight=false
[ReusableMonitor] [BookRow:default] appear instance=BookRow#9af3 itemKey=ID-20 active=10
这行日志的"人话解读"是:
BookRow#9af3这个实例原来显示ID-0- 滚出屏幕后
disappear - 再次被拿出来复用显示
ID-20 - 复用时我们重置了 highlight,避免串数据
- 然后重新 appear
快照会输出:
ini
SNAPSHOT create=12 reuse=180 reuseRate=93.8% appear=... disappear=... reset=180 active=10 knownInstances=12
重点看两个数:
reuseRate:越高说明复用越充分(通常长列表会非常高)knownInstances:基本能近似理解为"池子里曾经创建过多少个实例"(不会无限增长的话,说明复用稳定)
6)最常见的"复用串数据"怎么用日志定位?
症状
你发现某一行高亮了、按钮 disabled 了、进度条停在某个值......但那是上一条数据的状态。
定位方式(很快)
- 看日志里同一个
instanceId - 它从
itemKeyAreuse 到itemKey=B - 如果你没看到对应的
reset日志(或 reset 不完整) → 99% 就是状态没清干净
我一般会把 reset 写得很"啰嗦",调试阶段宁可多打两行:
- reset highlight
- reset progress
- reset selected
- reset loading
调通后再精简。
7)上线建议:怎么不影响性能?
- 监控类里加
enable(false)开关 - 或者用环境变量控制(Debug 开、Release 关)
setInterval(snapshot)只在调试打开,上线必须关
示例:
arduino
reuseMonitor.enable(false) // release 环境
8)一句总结
复用池监控这件事,说白了就是:你无法直接看池子,但你能看"复用行为" 。 只要把 create / reuse / disappear / reset 这条链路跑通,再配上按 reuseId 的统计快照,排查性能和串状态问题会省非常多时间。