HarmonyOS应用开发实战:猫猫大作战-ArkUI 页面生命周期中的 `aboutToDisappear` 回调


前言
在 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 — 可视区变化感知在滚动曝光与懒加载中的应用。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
更多推荐

所有评论(0)