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

本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:
ArkTS 九种泄漏模式的排查指南(上)-华为开发者话题 | 华为开发者联盟
前言
你有没有这样的经历?
辛辛苦苦写好的鸿蒙应用,在测试环境跑得好好的,一上现网就开始”抽风”——搜索页面偶尔闪退、联系人列表越用越卡、签到功能用久了直接崩溃。你打开日志一看,满屏的 raw stack,文件名和行号倒是有了,可到底是谁在背后搞鬼?一头雾水。
别急,你不是一个人。我们团队在一次现网闪退排查中,对着源码一行行啃,硬是从一堆”莫名其妙”的崩溃里,归纳出了 ArkTS 内存泄漏的九种典型模式。今天,咱们就用大白话+代码,把这九种模式掰开揉碎讲清楚。
本文(上) 带你认识前五种泄漏模式:
1. Observer / Callback 未移除
2. super.aboutToDisappear() 调用缺失
3. 配置类持有 ViewModel 闭包
4. 双向循环强引用
5. 路由参数持有页面闭包
一、先搞懂一个概念:内存泄漏到底在”漏”什么?
内存泄漏的本质,其实就一句话:一个本该”退休”的对象,被一个”长命”的对象死死拽着,死活不让它走。
想象一下,你租了一间房子(短生命周期对象),租期到了该搬走,结果房东(长生命周期对象)把你的钥匙藏了起来,还跟物业说”这人还没搬呢”。于是你的东西一直堆在屋里,房间永远腾不出来——这就是内存泄漏。
排查时,你只需要反复问自己一个问题:
“谁还持有它?”
沿着这条引用链一路往上追,找到那个”长命”的持有方,然后在合适的时机把引用切断依次搞定。
二、模式一:Observer / Callback 未移除——”注册了却忘了退群”
鸿蒙的生命周期框架靠”观察者列表”来感知页面状态变化。你调用 addObserver 注册了一个观察者,相当于加了一个群。可问题是——页面销毁时,你有没有退群?
如果没有对称地调用 removeObserver,框架就会通过观察者列表一直持有你的页面实例。页面虽然已经从视图树上消失了,但在框架眼里它”还活着”,GC 自然不敢回收。
🔍 问题根因: addObserver 注册后,页面销毁时没有对称地调用 removeObserver,框架通过观察者列表持续持有页面实例。
| ❌ 反例代码 | ✅ 修复方案 | 🔗 引用持有链 |
| @Component struct MyPage { private viewModel: MyViewModel = new MyViewModel() aboutToAppear() { |
@Component struct MyPage { private viewModel: MyViewModel = new MyViewModel() private lifecycleObserver: LifecycleObserver | null = null aboutToAppear() { aboutToDisappear() { |
说明:红色节点是”长生命周期”持有方(全局单例)和被泄漏对象(页面),橙色是泄漏的关键链路(闭包)。 |
🔀 同类变体:子组件销毁未透传
当父组件持有多个子 Tab 时,只有”当前激活的 Tab”的 aboutToDisappear() 会被系统调用。那些没被激活的 Tab,就像被遗忘在角落的租客——它们的回调永远不会被移除。
| ❌ 反例代码 | ✅ 修复方案 | 🔗 引用持有链 |
| // 修复:父容器的 aboutToDisappear 中,遍历所有子组件主动通知销毁 aboutToDisappear() { this.tabItems.forEach(item => item.destroy()) } |
![]() |
三、模式二:super.aboutToDisappear() 调用缺失——”断链的多米诺骨牌”
ArkTS 中子类重写 aboutToDisappear 时,编译器并不会强制你调用 super.aboutToDisappear()。这就像接力赛——你跑完了自己那一棒,却忘了把接力棒交给下一位选手。
一旦漏调,继承链上所有基类积累的清理逻辑都会静默跳过。adapter、loadMoreHelper、childViewModels……这些资源就像多米诺骨牌一样,从你漏调的那一环开始,全部倒下。
🔍 问题根因: 子类重写 aboutToDisappear 时忘记调用 super,继承链上所有基类的清理逻辑全部跳过。
| ❌ 反例代码 | ✅ 修复方案 | 🔗 引用持有链 |
| class BaseViewModel { protected adapter: ListAdapter | null = null protected loadMoreHelper: LoadMoreHelper | null = null protected childViewModels: BaseViewModel[] = [] aboutToDisappear() { class ParentViewModel extends BaseViewModel { aboutToDisappear() { class ChildViewModel extends ParentViewModel { aboutToDisappear() { |
class ChildViewModel extends ParentViewModel { private searchConfig: SearchConfig | null = null aboutToDisappear() { |
![]() |
💡 记住一个原则: super.aboutToDisappear() 永远放在 aboutToDisappear 的最后——子类先清理自己,再触发基类清理链。
四、模式三:配置/缓存类持有 ViewModel 闭包——”被遗忘的抽屉”
业务中经常有这样的场景:把 ViewModel 的方法作为回调注入到 Config/Helper 对象中。Config 对象把这些闭包像文件一样塞进抽屉里缓存起来,可问题是——页面退出时,有没有人去翻这个抽屉?
如果没有提供”清理”入口,Config 侧向 ViewModel 侧的引用就永远断不开。页面虽然销毁了,但 Config 抽屉里的闭包还紧紧抓着 ViewModel 不放。
🔍 问题根因: Config 对象缓存了 ViewModel 的闭包,但页面退出时没有清理入口,引用无法断开。
| ❌ 反例代码 | ✅ 修复方案 | 🔗 引用持有链 |
| class SearchConfig { private onDataChanged: ((data: Result[]) => void) | null = null private onError: ((err: Error) => void) | null = null private resultCache: Map<string, Result[]> = new Map() bindViewModel(viewModel: MyViewModel) { class MyViewModel { aboutToDisappear() { |
// Step 1:Config 提供 clearClosures() 方法 class SearchConfig { clearClosures() { this.onDataChanged = null this.onError = null this.resultCache.clear() } } // Step 2:闭包通过 this 字段访问 // Step 3:ViewModel 销毁时通知 Config 清理 aboutToDisappear() { } } |
![]() |
五、模式四:双向循环强引用——”互相牵手的两个人”
两个对象互相持有对方的强引用,就像两个人手拉手转圈。JavaScript/ArkTS 的 GC 确实能回收循环引用——但前提是这整个圈子”没人认识”。一旦圈子中任意一端被外部强引用锚定(比如 @State、@Observed、全局变量),整个环就长期存活了。
🔍 问题根因: 两个对象互相持有强引用形成闭环,且环中至少一端被外部锚定,GC 无法回收。
| ❌ 反例代码 | ✅ 修复方案 | 🔗 引用持有链 |
| class WebController { private viewModel: WebViewModel init(vm: WebViewModel) { aboutToDisappear() { |
class WebController { private viewModel: WebViewModel | null = null init(vm: WebViewModel) { aboutToDisappear() { |
引用持有链:Controller → viewModel → onBackAction 闭包 → this(Controller),形成闭环 |
🔀 同类变体:ViewModel ↔ Config 的双向持有
ViewModel 持有 Config,Config 又持有 ViewModel 的闭包——同样是双向循环。修复思路一样:在任意一方销毁时,置空对方引用即可打破环。
| ❌ 反例代码 | ✅ 修复方案 | 🔗 引用持有链 |
| class FilterViewModel { private searchConfig: SearchConfig | null = null aboutToDisappear() { class SearchConfig { |
![]() |
六、模式五:路由参数持有页面闭包——”被框架扣为人质”
路由框架在页面退栈后,并不会立刻释放路由参数对象(PageParam),而是延迟释放。如果你的 PageParam 里塞了页面的箭头函数字段,而该函数词法捕获了页面的 this——恭喜你,页面被路由框架间接”扣为人质”了。
🔍 问题根因: 路由框架延迟释放 PageParam,PageParam 中的箭头函数词法捕获了页面的 this,页面被间接延迟持有。
| ❌ 反例代码 | ✅ 修复方案 | 🔗 引用持有链 |
| @Component struct MyNavPage { private viewModel: NavViewModel = new NavViewModel() // 【问题】箭头函数字段,this 被永久捕获 aboutToAppear() { |
// 方案一:在 aboutToDisappear 中主动清空闭包 aboutToDisappear() { const param = RouterMgr.getCurrentParam() as MyNavPageParam if (param?.navParam) { param.navParam.onBackPressed = undefined } this.viewModel.destroy() } // 方案二:改用事件总线,不直接持有页面实例 aboutToDisappear() { |
![]() |
七、下篇预告
上篇我们聊了五种泄漏模式,下篇将继续揭晓剩下的四种”内存刺客”:
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 技术交流群】
本文由 @华为开发者联盟 授权发布于人人都是产品经理。未经作者许可,禁止转载
题图来自Unsplash,基于CC0协议
该文观点仅代表作者本人,人人都是产品经理平台仅提供信息存储空间服务
- 目前还没评论,等你发挥!

起点课堂会员权益









