WindowStage.createSubWindow:辅助任务窗重复唤起与销毁竞态【鸿蒙心迹】
这次把问题放在一个很小的任务上:主页面有个“打开辅助任务窗”的按钮,用户连续点三次,系统究竟应该出现几个窗口?如果只把按钮回调直接接到 createSubWindow,窗口创建是异步的,第二次点击很可能发生在第一次创建完成之前。开发者在页面上看到的是“只按了几下”,代码里却是几条彼此不知道对方存在的创建链路。
我给演示项目起名 QuickWindow,任务编号 QW-416。预置三次打开请求,目标是只创建一个名为 QuickNotePanel 的应用内子窗口,其余两次在业务门禁处拦截。截图里的请求数 3、创建数 1、拦截数 2、最终状态 VISIBLE 均为固定演示契约,不代表执行过手机测试。

一、先讲清楚窗口类型,再谈“悬浮”
HarmonyOS 的窗口能力不是一个名字对应一种效果。WindowStage 管理应用主窗,也能创建应用内子窗口;子窗口可用于补充信息、编辑或轻量任务。它和系统级闪控窗、全局悬浮窗不是同一项能力,权限边界、前后台跟随策略也不能直接互换。官方《子窗口开发指导》明确给出 createSubWindow()、setUIContent()、showWindow() 和 destroyWindow() 的创建、装载、展示、销毁顺序。
因此这里用“辅助任务窗”而不用“闪控窗”冒充系统形态。演示先覆盖应用内部的重复唤起与销毁竞态,再列出进入真实产品时需要验证的窗口模式和设备行为。固定窗口名称的好处也很直接:定位日志时不必追逐随机名称;代价是如果上一个窗口尚未销毁,不能随意再建一个同名实例。
任务窗内只有一条记录“今晚核对课程提纲”和一个“关闭辅助窗口”按钮;这些都是示例业务字段。它们不会改变窗口 API 行为,却能让后续验收聚焦在同一对象上。
二、创建请求必须在回调之前占住位置
核心故障通常不是 createSubWindow 本身,而是状态修改太晚。若等待成功回调才把 opened 改成 true,多次点击就会同时穿过判断。更可靠的顺序是:进入回调前先切为 CREATING,失败后才释放创建门禁。这里把窗口控制收敛到 QuickPanelController,不让不同页面各自维护一个布尔值。
下面第一段代码解决“一个窗口多条创建链”的问题。phase 是业务层状态机,QuickNotePanel 是传给系统的子窗口名称。错误路径必须恢复可重试状态;加载与展示也必须逐次判错,不应把“对象创建成功”当作“用户已经看到了窗口”。
import { window } from '@kit.ArkUI';
type Phase = 'IDLE' | 'CREATING' | 'VISIBLE' | 'CLOSING' | 'FAILED';
export class QuickPanelController {
private stage: window.WindowStage;
private panel: window.Window | undefined;
private phase: Phase = 'IDLE';
private pendingClose: boolean = false;
private requests: number = 0;
private ignored: number = 0;
private created: number = 0;
constructor(stage: window.WindowStage) { this.stage = stage; }
open(): void {
this.requests++;
if (this.phase !== 'IDLE') {
this.ignored++;
console.info(`[QuickWindow] QW-416 ignored=${this.ignored}`);
return;
}
this.phase = 'CREATING';
this.stage.createSubWindow('QuickNotePanel', (err, child) => {
if (err?.code || !child) {
this.pendingClose = false;
this.phase = 'IDLE';
console.error(`[QuickWindow] QW-416 create failed: ${err?.code}`);
return;
}
this.panel = child;
this.created++;
if (this.pendingClose) { this.dispose(); return; }
child.setUIContent('pages/QuickNotePanel', (loadErr) => {
if (this.pendingClose) { return; }
if (loadErr?.code) { this.close(); return; }
child.showWindow((showErr) => {
if (this.pendingClose) { return; }
if (showErr?.code) { this.close(); return; }
this.phase = 'VISIBLE';
console.info('[QuickWindow] QW-416 created=1 phase=VISIBLE');
});
});
});
}
这里的 requests、ignored、created 都属于控制器,不是系统窗口管理器的统计指标。演示脚本可以模拟三次点击,但截图的日志只是人为编排的目标状态。如果实际 createSubWindow 返回错误码,应记录错误并回到 IDLE,同时检查窗口名是否仍被其他未销毁对象占用。不能在 catch 或错误回调里无条件宣布创建成功。
另一个容易忽略的细节是“从创建回调到 setUIContent 回调”也存在时间缝隙。窗口对象已经返回不等于内容已装载,内容装载完成也不等于系统已显示。把这三步拆开,才能判断重复点击被拒绝时究竟处于哪一阶段。另一个验收细节是按钮的可用状态:从 CREATING 到 VISIBLE,可以给用户显示“正在打开”,但不需要向系统重复提交相同请求。若业务页面重建,按钮显示的文字应当重新读取控制器状态,而不是重新发起创建。应用层门禁在这里更像一把单独的锁,而不是让UI组件自己凭经验判断。
三、销毁不能仅把一个变量置空
用户在正在创建时就关闭任务窗,是第二条竞争路径。若 panel 还没有返回,此时直接清空变量,稍后的创建回调仍可能把一个过期窗口展示出来。因此关闭请求需要记下 pendingClose,让创建回调负责销毁“晚到”的对象。若窗口对象已经存在,则进入 CLOSING,等待 destroyWindow 的回调再恢复 IDLE。
第二段代码补充到同一个控制器类中,用于把销毁变成单独的状态转移。销毁失败时保留 FAILED 状态而不是立刻重新创建;正式工程可再设计“查找已存在窗口—复核状态—人工或程序化恢复”的路径。
close(): void {
if (this.phase === 'IDLE' || this.phase === 'CLOSING') { return; }
if (this.phase === 'FAILED') { return; }
this.pendingClose = true;
if (!this.panel) { this.phase = 'CLOSING'; return; }
this.dispose();
}
private dispose(): void {
const target = this.panel;
if (!target) { return; }
this.phase = 'CLOSING';
target.destroyWindow((err) => {
if (err?.code) {
this.phase = 'FAILED';
console.error(`[QuickWindow] QW-416 destroy failed: ${err.code}`);
return;
}
this.panel = undefined;
this.pendingClose = false;
this.phase = 'IDLE';
console.info('[QuickWindow] QW-416 phase=IDLE');
});
}
}
CLOSING 不是“窗口已经不存在”的意思,它代表对象正在释放,新的打开请求仍应被拦截。失败时留下对象引用,是为了避免失去唯一可处理的窗口句柄;但如果用户通过标题栏等系统交互关闭了窗口,还需要在产品里结合窗口事件和查询机制同步状态。本段只处理由业务按钮发起的关闭,不把所有系统关闭路径都假定为已经覆盖。
还有一个取舍:这里的 FAILED 暂停新建,是偏保守的故障策略。在真实工具类产品中,可以提示用户返回主页面重试,但绝不能在对象状态未知时反复调用 createSubWindow,把一个显示问题放大成窗口残留问题。
四、页面与 WindowStage 的责任要分开
第三段代码展示集成位置,解决“页面消失后还有一个控制器在工作”的问题。WindowStage 来自 UIAbility.onWindowStageCreate,控制器最好由对应 Ability 持有;页面点击只提交请求。示例中的 QuickPanelBridge 是项目自建的轻量引用容器,并不是 HarmonyOS 新增系统 API。
// QuickPanelBridge.ets:项目自定义桥接对象
export class QuickPanelBridge {
static current?: QuickPanelController;
}
// EntryAbility.ets:放在 UIAbility 类的对应生命周期方法中
onWindowStageCreate(stage: window.WindowStage): void {
QuickPanelBridge.current = new QuickPanelController(stage);
stage.loadContent('pages/HomePage', (err) => {
if (err?.code) { console.error(`HomePage load failed: ${err.code}`); }
});
}
onWindowStageDestroy(): void {
QuickPanelBridge.current?.close();
QuickPanelBridge.current = undefined;
}
// HomePage.ets:按钮的业务调用示意
Button('打开辅助任务窗')
.onClick(() => QuickPanelBridge.current?.open())
这里是分文件摘录,不是要求把 onWindowStageCreate 直接放进 ArkUI 组件。即使主动调用了 close(),Ability 销毁也可能使异步回调被中断,因此真正验收仍要观察窗口对象是否按系统生命周期释放。对于在自由窗口模式下能否超出主窗口显示,也必须按官方边界在对应设备上单独确认。

