写点什么

使用 mem-analyzer 高效分析 ArkTS 内存泄漏

  • 2026-11-06
    北京
  • 本文字数:7391 字

    阅读完需:约 24 分钟

AI摘要

ArkTS中ArkUI组件泄漏可通过快照日志自动识别,依赖框架在DISAPPERED状态后标记isDeleting_字段,检测准确率高且无需开发者适配。

1)泄漏引用链明确,修复成本低;2)快照日志支持命令行/IDE/API多途径获取;3)修复可消除底层native内存分析干扰。

适合ArkTS开发者、内存问题排查工程师、鸿蒙平台性能优化工程师阅读。

在 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