使用mem-analyzer高效分析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中,对引用链进行聚合展示,可以看到关键的引用节点。由于原始的详情列表中的引用链可能会非常多,平铺展示,不利于交叉分析,因此按照应用链进行聚合。聚合后,可清楚看到不同泄漏点的相同中间节点,释放此中间节点可修复多个泄漏问题。

此分析还支持其他参数,以实现更多、更详细的分析:
| jsleak常用命令行选项相 | 说明 |
| –sourcemap PATH | 指定工程 sourceMap 文件或目录(含 sourceMaps.map),用于将混淆的编译产物路径还原为源码路径 |
| –namecache PATH | 指定工程 nameCache 文件或目录(含 nameCache.json),用于将混淆的函数/属性名还原为原始名 |
| -o, –auto-open | 分析完成后自动打开结果文件 |
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.ets
interface 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.ets
export 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装饰器,提供数据
}
@ComponentV2
struct 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);
}
}
@Component
struct 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);
}
};
}
}
@Component
struct 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


【扫码加入 HarmonyOS DFX 技术交流群】
本文由 @华为开发者联盟 授权发布于人人都是产品经理。未经作者许可,禁止转载
题图来自Unsplash,基于CC0协议
该文观点仅代表作者本人,人人都是产品经理平台仅提供信息存储空间服务
- 目前还没评论,等你发挥!

起点课堂会员权益



