HarmonyOS 窗口资源降载:WindowStage前后台切换与任务收放
HarmonyOS 窗口资源降载:WindowStage前后台切换与任务收放

应用进入后台后仍保持地图定位刷新、动画定时器、相机预览、WebSocket心跳和大图预取,不仅增加功耗,也可能与系统后台规则冲突。另一种极端是窗口刚失去焦点就停止所有任务,用户打开弹窗、分屏操作或临时切换焦点时,页面频繁断流重建,体验同样糟糕。
WindowStage事件提供了比页面显示更高一层的窗口状态。正确做法不是在每个回调里随手启动或停止资源,而是把事件翻译成“活跃、降频、冻结”三档策略,由单一资源协调器幂等执行。本文给出完整实现,并说明INACTIVE与HIDDEN不能等价处理。
1. 四个基础事件表达不同事实
SHOWN表示窗口处于前台,ACTIVE表示获得焦点,INACTIVE表示失去焦点,HIDDEN表示进入后台。API 11还提供RESUMED和PAUSED表示前台是否可交互。
| 事件 | 窗口事实 | 建议资源档位 |
|---|---|---|
| SHOWN | 已在前台显示 | 恢复必要数据,先用温和频率 |
| ACTIVE / RESUMED | 前台且可交互 | 恢复交互所需实时任务 |
| INACTIVE / PAUSED | 前台但暂不可交互 | 降频,不立刻销毁全部资源 |
| HIDDEN | 已进入后台 | 停止前台专属任务,保存检查点 |
如果一收到INACTIVE就断开长连接,系统弹窗或多窗口焦点变化会造成频繁重连。真正适合重降载的边界通常是HIDDEN。
2. 先列出每类任务的收放策略
不同资源不能统一调用一个stopAll()。动画可暂停并保留进度,图片预取可直接取消,长连接可降低心跳或按业务断开,编辑草稿需要先保存。
type ResourceLevel = 'active' | 'reduced' | 'frozen';
interface ManagedTask {
readonly name: string;
apply(level: ResourceLevel): Promise<void>;
}
interface ResourceDecision {
level: ResourceLevel;
reason: string;
changedAt: number;
}
把策略放在任务实现内,协调器只决定档位。这样新增播放器或传感器任务时,不需要修改窗口事件分支。
3. WindowStage事件需要保存同一个回调引用
注册与解除必须使用同一个函数对象。把回调定义为类字段,避免off时重新创建匿名函数导致监听残留。
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
private windowStage?: window.WindowStage;
private readonly onStageEvent = (event: window.WindowStageEventType): void => {
StageResourceHub.instance.onWindowEvent(event);
};
onWindowStageCreate(stage: window.WindowStage): void {
this.windowStage = stage;
stage.on('windowStageEvent', this.onStageEvent);
stage.loadContent('pages/Index');
}
onWindowStageDestroy(): void {
this.windowStage?.off('windowStageEvent', this.onStageEvent);
this.windowStage = undefined;
StageResourceHub.instance.shutdown();
}
}
Ability销毁时不仅解除事件,还要关闭协调器拥有的任务。只做off会留下网络、定时器或设备句柄。
4. 窗口降载闭环允许再次恢复

窗口显示后先准备资源,获得焦点进入活跃档;失去焦点只降频;进入后台冻结前台任务;再次回到前台时按依赖顺序恢复。恢复必须读取上次检查点,而不是假设旧句柄仍然有效。
5. 用纯函数把事件映射为档位
事件到策略的映射最好独立成纯函数,便于单元测试覆盖每个枚举值。未知事件保持当前档位或使用保守策略,不能默认进入活跃状态。
function levelForEvent(event: window.WindowStageEventType): ResourceLevel {
switch (event) {
case window.WindowStageEventType.ACTIVE:
case window.WindowStageEventType.RESUMED:
return 'active';
case window.WindowStageEventType.SHOWN:
case window.WindowStageEventType.INACTIVE:
case window.WindowStageEventType.PAUSED:
return 'reduced';
case window.WindowStageEventType.HIDDEN:
return 'frozen';
default:
return 'reduced';
}
}
实际产品可以让SHOWN直接进入active,但仍要等待页面与账号状态准备完成。事件只说明窗口状态,不保证所有业务依赖已经就绪。
6. 协调器必须幂等并串行执行
系统可能连续发送相同或相邻事件。协调器遇到相同档位直接返回,不重复启动任务;档位变化时串行应用,防止active和frozen并发操作同一连接。
class StageResourceHub {
static readonly instance: StageResourceHub = new StageResourceHub();
private level: ResourceLevel = 'frozen';
private tasks: ManagedTask[] = [];
private transition: Promise<void> = Promise.resolve();
register(task: ManagedTask): void {
this.tasks.push(task);
}
onWindowEvent(event: window.WindowStageEventType): void {
const target = levelForEvent(event);
this.transition = this.transition.then(async () => {
if (target === this.level) return;
for (const task of this.tasks) {
await task.apply(target);
}
this.level = target;
}).catch((error: Object) => {
console.error(`resource transition failed: ${JSON.stringify(error)}`);
});
}
shutdown(): void {
this.tasks = [];
this.level = 'frozen';
}
}
如果任务之间有依赖,应明确恢复顺序与冻结的逆序。例如先恢复认证再恢复业务连接,冻结时先停业务请求再停认证刷新。
7. 定时刷新任务按档位改变频率
下面的任务在active每15秒刷新,在reduced每60秒刷新,frozen彻底清除定时器。它不会重复创建多个定时器。
class RefreshTask implements ManagedTask {
readonly name: string = 'dashboard_refresh';
private timerId: number = -1;
async apply(level: ResourceLevel): Promise<void> {
this.stopTimer();
if (level === 'frozen') return;
const interval = level === 'active' ? 15_000 : 60_000;
this.timerId = setInterval(() => this.refresh(), interval);
if (level === 'active') {
await this.refresh();
}
}
private stopTimer(): void {
if (this.timerId !== -1) {
clearInterval(this.timerId);
this.timerId = -1;
}
}
private async refresh(): Promise<void> {
// 从仓储层获取轻量增量数据
}
}
降频间隔应根据业务时效性决定。对没有后台价值的推荐刷新,reduced也可以停止;对用户正在看的计时信息,可保留本地计算而停止网络。
8. 窗口资源边界统一进入协调器

