前言
在 HarmonyOS 应用开发中,资源泄漏是导致应用崩溃和耗电量飙升的头号元凶。游戏应用中常见的问题——退出页面后游戏音乐还在响、定时器仍然在跑、网络请求回调仍被持有——根源都是没有在组件销毁时正确释放资源。
本文以开源项目「猫猫大作战」(一款猫咪合并消除游戏)为锚点,深入解析 ArkUI 页面生命周期中的aboutToDisappear回调。你将看到项目源码中如何通过aboutToDisappear+clearTimers()彻底解决定时器泄漏问题,并掌握组件销毁的最佳实践。
提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1–64 篇。本篇是阶段二第 65 篇,也是页面生命周期专题的第一篇。
一、场景拆解:游戏页面的“退出后遗症“
1.1 问题描述
在「猫猫大作战」中,游戏运行时启动了三个独立定时器:
| 定时器 | 作用 | 间隔 | 位置 |
|---|---|---|---|
gameLoopTimer | 游戏主循环(物理更新) | 100ms | index.ets Row 37 |
spawnTimer | 猫咪自动生成 | 2s | index.ets Row 49 |
timeTimer | 游戏计时 | 1s | index.ets Row 59 |
当用户直接退出游戏页面(例如返回主页或关闭应用),如果这些定时器没有被清理,会发生:
- 定时器回调继续执行,尝试更新已销毁的 UI 组件 →崩溃
- 应用进程无法被 GC 回收 →内存泄漏
- 后台空转耗电 →电量消耗
// 🚫 错误示范:退出页面后定时器仍然在跑 private gameLoopTimer: number = setInterval(() => { // 即使页面已销毁,这里还在执行! this.cats = this.gameEngine.updateCats(); this.score = this.gameEngine.getScore(); }, 100);1.2 解决方案
ArkUI 为每个自定义组件提供了生命周期回调函数,其中aboutToDisappear正是解决资源清理问题的标准入口。「猫猫大作战」的Index.ets中通过极简的两行代码解决了整个问题:
// ✅ 正确做法:在 aboutToDisappear 中清理资源 aboutToDisappear() { this.clearTimers(); }二、ArkUI 页面生命周期体系总览
在深入aboutToDisappear之前,先理解 ArkUI 自定义组件的完整生命周期。
2.1 生命周期全景图
ArkUI 自定义组件的生命周期按顺序分为以下阶段:
| 阶段 | 回调 | 触发时机 | 能否修改状态 |
|---|---|---|---|
| ① 创建 | constructor | 组件实例化 | ✅ 可初始化 |
| ② 即将出现 | aboutToAppear | build() 执行前 | ✅ |
| ③ 首次渲染 | build() | 组件挂载到 UI 树 | ✅ |
| ④ 构建完成 | onDidBuild | build() 执行完毕后 | ✅ 但不能在 build 中改 |
| ⑤ 重新渲染 | build()(再次) | 状态变量改变时 | ✅ |
| ⑥ 即将销毁 | aboutToDisappear | 组件从 UI 树移除前 | ❌禁止修改 |
| ⑦ 销毁 | 析构 | 组件内存释放 | — |
2.2 组件创建与销毁的完整时序
@Entry @Component struct Index { aboutToAppear() { console.info('① 组件即将出现 — 初始化数据'); } onDidBuild() { console.info('② 首次渲染完成 — 适合埋点上报'); } aboutToDisappear() { console.info('③ 组件即将销毁 — 清理资源'); } build() { Column() { Text('生命周期演示') } } }重要:
aboutToDisappear逆操作是aboutToAppear,两者成对出现,分别对应组件的“生“与“死“。
2.3 嵌套组件的生命周期顺序
当页面包含子组件时,生命周期调用顺序遵循先父后子创建,先子后父销毁的规则:
// 创建顺序(冷启动) Parent aboutToAppear → Parent build → Parent onDidBuild → Child aboutToAppear → Child build → Child onDidBuild // 销毁顺序(页面退出) Parent aboutToDisappear → Child aboutToDisappear → Child 析构 → Parent 析构三、aboutToDisappear 深度解析
3.1 触发时机
aboutToDisappear在以下两种场景下触发:
- 组件被条件移除:
if/else分支切换、ForEach/LazyForEach列表元素被删除 - 页面/组件销毁:
router.back()返回、Navigation出栈、应用退出
// 场景一:if 条件移除 → 触发 Child 的 aboutToDisappear @State showChild: boolean = true; build() { Column() { if (this.showChild) { ChildComponent() // 当 showChild 变为 false 时,ChildComponent 被销毁 } Button('删除子组件').onClick(() => { this.showChild = false; // 触发 ChildComponent.aboutToDisappear() }) } }3.2 使用约束(必须遵守)
| 约束 | 说明 | 后果 |
|---|---|---|
| ❌禁止修改状态变量 | 不能对 @State / @Link / @Prop 赋值 | 应用行为不稳定 |
| ❌禁止 async/await | 不能使用异步操作 | 阻止 GC,永久内存泄漏 |
| ✅只能做同步清理 | clearInterval、close 连接、释放 listener | 安全 |
// 🚫 错误:在 aboutToDisappear 中使用 async/await aboutToDisappear() { await this.saveData(); // ❌ 组件保留在 Promise 闭包中,无法 GC! this.clearTimers(); } // ✅ 正确:纯同步清理 aboutToDisappear() { this.clearTimers(); // ✅ 同步操作,组件立即可以被 GC }3.3 与 onPageShow/onPageHide 的区别
| 回调 | 作用域 | 触发场景 | 对应关系 |
|---|---|---|---|
aboutToAppear | 组件级 | 组件首次创建 | 与aboutToDisappear成对 |
aboutToDisappear | 组件级 | 组件最终销毁 | 与aboutToAppear成对 |
onPageShow | 页面级 | 页面每次显示 | 与onPageHide成对 |
onPageHide | 页面级 | 页面每次隐藏 | 与onPageShow成对 |
关键区别:页面跳转到下一页时触发
onPageHide,但不触发aboutToDisappear(页面未被销毁),此时定时器仍在运行。只有页面真正被销毁(如返回退出)时aboutToDisappear才被触发。
四、项目实战:猫猫大作战的 aboutToDisappear 实现
4.1 源码定位
打开「猫猫大作战」entry/src/main/ets/pages/Index.ets,在第 124-126 行可以看到完整的实现:
aboutToDisappear() { this.clearTimers(); }配合clearTimers()方法的实现(第 102-115 行):
// 清除所有定时器 — 一行代码解决泄漏隐患 clearTimers() { if (this.gameLoopTimer !== -1) { clearInterval(this.gameLoopTimer); this.gameLoopTimer = -1; } if (this.spawnTimer !== -1) { clearInterval(this.spawnTimer); this.spawnTimer = -1; } if (this.timeTimer !== -1) { clearInterval(this.timeTimer); this.timeTimer = -1; } }4.2 清除机制详解
clearTimers()的设计遵循三个原则:
- 幂等性:无论调用多少次,都不会产生副作用
- 标记清除:每次
clearInterval后立即将 timerId 重置为-1 - 统一入口:
startGame()、endGame()、aboutToDisappear()统一调用
// clearTimers 的三种调用场景 startGame() { this.clearTimers(); // ① 重新开始前清理旧的 this.gameEngine.reset(); // ... 启动新定时器 } endGame() { this.clearTimers(); // ② 游戏结束时清理 // ... 记录高分 } aboutToDisappear() { this.clearTimers(); // ③ 页面销毁时兜底清理 }4.3 完整调用链路
用户操作 → [返回/退出] → ArkUI 框架触发 aboutToDisappear() → Index.clearTimers() → clearInterval(gameLoopTimer) // 停止主循环 → clearInterval(spawnTimer) // 停止生成 → clearInterval(timeTimer) // 停止计时 → Index 组件从 UI 树摘除 → 子组件(GameOverOverlay 等)aboutToDisappear → 所有组件内存释放五、aboutToAppear vs aboutToDisappear 对比分析
5.1 成对设计模式
「猫猫大作战」中这两个回调的职责非常清晰:
| 维度 | aboutToAppear | aboutToDisappear |
|---|---|---|
| 时机 | build() 之前 | 析构之前 |
| 职责 | 获取资源 | 释放资源 |
| 操作 | JSON.parse 高分、读取配置 | clearInterval 定时器 |
| 是否可改状态 | ✅ 可以 | ❌ 不可以 |
| 耗时限制 | 建议 < 50ms | 建议 < 10ms |
| 异步 | ✅ 可以(但没必要) | ❌ 绝对禁止 |
5.2 项目中的应用
在 Index.ets 中,aboutToAppear负责初始化数据(但因为使用了 Preference 异步存储,实际数据获取延迟到了startGame中),而aboutToDisappear负责清理定时器。对于纯同步的初始化场景,典型的成对写法是:
@Entry @Component struct GamePage { @State playerName: string = ''; aboutToAppear() { // 创建阶段 — 获取资源 const saved = AppStorage.get<string>('playerName'); if (saved) { this.playerName = saved; // ✅ aboutToAppear 中可以修改状态 } } aboutToDisappear() { // 销毁阶段 — 释放资源 // this.playerName = ''; // ❌ 禁止修改状态! releaseAudio(); // ✅ 只做释放操作 } }六、子组件生命周期嵌套实战
6.1 @Reusable 子组件的生命周期
当使用@Reusable装饰器时,组件被回收后不会立即销毁,而是进入复用池,因此aboutToDisappear的触发时机不同:
@Reusable @Component struct CatItem { @State level: number = 0; aboutToAppear() { console.info('CatItem 进入视图'); } aboutToDisappear() { console.info('CatItem 离开视图 — 清理资源'); // 如果 CatItem 持有图片资源或动画实例,在此释放 } build() { Text('🐱' + this.level) } }6.2 if/else 条件组件的销毁
@Entry @Component struct GameContainer { @State isPlaying: boolean = false; aboutToDisappear() { // 页面的生命周期 — 整个页面退出 console.info('GameContainer 销毁'); } build() { if (this.isPlaying) { GameBoard(); // 当 isPlaying 变为 false 时触发 GameBoard.aboutToDisappear() } else { MainMenu(); // 当 isPlaying 变为 true 时 MainMenu 被销毁 } } }组件销毁时序表:
| 操作 | 触发顺序 |
|---|---|
isPlaying: false → true | GameBoard.aboutToDisappear()→ GameBoard 析构 → MainMenu.aboutToAppear() → MainMenu 渲染 |
| 页面退出 | GameContainer.aboutToDisappear()→ 子组件逐个 aboutToDisappear → 全部析构 |
七、常见踩坑与最佳实践
7.1 坑一:async/await 导致内存泄漏
// 🚫 错误:aboutToDisappear 中使用 async aboutToDisappear() { this.saveHighScore(); // 假设 saveHighScore 返回 Promise // ↑ 组件被 Promise 的闭包持有,无法 GC }解决方案:所有清理操作必须是同步的。如需异步存储,在onPageHide中处理:
onPageHide() { // 页面隐藏但不是销毁,适合在这里做异步存储 this.saveHighScoreAsync(); } aboutToDisappear() { // 页面销毁,只做同步清理 this.clearTimers(); }7.2 坑二:修改 @Link 变量
@Component struct ChildComp { @Link score: number; aboutToDisappear() { // 🚫 错误:aboutToDisappear 中不能修改 @Link this.score = 0; } }原因:aboutToDisappear中修改状态可能导致父组件在销毁过程中收到意外的变更通知,引发不可预知的 UI 行为和崩溃。
7.3 坑三:忘记清理 EventListener
定时器不是唯一需要清理的资源,以下场景同样需要:
aboutToAppear() { // 注册事件监听 this.eventListener = (data: string) => { this.handleEvent(data); }; EventBus.on('gameEvent', this.eventListener); } aboutToDisappear() { // ❌ 漏掉这行 → EventBus 持有组件引用,内存泄漏 EventBus.off('gameEvent', this.eventListener); this.clearTimers(); }7.4 最佳实践清单
- 所有setInterval/setTimeout在
aboutToDisappear中统一清理 - EventBus / emitter 的注册/注销成对出现
- 网络请求(http.Request)在页面退出时主动 abort
- AVPlayer / AudioRenderer 在销毁时release
- 文件流 / 数据库连接在销毁时close
- 绝对不用 async/await
- 绝对不修改状态变量
八、HiLog 日志埋点验证生命周期调用
8.1 引入 HiLog
使用系统日志工具@kit.PerformanceAnalysisKit验证生命周期各节点的调用:
import { hilog } from '@kit.PerformanceAnalysisKit'; const TAG = 'IndexLifecycle'; const DOMAIN = 0xFF00; @Entry @Component struct Index { aboutToAppear() { hilog.info(DOMAIN, TAG, 'Index aboutToAppear called'); // 预加载数据 } onDidBuild() { hilog.info(DOMAIN, TAG, 'Index onDidBuild called'); } aboutToDisappear() { hilog.info(DOMAIN, TAG, 'Index aboutToDisappear called — cleaning up'); this.clearTimers(); } }8.2 在 DevEco Studio 中查看日志
运行后在Log 窗口过滤IndexLifecycle,可看到以下日志输出:
// 冷启动 09:15:23.456 [INFO] IndexLifecycle: Index aboutToAppear called 09:15:23.512 [INFO] IndexLifecycle: Index onDidBuild called // 点击返回/退出页面 09:16:01.234 [INFO] IndexLifecycle: Index aboutToDisappear called — cleaning up通过日志可以精确验证aboutToDisappear是否被正确触发,以及clearTimers()的执行时机。
九、性能优化建议
9.1 aboutToDisappear 执行耗时控制
aboutToDisappear的执行时间直接影响页面退出的流畅度:
| 耗时 | 用户体验 | 建议 |
|---|---|---|
| < 1ms | 完全无感 | ✅ 理想 |
| 1ms - 5ms | 几乎无感 | ✅ 可接受 |
| 5ms - 16ms | 轻微卡顿 | ⚠️ 需优化 |
| > 16ms | 明显卡顿 | 🚫 必须优化 |
clearTimers()在猫猫大作战中的实测耗时约为0.3ms— 三个clearInterval调用极快,对性能没有影响。
9.2 批量清理策略
当页面持有大量资源时,可以使用数组统一管理:
private timers: number[] = []; startGame() { this.timers.push(setInterval(() => { /* ... */ }, 100)); this.timers.push(setInterval(() => { /* ... */ }, 2000)); } aboutToDisappear() { // 批量清理 — 一行代码全部清除 this.timers.forEach(id => clearInterval(id)); this.timers = []; }十、总结
aboutToDisappear是 ArkUI 组件生命周期中最关键的清理入口。本文从「猫猫大作战」的clearTimers()实战出发,覆盖了它的触发机制、使用约束、与aboutToAppear的配合模式,以及常见踩坑和 HiLog 验证方法。
核心要点:
aboutToDisappear在组件销毁前同步执行,是释放定时器、事件监听、网络请求的标准化场所- 绝对禁止修改状态变量和使用 async/await
- 与
aboutToAppear成对设计,前者获取资源,后者释放资源 - 通过 HiLog 日志验证生命周期调用,确保清理逻辑被正确执行
下一篇预告:第 66 篇将深入onVisibleAreaChange— 可视区变化感知在滚动曝光与懒加载中的应用。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- 猫猫大作战 GitHub 项目
- HarmonyOS 自定义组件生命周期官方文档
- HarmonyOS 页面路由与生命周期
- 开源鸿蒙跨平台社区
- @ohos.hilog 日志 API 参考
- 第 36 篇:clearTimers 防泄漏
- 第 64 篇:SQLite 持久化
- 第 66 篇:onVisibleAreaChange