写点什么

ArkTS 九种泄漏模式的排查指南(上)

  • 2026-09-20
    北京
  • 本文字数:2217 字

    阅读完需:约 7 分钟

AI摘要

ArkTS内存泄漏的九种典型模式中,前五种被系统性归纳并配以代码示例说明,聚焦于Observer未移除、生命周期钩子缺失、配置类闭包持有、双向强引用及路由参数闭包等高频问题。

  1. Observer或Callback未显式移除将导致页面实例无法释放
  2. 缺失super.aboutToDisappear()调用会阻断组件销毁链
  3. 配置类意外捕获ViewModel形成隐式强引用

适合鸿蒙ArkTS应用开发者、内存问题排查工程师、前端架构师阅读

本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:ArkTS 九种泄漏模式的排查指南(上)-华为开发者话题 | 华为开发者联盟

前言

你有没有这样的经历?

辛辛苦苦写好的鸿蒙应用,在测试环境跑得好好的,一上现网就开始"抽风"——搜索页面偶尔闪退、联系人列表越用越卡、签到功能用久了直接崩溃。你打开日志一看,满屏的 raw stack,文件名和行号倒是有了,可到底是谁在背后搞鬼?一头雾水。

别急,你不是一个人。我们团队在一次现网闪退排查中,对着源码一行行啃,硬是从一堆"莫名其妙"的崩溃里,归纳出了 ArkTS 内存泄漏的九种典型模式。今天,咱们就用大白话+代码,把这九种模式掰开揉碎讲清楚。

本文(上)带你认识前五种泄漏模式:

1. Observer / Callback 未移除

2. super.aboutToDisappear() 调用缺失

3. 配置类持有 ViewModel 闭包

4. 双向循环强引用

5. 路由参数持有页面闭包

一、先搞懂一个概念:内存泄漏到底在"漏"什么?

内存泄漏的本质,其实就一句话:一个本该"退休"的对象,被一个"长命"的对象死死拽着,死活不让它走。

想象一下,你租了一间房子(短生命周期对象),租期到了该搬走,结果房东(长生命周期对象)把你的钥匙藏了起来,还跟物业说"这人还没搬呢"。于是你的东西一直堆在屋里,房间永远腾不出来——这就是内存泄漏。

排查时,你只需要反复问自己一个问题:

"谁还持有它?"

沿着这条引用链一路往上追,找到那个"长命"的持有方,然后在合适的时机把引用切断依次搞定。

二、模式一:Observer / Callback 未移除——"注册了却忘了退群"

鸿蒙的生命周期框架靠"观察者列表"来感知页面状态变化。你调用 addObserver 注册了一个观察者,相当于加了一个群。可问题是——页面销毁时,你有没有退群?

如果没有对称地调用 removeObserver,框架就会通过观察者列表一直持有你的页面实例。页面虽然已经从视图树上消失了,但在框架眼里它"还活着",GC 自然不敢回收。

🔍 问题根因: addObserver 注册后,页面销毁时没有对称地调用 removeObserver,框架通过观察者列表持续持有页面实例。

🔀 同类变体:子组件销毁未透传

当父组件持有多个子 Tab 时,只有"当前激活的 Tab"的 aboutToDisappear() 会被系统调用。那些没被激活的 Tab,就像被遗忘在角落的租客——它们的回调永远不会被移除。

三、模式二:super.aboutToDisappear() 调用缺失——"断链的多米诺骨牌"

ArkTS 中子类重写 aboutToDisappear 时,编译器并不会强制你调用 super.aboutToDisappear()。这就像接力赛——你跑完了自己那一棒,却忘了把接力棒交给下一位选手。

一旦漏调,继承链上所有基类积累的清理逻辑都会静默跳过。

adapter、loadMoreHelper、childViewModels……这些资源就像多米诺骨牌一样,从你漏调的那一环开始,全部倒下。

🔍 问题根因: 子类重写 aboutToDisappear 时忘记调用 super,继承链上所有基类的清理逻辑全部跳过。

💡 记住一个原则: super.aboutToDisappear() 永远放在 aboutToDisappear 的最后——子类先清理自己,再触发基类清理链。

四、模式三:配置/缓存类持有 ViewModel 闭包——"被遗忘的抽屉"

业务中经常有这样的场景:把 ViewModel 的方法作为回调注入到 Config/Helper 对象中。Config 对象把这些闭包像文件一样塞进抽屉里缓存起来,可问题是——页面退出时,有没有人去翻这个抽屉?

如果没有提供"清理"入口,Config 侧向 ViewModel 侧的引用就永远断不开。页面虽然销毁了,但 Config 抽屉里的闭包还紧紧抓着 ViewModel 不放。

🔍 问题根因: Config 对象缓存了 ViewModel 的闭包,但页面退出时没有清理入口,引用无法断开。

五、模式四:双向循环强引用——"互相牵手的两个人"

两个对象互相持有对方的强引用,就像两个人手拉手转圈。

JavaScript/ArkTS 的 GC 确实能回收循环引用——但前提是这整个圈子"没人认识"。一旦圈子中任意一端被外部强引用锚定(比如 @State、@Observed、全局变量),整个环就长期存活了。

🔍 问题根因: 两个对象互相持有强引用形成闭环,且环中至少一端被外部锚定,GC 无法回收。

🔀 同类变体:ViewModel ↔ Config 的双向持有

ViewModel 持有 Config,Config 又持有 ViewModel 的闭包——同样是双向循环。修复思路一样:在任意一方销毁时,置空对方引用即可打破环。

六、模式五:路由参数持有页面闭包——"被框架扣为人质"

路由框架在页面退栈后,并不会立刻释放路由参数对象(PageParam),而是延迟释放。如果你的 PageParam 里塞了页面的箭头函数字段,而该函数词法捕获了页面的 this——恭喜你,页面被路由框架间接"扣为人质"了。

🔍 问题根因: 路由框架延迟释放 PageParam,PageParam 中的箭头函数词法捕获了页面的 this,页面被间接延迟持有。

七、下篇预告

上篇我们聊了五种泄漏模式,下篇将继续揭晓剩下的四种"内存刺客":

6. 全局静态集合残留——"进程维度的记忆宫殿"

7. 箭头函数跨组件持有 this——"父组件的影子"

8. 原生/框架资源未 dispose——"跨平台的幽灵"

9. NAPI 跨层闭包泄漏——"C++ 端的定时炸弹"

最后还会附上五步排查方法论总结,帮你建立一套系统的内存泄漏排查思路。敬请期待!

—— 未完待续,下篇更精彩 ——


🔗 官网开发者学堂视频: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 技术交流群】