WindowStage和页面会话提供状态信号,资源协调器决定档位,后台策略约束任务可以做什么。页面不直接持有全局长连接,WindowStage也不理解具体业务任务,边界因此保持清晰。
class PageSessionGate {
private visiblePageCount: number = 0;
enter(): void {
this.visiblePageCount += 1;
}
leave(): void {
this.visiblePageCount = Math.max(0, this.visiblePageCount - 1);
}
get hasVisiblePage(): boolean {
return this.visiblePageCount > 0;
}
}
多窗口或多个页面共享任务时,仅靠单个页面aboutToDisappear无法决定全局资源是否可停。窗口档位与可见页面计数应共同参与决策。
9. 前台恢复先检查句柄是否仍有效
相机、传感器、音频和WebSocket在后台可能被系统或远端关闭。恢复时先核对状态,再决定复用还是重建;不要直接调用旧实例的start()。
interface RecoverableConnection {
isUsable(): boolean;
connect(): Promise<void>;
reduce(): Promise<void>;
disconnect(): Promise<void>;
}
async function applyConnectionLevel(
connection: RecoverableConnection,
level: ResourceLevel
): Promise<void> {
if (level === 'active' && !connection.isUsable()) {
await connection.connect();
} else if (level === 'reduced') {
await connection.reduce();
} else if (level === 'frozen') {
await connection.disconnect();
}
}
10. HIDDEN前保存检查点而不是等待销毁
窗口进入后台时,保存编辑位置、下载断点和任务代次。不要把关键保存只放到Ability销毁,因为进程终止前未必给业务足够时间执行复杂操作。
interface SessionCheckpoint {
route: string;
scrollOffset: number;
draftVersion: number;
savedAt: number;
}
async function saveCheckpoint(checkpoint: SessionCheckpoint): Promise<void> {
// 写入Preferences或RDB,保持操作短小、可重试
}
保存操作也要防抖,避免INACTIVE与HIDDEN连续到来时重复写同一内容。
11. 多窗口不能用单个布尔值表示前后台
一个应用可能存在多个WindowStage。某个窗口HIDDEN时,另一个仍ACTIVE。应用级资源需要维护每个窗口的状态,再计算全局最高档位。
function mergeLevels(levels: ResourceLevel[]): ResourceLevel {
if (levels.includes('active')) return 'active';
if (levels.includes('reduced')) return 'reduced';
return 'frozen';
}
窗口ID到档位的映射在窗口销毁时必须移除,否则全局状态可能永远停留在旧档位。
12. 前后台切换验收矩阵
[ ] ACTIVE与INACTIVE之间切换不会反复重建重资源
[ ] HIDDEN后前台专属定时器与预取任务停止
[ ] SHOWN恢复时先核对旧句柄可用性
[ ] 重复事件不会创建重复定时器或连接
[ ] 任务应用档位的过程串行执行
[ ] 关键检查点在进入后台时保存
[ ] 多窗口状态按窗口分别维护
[ ] WindowStage销毁时回调与任务都已清理
13. WindowStage资料索引
WindowStage.on/off与WindowStageEventType:以本机HarmonyOS SDK API 23类型声明为准。- Stage模型开发概览
窗口事件只是事实,资源档位才是策略。把INACTIVE理解为暂时失焦,把HIDDEN理解为真正后台,再由幂等协调器统一收放任务,应用既不会在短暂焦点变化中频繁重建,也不会进入后台后继续无意义地消耗资源。
更多推荐



所有评论(0)