积分中心
ArkTS 九种泄漏模式的排查指南(上)
头像 鸿蒙小助手 2026-09-20 16:13:07    发布
505 浏览 5 点赞 5 收藏

本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:

ArkTS 九种泄漏模式的排查指南(上)-华为开发者话题 | 华为开发者联盟


前言

      你有没有这样的经历?

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

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

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

      1. Observer / Callback 未移除

      2. super.aboutToDisappear() 调用缺失

      3. 配置类持有 ViewModel 闭包

      4. 双向循环强引用

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

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

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

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

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

      "谁还持有它?"

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

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

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

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

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


❌ 反例代码✅修复方案🔗引用持有链
@Componentstruct MyPage {private viewModel: MyViewModel = new MyViewModel()aboutToAppear() {// 【问题1】注册了 observer,但没有保存引用// 【问题2】闭包中的 this 词法捕获了 MyPage 实例// 【问题3】没有 aboutToDisappear 移除LifecycleOwner.addObserver({onForeground: () => {this.viewModel.onForeground()},onBackground: () => {this.viewModel.onBackground()}})}}@Componentstruct MyPage {private viewModel: MyViewModel = new MyViewModel()private lifecycleObserver: LifecycleObserver | null = nullaboutToAppear() {// 【修复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 = nullprotected loadMoreHelper: LoadMoreHelper | null = nullprotected childViewModels: BaseViewModel[] = []aboutToDisappear() {this.adapter?.release()this.adapter = nullthis.loadMoreHelper = nullthis.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 = nullaboutToDisappear() {this.searchConfig = null// 【问题】忘记调用 super.aboutToDisappear()// 后果:ParentViewModel 和 BaseViewModel 的清理全部跳过}}class ChildViewModel extends ParentViewModel {private searchConfig: SearchConfig | null = nullaboutToDisappear() {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 = nullprivate onError: ((err: Error) => void) | null = nullprivate 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 = nullthis.onError = nullthis.resultCache.clear()}}// Step 2:闭包通过 this 字段访问bindViewModel(viewModel: MyViewModel) {this.mViewModel = viewModelthis.onDataChanged = (data) => {this.mViewModel?.refreshList(data)}}// Step 3:ViewModel 销毁时通知 Config 清理class MyViewModel {private config: SearchConfig = new SearchConfig()aboutToDisappear() {this.config.clearClosures()this.config = nullsuper.aboutToDisappear()}}

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

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

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


❌反例代码✅修复方案🔗引用持有链
class WebController {private viewModel: WebViewModelinit(vm: WebViewModel) {this.viewModel = vm // Controller 持有 ViewModelvm.onBackAction = () => { this.handleBack() } // 闭包捕获 thisvm.onCloseAction = () => { this.handleClose() }}aboutToDisappear() {this.viewModel = null// 【问题】只断了 Controller → ViewModel,// ViewModel → 闭包 → Controller 这条还在!}}class WebController {private viewModel: WebViewModel | null = nullinit(vm: WebViewModel) {this.viewModel = vmvm.onBackAction = () => { this.handleBack() }vm.onCloseAction = () => { this.handleClose() }}aboutToDisappear() {// 第一步:先断开 ViewModel 持有的闭包if (this.viewModel) {this.viewModel.onBackAction = undefinedthis.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 = nullaboutToDisappear() {this.searchConfig?.unbindViewModel() // 打破环的一端this.searchConfig = nullsuper.aboutToDisappear()}}class SearchConfig {unbindViewModel() {this.filterViewModel = null // 另一端也置空}}

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

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

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


❌反例代码✅修复方案🔗引用持有链
@Componentstruct MyNavPage {private viewModel: NavViewModel = new NavViewModel()// 【问题】箭头函数字段,this 被永久捕获onBackPressed = () => {this.viewModel.handleBack()router.back()}aboutToAppear() {const param = RouterMgr.getCurrentParam() as MyNavPageParamparam.navParam.onBackPressed = this.onBackPressed}// 【问题】aboutToDisappear 中没有清空路由参数中的闭包}// 方案一:在 aboutToDisappear 中主动清空闭包aboutToDisappear() {const param = RouterMgr.getCurrentParam() as MyNavPageParamif (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?ha_source=zxqy-IT&ha_sourceId=89000468

🔗 社区DFX专题文章: https://developer.huawei.com/consumer/cn/forum/subject/2101218731402391001?ha_source=zxqy-IT&ha_sourceId=89000468

【扫码加入 HarmonyOS DFX 技术交流群】

©本站发布的所有内容,包括但不限于文字、图片、音频、视频、图表、标志、标识、广告、商标、商号、域名、软件、程序等,除特别标明外,均来源于网络或用户投稿,版权归原作者或原出处所有。我们致力于保护原作者版权,若涉及版权问题,请及时联系我们进行处理。
分类
HarmonyOS
地址:北京市朝阳区北三环东路三元桥曙光西里甲1号第三置业A座1508室 电话:13391790444或(010)62178877
版权所有:电脑商情信息服务集团 北京赢邦策略咨询有限责任公司
声明:本媒体部分图片、文章来源于网络,版权归原作者所有,我司致力于保护作者版权,如有侵权,请与我司联系删除

京ICP备:2022009079号-2

京公网安备:11010502051901号

ICP证:京B2-20230255