一个播放器页面,退出时你在 onDestroy 里规规矩矩调了 AVPlayer.release(),心想这次总算干净了。结果用户反馈:退出播放页,切到别的应用,声音还会从后台冒出来一两秒才断。你加日志一看,release 确实执行了,执行时间点也没问题——在 onDestroy 里,页面都该没了。那声音为什么还在?

答案不复杂,但你多半不会第一时间想到:AVPlayer.release() 不是同步停掉硬件输出。它做的是状态机收尾和资源标记,底层 AudioTrack 或 MediaCodec 的停止依赖后续的系统调度。如果你的 onDestroy 只是“调了 release”就结束,没有先显式 stop() 或确认播放已经进入 idle,硬件层可能还会短暂残留音频缓冲。更隐蔽的情况是:你在 onWindowStageDestroy 里就开始碰播放器了,而那个时间点,窗口还没彻底退出,播放器的某些回调可能还在路上。

onWindowStageDestroy 和 onDestroy 在 Stage 模型里是两层完全不同的生命周期,它们之间的边界,文档写了,但没有写全你该往里面放什么。

onWindowStageDestroy 只管窗口,不管 Ability

Stage 模型把应用组件和窗口管理做了解耦。UIAbility 负责组件的创建、前后台切换和销毁,WindowStage 负责窗口的创建与销毁。这个设计意图很明确,但落到代码里,很多人会不自觉地把“退出时该释放的东西”全部堆到其中一个回调里。

看一个最小对照。你在 onWindowStageCreate 里注册了 windowStage.on(‘windowStageEvent’),用来监听前台/后台/焦点变化。这个监听器持有的是 WindowStage 的引用。如果你不在窗口销毁前注销它,窗口对象已经进入销毁流程,监听器仍然从事件分发器那边被持有,内存里就多了一个“窗口已经没了,但监听还活着”的悬挂引用。Profiler 里看到内存曲线一路向上、窗口事件引用数量不减,基本就是这里。

正确的注销时机不是 onWindowStageDestroy,而是 onWindowStageWillDestroy。系统回调顺序是 onWindowStageWillDestroy → onWindowStageDestroy → onDestroy。onWindowStageWillDestroy 触发时,WindowStage 仍然可用,你还能调用 off()。到了 onWindowStageDestroy,窗口管理器已经进入销毁阶段,你再碰 windowStage 上的监听接口,行为就不保证了。

typescript

export default class EntryAbility extends UIAbility {
private windowStage: window.WindowStage | undefined = undefined;

onWindowStageCreate(windowStage: window.WindowStage): void {
this.windowStage = windowStage;
windowStage.loadContent(‘pages/Index’);
windowStage.on(‘windowStageEvent’, (event) => {
// 前后台/焦点变化处理
});
}

onWindowStageWillDestroy(windowStage: window.WindowStage): void {
// 最后的安全注销点,此时 windowStage 仍可用
windowStage.off(‘windowStageEvent’);
}

onWindowStageDestroy(): void {
this.windowStage = undefined; // 只断开引用,不在这里调 off
}
}

这里有一个容易忽略的细节:onWindowStageDestroy 里你把 this.windowStage 置为 undefined,这本身没错,断开 Ability 对窗口对象的强引用是好事。但如果你只在 onWindowStageDestroy 里做 off,就已经晚了。官方问答里有一句很直白的话:在 onWindowStageCreate 注册的监听,必须在 onWindowStageWillDestroy 中注销,否则 WindowStage 销毁后仍被持有,引发泄漏。这不是建议,是硬性时序约束。

onDestroy 清理的是 Ability 级资源,不是窗口残局

onWindowStageDestroy 负责窗口相关资源,但不意味着 Ability 层面的东西也在那里清。定时器、ApplicationContext 上注册的 environment 监听、数据库连接、全局的单例引用——这些和窗口生命周期无关,它们跟着 UIAbility 实例走。

一个典型的错误是:在 onWindowStageDestroy 里 clearInterval 或注销 environment 监听,然后在 onDestroy 里什么都不做,或者反过来,把窗口监听留到 onDestroy 才 off。两种都会出问题。前者在窗口重建(比如分屏切换)时把 Ability 级资源误杀了,后者在 Ability 真正退出时窗口监听早就该注销却没注销。

判断标准很简单:这个资源是在哪个作用域里 on 或 new 出来的,就回到那个作用域的销毁回调里去 off 或 release。窗口作用域的资源回 onWindowStageWillDestroy,Ability 作用域的资源回 onDestroy。

