文章配图:ArkUI 页面生命周期中的  回调

页面预览

前言

在 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

当用户直接退出游戏页面(例如返回主页或关闭应用),如果这些定时器没有被清理,会发生:

  1. 定时器回调继续执行,尝试更新已销毁的 UI 组件 → 崩溃
  2. 应用进程无法被 GC 回收 → 内存泄漏
  3. 后台空转耗电 → 电量消耗
// 🚫 错误示范:退出页面后定时器仍然在跑
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 在以下两种场景下触发:

  1. 组件被条件移除if/else 分支切换、ForEach/LazyForEach 列表元素被删除
  2. 页面/组件销毁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() 的设计遵循三个原则:

  1. 幂等性:无论调用多少次,都不会产生副作用
  2. 标记清除:每次 clearInterval 后立即将 timerId 重置为 -1
  3. 统一入口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/setTimeoutaboutToDisappear 中统一清理
  • 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 — 可视区变化感知在滚动曝光与懒加载中的应用。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