在 ArkTS 内存泄漏中,有一类是 ArkUI 组件泄漏,只需要有快照日志就能完成对此问题的分析,不依赖开发者做任何适配。其利用 ArkUI 框架自动标记已销毁组件,分析准确率很高。当前 ArkUI 组件相关泄漏非常普遍,建议在日常开发过程中对此类问题应改尽改,原因是:1)此类内存泄漏引用链明确,修改简单;2)快照日志获取容易,通过命令行、IDE 工具和 API 在开发态/运维态便捷获取日志;3)ArkUI 组件泄漏会导致绘制等相关的底层内存泄漏,这些底层内存泄漏分析相对比较复杂,容易造成干扰,影响其他 native 内存分析,可以通过修复组件泄漏减少干扰项,减少问题分析复杂度。
1. ArkUI 组件泄漏检测原理
ArkUI Component 组件生命周期特点是在 aboutToDisappear 之后,即表示相关界面消失,组件已销毁,以 Component 为例,其完整的生命周期如下图:

当 Component 生命周期结束,进入到 DISAPPERED 状态后,后续无法被复用,不应该被继续持有,否则可能引发内存泄漏。
根据此特点,可以制定专门针对 ArkUI 组件的泄漏检测。其原理如下:

在应用侧,会对组件进行代码功能的调用,当生命周期结束后(如,页面退出),系统侧的 ArkUI 框架会自动给组件的对象打上标记,例如将 isDeleting_设置为 true。当 ArkTS OOM 发生时,ArkTS 运行时会先进行一次 GC,清理掉可回收的对象,然后再导出快照。如果在导出的快照中,发现 Component 的 isDeleting_为 true,则说明快照中 Component 对象没有按照预期释放,可能仍被其他对象持有,导致泄漏。
在多个应用线上 ArkTS OOM 泄漏问题分析中,通过此原理检测出的泄漏问题,准确率高达 90%+,对于内存定位非常有效。当前,在 HarmonyOS7.0 上,已经支持该检测的组件有:
1) BuilderNode、FrameNode、RenderNode、XComponent,这些组件生命周期结束后,ArkUI 框架会将对象上的_isDisposed 属性置为 true。
2) Ability,生命周期结束后,ArkUI 框架会将对象上的 isDestroyed 属性置为 true。
3) 自定义组件 Component,生命周期结束后,ArkUI 框架会将对象上的 isDeleting_属性置为 true。
备注:不同类型的组件生命周期结束的函数名称不一定相同,但检测原理是一致的。
2. ArkUI 组件泄漏问题分析与定位
在获取到 ArkTS 堆快照日志文件后,使用 mem-analyzer 即可高效完成 ArkUI 组件泄漏问题的分析与定位。mem-analyzer 是一个集开发态日志抓取和运维态日志分析的命令行工具。
其下载链接如下:
https://gitcode.com/ancimoon/developtools_dfx_mem_analyzer/releases。
使用手册链接为:https://gitcode.com/ancimoon/developtools_dfx_mem_analyzer/blob/master/docs/usage.md。 ArkTS 堆快照日志获取方法可参考第 4 章节。
2.1 聚类 ArkUI 泄漏组件对象及引用链
开发者可通过 mem-analyzer 命令行工具执行分析:mem-analyzer jsleak xx.rawheap -o
在处理过程中,分析工具会找到所有标记过的对象(属性 isDestroyed、_isDisposed、isDestroyed 为 true),并识别其是否为 ArkUI 组件(Component、BuilderNode、FrameNode、 RenderNode、Ability 等),然后按照对象类型进行聚类,计算大小、节点个数和引用链。之后,将信息输出为一份 html 文件,并自动打开该文件。文件示意如下:

