【OpenHarmony/HarmonyOS】游戏应用生命周期治理:页面、Canvas、定时器、音频与网络如何正确收放
【OpenHarmony/HarmonyOS】游戏应用生命周期治理:页面、Canvas、定时器、音频与网络如何正确收放
游戏项目常见的“第二次进入变快两倍”“退出后仍耗电”“BGM 重复播放”,本质上都是生命周期没有闭环。本文从 UIAbility 到 ArkUI 组件,整理一套资源创建、暂停、恢复和释放策略。🔄
一、HarmonyOS 游戏里有哪些生命周期?
至少存在五层:
- 应用/Ability:
onCreate、onForeground、onBackground、onDestroy; - WindowStage:窗口创建和销毁;
- 页面:
aboutToAppear、aboutToDisappear、onPageShow、onPageHide; - ArkUI 组件:Canvas ready、AreaChange、组件出现/消失;
- 游戏会话:初始化、开始、暂停、重开、结束、退出。
同一个资源可能跨越不同层。比如 AudioManager 跨页面存在,GameLoop 只属于一场游戏,GalaxyBackground 的定时器只属于一个组件实例。
二、建立资源所有权表
| 资源 | 创建者 | 应释放位置 |
|---|---|---|
| Preferences Manager | EntryAbility/Application | 应用结束或长期复用 |
| SoundPool、AVPlayer | AudioManager | Manager.release / Ability 销毁 |
| GameLoop、实体数组 | GameEngine | 游戏退出/引擎销毁 |
| Galaxy 定时器 | GalaxyBackground | aboutToDisappear |
| 启动页悬浮定时器 | StartPage | onPageHide/aboutToDisappear |
| P2P 发现广播 | P2PConnectionManager | 页面退出或停止发现 |
| UDP Socket | P2P Manager | 会话/应用结束 |
| Display/Fold 监听 | Index | aboutToDisappear |
| WebView Controller | WebViewPage | 页面销毁时停止加载/释放 |
只要某个资源没有明确所有者,就很容易泄漏或被重复初始化。
三、Ability 负责长生命周期服务
项目在窗口创建时初始化 Manager:
onWindowStageCreate(windowStage: window.WindowStage): void {
AudioManager.getInstance().init(this.context);
DataManager.getInstance().init(this.context);
ScoreManager.getInstance().init(this.context);
UpgradeManager.getInstance().init(this.context);
UserManager.getInstance().init(this.context);
windowStage.loadContent('pages/StartPage');
}
适合放在这里的能力是:跨页面共享、创建成本较高、依赖 Ability Context。初始化函数应该幂等,多次调用不会创建第二个 SoundPool 或覆盖正在使用的 Preferences。
四、前后台切换要区分音频和游戏
项目回到前台时恢复 BGM:
onForeground(): void {
AudioManager.getInstance().resumeBGM();
}
但游戏逻辑也需要响应后台:
- 停止或暂停 DisplaySync;
- 清零输入向量,防止恢复后继续移动;
- 记录暂停时间,修正限时模式;
- 暂停网络状态发送;
- 返回前台时弹出暂停菜单,而不是直接继续战斗。
可以建立 AppLifecycleBus,Ability 只发布前后台事件,当前游戏页面订阅并决定行为。
五、GameLoop 的 start/stop 必须幂等
start() {
if (this.running) return;
this.running = true;
this.lastTime = Date.now();
this.accumulator = 0;
// 启动 DisplaySync
}
stop() {
this.running = false;
this.displaySync?.stop();
if (this.timerId !== -1) {
clearTimeout(this.timerId);
this.timerId = -1;
}
}
重新开始游戏前先停止旧循环。页面退出时也必须调用 gameEngine.stopGameLoop(),仅把 isGameRunning 改成 false 不会自动停止底层回调。
六、Canvas ready 不等于会话 ready
Canvas 的生命周期可能因为页面重建或尺寸变化多次触发。项目将“上下文可用”和“首次初始化”分开:
.onReady(() => {
this.gameEngine?.setContext(this.context);
})
.onAreaChange((_, area) => {
if (!this.isGameInitialized) {
this.gameEngine?.initGame(width, height, mode, difficulty);
this.isGameInitialized = true;
} else {
this.gameEngine?.updateScreenSize(width, height);
}
})
如果每次 AreaChange 都 initGame,旋转、折叠或布局动画会不断重置关卡;如果只在 onReady 初始化,又可能拿到 0 尺寸。
七、页面定时器必须保存句柄 ⏲️
GalaxyBackground 正确保存 renderInterval 并在消失时清理。启动页的悬浮动画目前直接 setInterval,没有保留 ID,属于典型隐患。
推荐封装:
private hoverTimer: number = -1;
aboutToAppear() {
if (this.hoverTimer === -1) {
this.hoverTimer = setInterval(() => {
this.updateHover();
}, 1000) as number;
}
}
aboutToDisappear() {
if (this.hoverTimer !== -1) {
clearInterval(this.hoverTimer);
this.hoverTimer = -1;
}
}
任何 setTimeout 也要考虑页面退出后回调是否仍会修改状态。波次过渡的两层 timeout 应保存会话版本或在回调中验证当前 sessionId。
八、回调引用也会造成泄漏
组队页把闭包赋给 P2P 单例:
manager.onDeviceFound = (device) => {
this.nearbyDevices.push(...);
};
页面退出只停止发现,但如果不把 onDeviceFound、onReceiveInvite、onGameStart 等设回 null,单例仍然持有页面实例引用。下一次消息可能修改已经销毁的页面。
更好的 API 是返回取消订阅函数:
const unsubscribe = manager.onDeviceFound((device) => { ... });
aboutToDisappear() {
unsubscribe();
}
九、UDP Socket 与发现广播是两个资源
stopDiscovery() 清除了广播 interval 和设备发现监听,但 UDP Socket 仍绑定端口。长期单例复用时这是有意行为;如果希望退出近场功能后完全释放,需要单独 close() 并清空 Peer。
因此 API 应区分:
startDiscovery / stopDiscovery
openSession / closeSession
init / release
不要让一个“stop”名字模糊地承担所有层级。
十、音频资源的释放
AudioManager 已避免重复创建 SoundPool,但还需要完整释放路径:
- 取消 AVPlayer 的
stateChange监听; - 停止并 release AVPlayer;
- 卸载/释放 SoundPool;
- 清空 soundMap;
- 标记未初始化;
- 处理正在进行的异步 load。
如果应用仅存在一个 Ability,泄漏可能暂时不明显;在热重载、Ability 重建或自动化测试中会快速暴露。
十一、异步初始化的竞态
用户可能在音效仍预加载时进入游戏,或在升级数据读取完成前退出。异步回调中都应验证当前所有者仍有效:
const generation = this.sessionGeneration;
const state = await upgradeManager.getUpgradeState();
if (generation !== this.sessionGeneration || !this.playerTank) {
return;
}
this.applyState(state);
每次重开增加 generation,旧任务即使完成也不会污染新会话。
十二、页面路由栈与背景动画
pushUrl 后旧页面可能仍在路由栈中。应确认其 onPageHide/aboutToDisappear 是否触发并暂停背景。多个页面各运行一套 60 FPS 星空、BGM 控制和定时器,会让性能问题看似来自游戏引擎,实际来自隐藏页面。
一次性启动页使用 replaceUrl 是合理选择;设置、商城等返回型页面使用 push,但隐藏时必须安静。
十三、建议使用统一会话状态机
type SessionState =
'idle' | 'initializing' | 'running'
| 'paused' | 'ending' | 'disposed';
所有操作先检查状态:
start只允许从 idle;pause只允许从 running;resume只允许从 paused;end只执行一次;dispose可以从任意非 disposed 状态执行且幂等。
比多个 isRunning/isPaused/isGameOver/isInitialized 布尔值更不容易形成矛盾组合。
十四、验证生命周期的测试方法 ✅
- 连续进入退出战斗 20 次,确认循环数量不增长;
- 页面切后台 30 秒后恢复,坦克不瞬移、限时规则符合预期;
- 多次进入启动页,动画速度不翻倍;
- 进入退出组队页后端口和广播数量正确;
- 隐藏页面 CPU 使用下降;
- Ability 重建后 BGM 只有一路;
- 旋转/折叠时关卡不重置;
- 异步加载完成时页面已退出,不发生状态写入;
- 内存快照中旧页面实例可以回收;
dispose()重复调用不抛异常。
十五、总结 ✨
游戏资源治理可以归纳为四个问题:
- 谁创建?
- 谁拥有?
- 什么时候暂停/恢复?
- 什么时候最终释放?
在 HarmonyOS 工程中,Ability 管长生命周期服务,页面管订阅与页面定时器,组件管自己的动画源,GameEngine 管会话循环和实体,Manager 管底层系统资源。再配合幂等 start/stop、会话版本和显式取消订阅,就能避免绝大多数“第二次进入才出现”的诡异问题。🔄
推荐标签: HarmonyOS OpenHarmony 生命周期 ArkUI 资源管理 游戏开发

更多推荐


所有评论(0)