typescript

import { common } from ‘@kit.AbilityKit’;

export default class EntryAbility extends UIAbility {
private timerId: number = -1;
private envCallback: common.EnvironmentCallback | null = null;

onCreate(): void {
// Ability 级定时器
this.timerId = setInterval(() => { /* 轮询逻辑 / }, 5000);
// Ability 级环境监听
this.envCallback = {
onConfigurationUpdated: (config) => { /
… / },
onMemoryLevel: (level) => { /
… */ }
};
this.context.getApplicationContext()
.on(‘environment’, this.envCallback);
}

onDestroy(): void {
if (this.timerId !== -1) {
clearInterval(this.timerId);
this.timerId = -1;
}
if (this.envCallback) {
this.context.getApplicationContext()
.off(‘environment’, this.envCallback);
this.envCallback = null;
}
}
}

注意 onDestroy 里先把 this.envCallback 置 null 再存回去?顺序不重要,重要的是 off 必须用同一个函数引用。你如果在 onCreate 里写的是匿名箭头函数直接塞给 on,onDestroy 里没有那个引用,就 off 不掉。这不是鸿蒙独有的坑,但配合 Stage 模型的分层,泄漏的后果会叠加:窗口层漏一个,Ability 层漏一个,一次退出留下两个悬挂持有。

“谁创建谁释放”比“哪个回调更晚”更可靠

很多人排生命周期问题的思路是“找最晚的那个回调,把释放全塞进去”。这个思路在简单页面里能蒙对,在 Stage 模型下会失效。因为 onDestroy 确实最晚,但它执行的时候,窗口相关的对象可能已经处于半销毁状态,你再调窗口 API 是在碰一个不该碰的东西。反过来,onWindowStageDestroy 也不是 Ability 的终点,terminateSelf() 之后 Ability 实例可能还会经历清理流程,你把 Ability 级资源在那里释放,后面如果还有异步回调回来,就会摸到一个已经“提前清空”的对象。

比较稳的做法是给每个资源标记所有者。窗口监听的所有者是 WindowStage,定时器的所有者是 UIAbility,播放器的所有者是页面或 Ability(取决于你在哪一层创建的),ApplicationContext 监听的所有者是 Ability 的 context。所有者销毁,资源才销毁;所有者还活着,资源就不该被提前杀掉。

这也解释了为什么在 onBackground 里直接 release 播放器是个常见但不推荐的做法。onBackground 只是应用不可见,Ability 实例还在,窗口也还在。你如果在那里把播放器彻底释放了,用户切回前台时页面还在,播放器已经没了,要么黑屏,要么你得重建一套状态。前台独占资源的正确姿势是在 onBackground 里 pause 或 stop,把硬件输出让出去,回到 onForeground 再恢复,而不是把对象本身干掉。

播放器释放的额外一层:状态机不允许跳步

回到开头那个“退出后还有声音”的问题。即使你把 release 放对了位置(比如页面 aboutToDisappear 或 Ability 的 onDestroy,取决于播放器在哪一层创建),如果播放器当前处于 playing 状态,直接 release 在某些版本上不会立即静音。AVPlayer 有状态机约束:stop() 之后回到 idle 或 prepared,再 release() 才是干净的收尾。你在 onDestroy 里先 stop 再 release,和只写 release,底层音频硬件的行为可能不一样。

typescript

// 假设 player 是 AVPlayer 实例,且当前可能处于 playing
async function safeReleasePlayer(player: media.AVPlayer): Promise {
if (player.state === ‘playing’ || player.state === ‘paused’) {
await player.stop(); // 先回到 idle/prepared
}
if (player.state !== ‘released’) {
await player.release(); // 再释放资源
}
}

stop 的作用是让播放器退出播放状态、释放音频输出通道;release 才是销毁实例。两者顺序颠倒或者跳过前者,不会在编译期报错,但用户耳朵能听出来。

综上所述,onWindowStageWillDestroy 是窗口监听的最后安全出口,onWindowStageDestroy 只负责断开窗口引用,onDestroy 管 Ability 级资源——三者不是“程度不同的退出回调”,而是各自有明确的归属范围。释放之前先问一句:这个对象是在哪个作用域里被创建或被注册的?所有者还没死,就别急着替它收尸。播放器、数据库、监听器、定时器,都适用这一条。

Logo

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

更多推荐