在“ArkUI 组件/ArkTS 对象泄漏分析报告”这个 tab 中,详情列表中,按照 size,由大到小依次列出所有识别到的、可能发生泄漏的 ArkUI 组件。在表中,点击“详情”,可以查看此类型相关的所有引用链。引用链是一条从 Root 出发,经由中间对象,达到目标对象的链条,其中 A->B,说明 A 引用(持有)B。
mem-analyzer 对引用链里的特定类型进行了展开处理,提取出相关符号的业务特征信息,具体如下:
1)JSObject 对象,分析工具会读取此对象的属性总数、以及最多 10 个非系统的属性名字,以方便用户对应到真实的代码。系统公共属性,如 hclass、__proto__、ArkInternalHash、HashField 等会被直接过滤掉,不会显示。假设某个对象一共有 7 个属性,用户自定义的属性名为 id, alive, instance,则会显示为 JSObject(len=7, prop=id,alive,instance)。
2) string 对象,分析工具会截取真实值的前 20 个字符,并显示字符串长度的区间(长度<10KB, 长度>=10KB 并且<1MB,以及长度>=1MB)。例如,字符串“abc”会显示为 string(size:<10KB,content=abc)。在线上实际运行时,快照导出时会丢弃字符内容,因此会显示为 string(size:<10KB, content=)。
3) 装饰器对象,分析工具会找到此装饰器对象真实的 ArkTS 类名。例如,类名为 Foo 的对象被类装饰器(如 @Observed, @State 等)处理后,日志中引用链里名字会变成 declarative_frontend/stateMgmt.js#observedClass(line:xx),丢失业务特征,分析工具会将其还原,输出名字为 stateMgmt.js#observedClass(Foo)。
4) SourceTextModule 对象,是模块(Module)的运行时记录对象,其中包含了导出对象,堆快照中名字为 SourceTextModule,缺少业务信息。分析工具会找出此类对象的模块名,导出个数,导出名等信息,方便开发者直接找到相关的业务代码。输出结果示例为 SourceTextModule(nums=1, module=xx.ts),其中 nums 为导出条目的个数,module 为模块名。
在“引用链总览图中”这个 tab 中,对引用链进行聚合展示,可以看到关键的引用节点。由于原始的详情列表中的引用链可能会非常多,平铺展示,不利于交叉分析,因此按照应用链进行聚合。聚合后,可清楚看到不同泄漏点的相同中间节点,释放此中间节点可修复多个泄漏问题。

此分析还支持其他参数,以实现更多、更详细的分析:
2.2 手动分析 ArkUI 组件引用链
ArkTS 对象无法释放的核心原因是至少有一条从 GCRoot 到该对象的引用链,常见的 GCRoot 包含 GlobalHandleRoot、LocalHandleRoot、VMRoot、FrameRoot,参考链接。在实际场景中,从 GCRoot 到对象可能有多条不同的引用链,如果需要释放该对象,那么需要断开从 GCRoot 到该对象的所有引用链。以如下的引用链为例:
GlobalHandleRoot[4225]->com.example.memoryleakapplication/entry@1.0.0/src/main/ets/entryability/LeakAbility.ts#LeakAbility(line:45)[entry]->LexicalEnv->JSArray->ArkInternalArray->JSObject(len=7, prop=id,alive,instance)->com.example.memoryleakapplication/entry@1.0.0/src/main/ets/entryability/LeakAbility.ts#LeakAbility(line:45)[entry]此引用链中,相关的符号是已经编译过的 ts 文件。如果本地有编译过的.ts 文件,可以直接对应到代码。如果没有,则可以通过sourceMap.map进行反混淆。使用如下命令:
mem-analyzer jsleak xx.rawheap -o --source-map /path/to/sourceMaps.map直接反混淆。得到如下的引用链:
GlobalHandleRoot[4225]|->entry/src/main/ets/entryability/LeakAbility.ets#LeakAbility(line:53)[entry]|->LexicalEnv|->JSArray|->ArkInternalArray->JSObject(len=7, prop=id,alive,instance)|->entry/src/main/ets/entryability/LeakAbility.ets#LeakAbility(line:53)[entry]。得到真实的源文件 LeakAbility.ets。在此引用链中可以看到:
1)泄漏对象为 LeakAbility,其持有者为 JSObject,此 JSObject 有名为 id, alive, instance 的属性。通过该属性定位到,其类型为 AbilityLeakEntry。
2) JSObject(len=7, prop=id,alive,instance)的持有者为 JSArray,也就是有个 AbilityLeakEntry 数组。
3)数组的持有者为 LexicalEnv,也就是一个词法环境。其上游为 LeakAbility.ets#LeakAbility(line:53)[entry]。也就是说词法环境属于 LeakAbility.ets 文件。在 LeakAbility.ets 中直接搜索 AbilityLeakEntry 数组,找到对象 leakedAbilityRegistry,其在 Ability 的 onCreate()函数中存储 LeakAbility 的 this 指针。
由此还原出完整的引用链。GlobalHandleRoot 持有 LeakAbility 对象,LeakAbility 对象持有 LeakAbility.ets 模块级词法环境 LexicalEnv,此环境中有个全局变量 leakedAbilityRegistry 数组,数组中的对象又引用了 LeakAbility 对象自身,构成循环引用:instance:this->LeakAbility->LexicalEnv->leakedAbilityRegistry->entry.instance->LeakAbility。
// LeakAbility.etsinterface AbilityLeakEntry { id: number; alive: boolean; instance: object;}const leakedAbilityRegistry: AbilityLeakEntry[] = [];export default class LeakAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { … leakedAbilityRegistry.push({ id: abilityLeakId, alive: true, instance: this as object }); }}
为了不影响代码的正常功能,选择在 LeakAbility 生命周期结束(onDestroy())时,取消引用。
// LeakAbility.etsexport default class LeakAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { … } onDestroy(): void { for (let i = leakedAbilityRegistry.length - 1; i >= 0 ; i--) { leakedAbilityRegistry.splice(i, 1); } }}根据之前的引用链总览图,可以看到 LeakAbility 只有这一条从 GCRoot 出发引用链。断开后,对象没有其他链引用,也就可以正常释放。