五、从日志判断门禁是否有效
QuickWindow 的预设事件顺序是:12:40:11 接受第一条请求,状态进入 CREATING;12:40:12 又收到两次请求,ignored=2;12:40:13 装载和显示回调完成,得到 created=1、VISIBLE。这只是供图文校准使用的演示时间线,不是宣称设备实际运行到秒级时间。
如果要把它变成能复现的验收项,我会分别检查“快点三次只保留一个实例”“创建失败后恢复按钮”“加载失败不显示空白窗”“创建期间立即关闭不残留”“销毁失败禁止盲目重建”。每一项都要通过真实回调确认,而不能只看页面文字。尤其要记录 createSubWindow 和 destroyWindow 的错误码,以及是否仍能找到同名窗口。

运行界面示意将状态固定在“辅助窗口已显示”这一刻:任务 QW-416、窗口名 QuickNotePanel、请求 3、创建 1、拦截 2、状态 VISIBLE。点击“关闭辅助窗口”后预期进入 CLOSING,最终回到 IDLE;这条后续路径没有在该张图片中冒充已执行验收。
六、边界比“能弹出来”更重要
官方窗口生命周期文档区分了应用主窗、应用子窗、独立子窗、全局悬浮窗和闪控球的跟随行为。这个 Demo 使用的是普通子窗,不代表后台悬浮能力,也不代表具备系统级闪控体验。若产品希望主应用进入后台后仍保持可交互窗口,应重新评估窗口类型、产品准入和用户可感知的关闭方式,而不是把本例的 WindowStage 代码直接套用。
工程上最重要的判断是:先让“创建是否被接受”成为确定的状态,再处理“窗口是否显示”,最后处理“对象何时释放”。没有实机时可以完成状态模型、代码审查与示意图核对,但不能宣称创建竞态在各种机型上已经消失。下一次真实验证应重点观察快速点击、后台切换和异常关闭三条链路。还应覆盖窗口被系统回收后的反向通知:如果业务控制器只认为窗口还在,用户可能永远打不开第二次。对于销毁中收到的再次打开请求,本例一律拒绝,不做自动排队;若产品需要自动重开,必须再增加一次性排队令牌和过期取消机制,避免把已经撤销的用户意图重新执行。
资料依据:华为《子窗口开发指导》(2026-09-09)与《窗口生命周期》(2026-09-09)。本篇所示界面和日志均为模拟,未经 DevEco 编译与设备运行确认。
更多推荐





所有评论(0)