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

0 评论 768 浏览 0 收藏 19 分钟

你有没有这样的经历?辛辛苦苦写好的鸿蒙应用,在测试环境跑得好好的,一上现网就开始"抽风"——搜索页面偶尔闪退、联系人列表越用越卡、签到功能用久了直接崩溃。你打开日志一看,满屏的 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() {
// 【问题1】注册了 observer,但没有保存引用
// 【问题2】闭包中的 this 词法捕获了 MyPage 实例
// 【问题3】没有 aboutToDisappear 移除
LifecycleOwner.addObserver({
onForeground: () => {
this.viewModel.onForeground()
},
onBackground: () => {
this.viewModel.onBackground()
}
})
}
}

@Component
struct MyPage {
private viewModel: MyViewModel = new MyViewModel()
private lifecycleObserver: LifecycleObserver | null = null

aboutToAppear() {
// 【修复1】保存 observer 引用
this.lifecycleObserver = {
onForeground: () => { this.viewModel.onForeground() },
onBackground: () => { this.viewModel.onBackground() }
}
LifecycleOwner.addObserver(this.lifecycleObserver)
}

aboutToDisappear() {
// 【修复2】对称移除,彻底切断引用链
if (this.lifecycleObserver) {
LifecycleOwner.removeObserver(this.lifecycleObserver)
this.lifecycleObserver = null
}
this.viewModel.destroy()
}
}

说明:红色节点是”长生命周期”持有方(全局单例)和被泄漏对象(页面),橙色是泄漏的关键链路(闭包)。

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

       当父组件持有多个子 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() {
this.adapter?.release()
this.adapter = null
this.loadMoreHelper = null
this.childViewModels.forEach(vm => vm.aboutToDisappear())
this.childViewModels = []
}
}

class ParentViewModel extends BaseViewModel {
protected itemCache: Map<string, Item> = new Map()

aboutToDisappear() {
this.itemCache.clear()
super.aboutToDisappear() // ✅ ParentViewModel 正确调用了 super
}
}

class ChildViewModel extends ParentViewModel {
private searchConfig: SearchConfig | null = null

aboutToDisappear() {
this.searchConfig = null
// 【问题】忘记调用 super.aboutToDisappear()
// 后果:ParentViewModel 和 BaseViewModel 的清理全部跳过
}
}

class ChildViewModel extends ParentViewModel {
private searchConfig: SearchConfig | null = null

aboutToDisappear() {
this.searchConfig = null
// 【修复】放在最后,确保子类先清理自己,再触发基类清理链
super.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) {
this.onDataChanged = (data) => {
viewModel.refreshList(data) // 闭包捕获 viewModel
}
this.onError = (err) => {
viewModel.showError(err) // 闭包捕获 viewModel
}
}
// 没有 clearClosures / destroy 方法
}

class MyViewModel {
private config: SearchConfig = new SearchConfig()

aboutToDisappear() {
// 【问题】没有通知 config 清理
}
}

// Step 1:Config 提供 clearClosures() 方法
class SearchConfig {
clearClosures() {
this.onDataChanged = null
this.onError = null
this.resultCache.clear()
}
}

// Step 2:闭包通过 this 字段访问
bindViewModel(viewModel: MyViewModel) {
this.mViewModel = viewModel
this.onDataChanged = (data) => {
this.mViewModel?.refreshList(data)
}
}

// Step 3:ViewModel 销毁时通知 Config 清理
class MyViewModel {
private config: SearchConfig = new SearchConfig()

aboutToDisappear() {
this.config.clearClosures()
this.config = null
super.aboutToDisappear()

}

}

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

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

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

❌ 反例代码 ✅ 修复方案 🔗 引用持有链
class WebController {
private viewModel: WebViewModel

init(vm: WebViewModel) {
this.viewModel = vm // Controller 持有 ViewModel
vm.onBackAction = () => { this.handleBack() } // 闭包捕获 this
vm.onCloseAction = () => { this.handleClose() }
}

aboutToDisappear() {
this.viewModel = null
// 【问题】只断了 Controller → ViewModel,
// ViewModel → 闭包 → Controller 这条还在!
}
}

class WebController {
private viewModel: WebViewModel | null = null

init(vm: WebViewModel) {
this.viewModel = vm
vm.onBackAction = () => { this.handleBack() }
vm.onCloseAction = () => { this.handleClose() }
}

aboutToDisappear() {
// 第一步:先断开 ViewModel 持有的闭包
if (this.viewModel) {
this.viewModel.onBackAction = undefined
this.viewModel.onCloseAction = undefined
}
// 第二步:再置空 Controller 对 ViewModel 的引用
this.viewModel = null
}
}

引用持有链:Controller → viewModel → onBackAction 闭包 → this(Controller),形成闭环

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

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

❌ 反例代码 ✅ 修复方案 🔗 引用持有链
  class FilterViewModel {
private searchConfig: SearchConfig | null = null

aboutToDisappear() {
this.searchConfig?.unbindViewModel() // 打破环的一端
this.searchConfig = null
super.aboutToDisappear()
}
}

class SearchConfig {
unbindViewModel() {
this.filterViewModel = null // 另一端也置空
}
}

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

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

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

❌ 反例代码 ✅ 修复方案 🔗 引用持有链
@Component
struct MyNavPage {
private viewModel: NavViewModel = new NavViewModel()

// 【问题】箭头函数字段,this 被永久捕获
onBackPressed = () => {
this.viewModel.handleBack()
router.back()
}

aboutToAppear() {
const param = RouterMgr.getCurrentParam() as MyNavPageParam
param.navParam.onBackPressed = this.onBackPressed
}
// 【问题】aboutToDisappear 中没有清空路由参数中的闭包
}

// 方案一:在 aboutToDisappear 中主动清空闭包
aboutToDisappear() {
const param = RouterMgr.getCurrentParam() as MyNavPageParam
if (param?.navParam) {
param.navParam.onBackPressed = undefined
}
this.viewModel.destroy()
}

// 方案二:改用事件总线,不直接持有页面实例
aboutToAppear() {
EventBus.on(‘back’, ‘MyNavPage’, () => {
this.viewModel.handleBack()
})
}

aboutToDisappear() {
EventBus.off(‘back’, ‘MyNavPage’)
}

七、下篇预告

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

       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协议

该文观点仅代表作者本人,人人都是产品经理平台仅提供信息存储空间服务

更多精彩内容,请关注人人都是产品经理微信公众号或下载App
海报
评论
评论请登录
  1. 目前还没评论,等你发挥!