2.3 自动分析 ArkUI 组件引用链
通过 DevEco AI + DFX Skills 可以自动化完成分析、定位与修复。步骤:
1. 下载 DFX Skills,下载链接:https://gitcode.com/openharmony-sig/developtools_dfx_skills
2. 通过 DevEco Studio 打开项目。
3. 在 DevEco Studio 右边栏找到并登录 CodeGenie。安装 arkts 代码修复 Skill,名字为 memoryleak-arkts-code-patch。

4. 将引用链给到 AI

5. 得到修复建议

3. 常见导致 ArkUI 组件泄漏的案例
3.1 事件注册/注销不对称
OpenHarmony API 中存在有很多的注册/注销监听的接口,如使用不当可能导致 ArkUI 组件泄漏。
例如,组件通过 mediaquery.matchMediaSync()注册横屏/竖屏切换监听,传入的回调(通常是箭头函数属性)绑定了组件 this。系统 mediaquery 是 GC Root 可达的,如果 aboutToDisappear 中没有调用 listener.off(‘change’),组件实例无法被 GC 回收,导致组件泄漏。示例代码如下:
❌ 修复前
struct OrientationPage { … private listener: mediaquery.MediaQueryListener | null = null; private onOrientationChange = (result: mediaquery.MediaQueryResult): void => { // 箭头函数属性,绑定this this.isLandscape = result.matches; } aboutToAppear(): void { const mediaQuery = this.getUIContext().getMediaQuery(); this.listener = mediaQuery.matchMediaSync('(orientation: landscape)'); this.isLandscape = this.listener.matches; this.listener.on('change', this.onOrientationChange); // 注册到系统mediaquery } aboutToDisappear(): void { // 未取消监听,系统监听器持续持有回调 }}✅ 修复后
aboutToDisappear(): void { this.listener?.off('change', this.onOrientationChange); // 取消监听,组件销毁释放内存}3.2 闭包捕获 this 的回调属性未置空
箭头函数没有自己的 this,它会从定义时的词法环境中捕获外层的 this。当组件将一个箭头函数赋值给外部对象的属性时,外部对象通过这个箭头函数的 LexicalEnv 持有了组件 this 的引用,导致组件泄漏。
❌ 修复前
struct Index { @Provider() player: ...; // Provider装饰器,提供数据}@ComponentV2struct Child { @Consumer() player : ...; aboutToAppear(): void { this.player.switchContent = (contId?) => { //箭头函数属性,捕获this,外部对象间接持有 this.switchContent(contId) } this.mouseHandler.onMouse = (ac) => { if (this.playController...) { ... } } } aboutToDisappear(): void { this.playController.release(); // switchContent未置空,外部对象player仍持有引用 }}✅ 修复后
aboutToDisappear(): void { this.playController.release(); this.player.switchContent = undefined; // swtichContent置空,切断引用链 this.mouseHandler. onMouse = undefined;}3.3 静态属性/AppStorage/单例持有捕获 this 的闭包
将捕获组件 this 的闭包存入 AppStorage 或类的静态属性,这些全局存储的生命周期远长于组件,组件销毁后闭包仍持有组件引用,导致组件泄漏。
❌ 修复前
class GlobalBroker { static handlers: Array<() => void> = []; // 静态属性存储 static register(handler: () => void): void { GlobalBroker.handlers.push(handler); }}@Componentstruct LeakyChild { @State count: number = 0; aboutToAppear(): void { GlobalBroker.register(() => { // 闭包捕获this this.count++; }); } aboutToDisappear(): void { }}✅ 修复后
class GlobalBroker { static handlers: Array<() => void> = []; static register(handler: () => void): () => void { GlobalBroker.handlers.push(handler); return () => { // 返回反注册函数 const idx = GlobalBroker.handlers.indexOf(handler); if (idx >= 0) { GlobalBroker.handlers.splice(idx, 1); } }; }}@Componentstruct SafeChild { private unregister: (() => void) | undefined = undefined; aboutToAppear(): void { this.unregister = GlobalBroker.register(() => { // 保存反注册函数 this.count++; }); } aboutToDisappear(): void { this.unregister?.(); // 执行反注册函数 this.unregister = undefined; }}3.4 定时器未清理
定时器的回调函数通过词法环境持有组件 this,组件销毁后,未正确清理定时器,导致组件泄漏。
❌ 修复前
struct SetTimeoutDemo { jumpIndexPage() { setTimeout(async () => { // 闭包捕获 this,并且返回值被丢弃,无法清理 RouterRule.jumpWithAction(action) }, 2000); } aboutToDisappear() { clearTimeout(); // 无入参,不会取消任何定时器 }}✅ 修复后
struct SetTimeoutDemo { private jumpTimeoutId: number = -1 jumpIndexPage() { this.jumpTimeoutId = setTimeout(async () => { // 记录定时器Id RouterRule.jumpWithAction(action) }, 2000); } aboutToDisappear() { if (this.jumpTimeoutId !== -1) { // 取消指定的定时器 clearTimeout(this.jumpTimeoutId) this.jumpTimeoutId = -1 } }}4. ArkTS 堆快照日志获取方法
在 ArkUI 组件泄漏泄漏检测中,依赖获取到堆快照日志。不需要再使用 JsLeakWatcher 相关 API,系统自动埋点,应用无需适配。
4.1 开发态获取日志方法
在开发态可通过 mem-analyzer 命令行进行堆快照抓取。PC 设备安装 hdc,设置环境变量之后,通过如下命令抓取快照:
mem-analyzer profiler -c snap com.example.app
操作示意如下
由于 mem-analyzer 本身也支持分析,可直接使用“mem-analyzer profiler -c snapshot com.example.app -o ”,-o 意味着,抓取快照后,自动分析,并打开分析结果。
4.2 运维态获取日志方法
4.2.1 通过 HiAppEvent API 订阅获取快照
开发者可通过 HiAppEvent API 订阅获取 OOM 快照,然后上传到自己的云端。快照的订阅方法可参考链接https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/hiappevent-watcher-resourceleak-events。示例代码如下:
const config: Record<string, hiAppEvent.ParamType> = { 'js_heap_logtype': 'event_rawheap', // 设置参数,通知系统,OOM时需要导出rawheap快照};hiAppEvent.setEventConfig(hiAppEvent.event.RESOURCE_OVERLIMIT, config);hiAppEvent.addWatcher({ name: 'resourceOverLimitWatcher', appEventFilters: [{ domain: hiAppEvent.domain.OS, names: [hiAppEvent.event.RESOURCE_OVERLIMIT] }], onReceive: (domain: string, appEventGroups: Array<hiAppEvent.AppEventGroup>) => { for (const eventGroup of appEventGroups) { if (eventGroup.name != hiAppEvent.event.RESOURCE_OVERLIMIT){ // 过滤,找到资源泄漏事件 continue; } for (const eventInfo of eventGroup.appEventInfos) { const params: Record<string, Object> = eventInfo.params as Record<string, Object>; if ((params[‘resource_type’] as string) === ‘js_heap’) { // 获取OOM快照地址 const logs: Array<string> = (params[‘external_log’] as Array<string>) ?? []; … } } } }});当应用收到泄漏事件为 OOM 且日志文件路径有效后,可以拷贝此文件,通过网络上传到应用自己的云端,以便进行后续分析。
4.2.2 通过 APMS 获取快照
通过 APMS 获取快照,需要几个步骤:1)接入应用灰度 API; 2)在 APMS 上下发灰度任务;3)在 APMS 上下载快照日志。
接入应用灰度 API:在 HarmonyOS 7.0 后,应用可通过应用灰度 API,自动触发线上的 OOM 快照抓取,相关日志会自动上传到 APMS 平台。灰度的接入参考链接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/hiretrieval-intro。接入完成后,上架发布应用。
下发灰度任务:登录AppGallery Connect,点击“开发与服务”,在项目列表中找到对应项目,在项目下的应用列表中点击对应应用/元服务。选择“质量 > APMS > 配置管理”,选择灰度采集,选择针对 JS_LEAK 故障发起灰度任务。系统会根据用户选择的信息,圈选目标设备,在指定的时间段内,发起 OOM 快照的灰度采集。

下载快照日志:灰度任务运行一段时间后,如果圈选的设备发生故障,则云端平台会收到快照,开发者可通过页面中的链接下载日志文件。参考如下链接https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-arkts-leak-in-operation#section1831731688

🔗 官网开发者学堂视频:https://developer.huawei.com/consumer/cn/training/result?type2List=201783644516849879&orderBy=1&courseType=5

🔗 社区 DFX 专题文章: https://developer.huawei.com/consumer/cn/forum/subject/2101218731402391001







