HarmonyOS 7 闪控窗口生命周期与资源回收:连载06(共08篇)【共创稿事节】
HarmonyOS 7 闪控窗口生命周期与资源回收:连载06(共08篇)【共创稿事节】
用户在城市艺术夜展的入口排队时,可能反复打开活动详情、退至其他应用、点击闪控球展开闪控窗,再从闪控窗返回主应用。这些动作会改变系统窗口状态,却未必改变排队业务状态。若把“主应用进入后台”直接当作“排队结束”,用户的凭证就可能被错误取消;如果每次恢复前台都新建一个窗口控制器,又会遇到重复启动、事件回调重叠以及资源无法回收的问题。
上篇已经把进度消息集中到 QueueStore,主页面与轻量窗口均以 ticketId 和 seq 读取同一快照。现在要管理的是窗口本身:什么时候创建、什么时候可见、何时停止、停止后清理哪些订阅,以及主窗口返回时如何继续展示最新快照。排队资格始终由业务服务确认,轻量窗口生命周期只影响信息呈现。

一、先区分三种不同的“状态”
一个完整的排队应用至少包含三条状态轴。第一条是 EntryPhase,描述预约、等待、叫号、入场及终止阶段;第二条是 UIAbility 的前台、后台与销毁过程;第三条是闪控球、闪控窗各自的启动、隐藏、切换及停止。三者可以同时存在,但不能互相替代。
| 发生的动作 | 业务凭证 | 窗口管理 |
|---|---|---|
| 用户切换到其他应用 | 保留最新排队快照 | 依系统窗口规则继续呈现 |
| 点击闪控球展开闪控窗 | 凭证不变 | 变更窗口形态 |
| 关闭闪控窗 | 仍可在主应用查看凭证 | 停止窗口并注销窗口监听 |
| 收到入场成功事件 | 归约到 ADMITTED | 根据产品策略收起或停止窗口 |
| 重新进入活动页面 | 查询并显示最新快照 | 按需重建入口与观察关系 |
对于已经绑定的闪控球与闪控窗,floatView.bind() 要求两个控制器在启动前完成绑定。绑定后任一停止接口都会同时结束两个窗口;切换为另一种形态时,不能按“被停止后重新创建”的逻辑反复调用启动接口。闪控窗 onStateChange() 可能返回 STARTED、HIDDEN、STOPPED、IN_SIDEBAR、IN_FLOATING_BALL 和 ERROR,其中 IN_FLOATING_BALL 是形态变化的重要信号。
stop() 返回的 Promise 表示停止请求已处理,不能据此立即认为完整停止流程已经结束。窗口真正进入停止状态应结合 FloatViewState.STOPPED 回调判断。这个细节直接关系到重复启动防护和监听器释放的先后顺序。
二、集中管理窗口控制器与回调引用
沿用前面的 QueueBallGateway、QueueFloatGateway 与 QueueStore,新增 logic/QueueWindowLifecycle.ets 专门处理两个控制器的状态回调。下面的类接收已经创建好的控制器,监听器作为稳定的成员函数保存。只有保存相同的函数引用,才能在停止后准确注销,防止匿名回调长期挂在旧控制器上。
// logic/QueueWindowLifecycle.ets
import { floatView, floatingBall } from '@kit.ArkUI';
export class QueueWindowLifecycle {
private view?: floatView.FloatViewController;
private ball?: floatingBall.FloatingBallController;
private active: boolean = false;
private stopping: boolean = false;
private bound: boolean = false;
private readonly onViewState =
(info: floatView.FloatViewStateChangeInfo): void => {
if (info.state === floatView.FloatViewState.STARTED) {
this.active = true;
} else if (info.state === floatView.FloatViewState.STOPPED) {
this.active = false;
this.stopping = false;
this.detach();
} else if (info.state === floatView.FloatViewState.ERROR) {
console.error('闪控窗状态异常');
}
};
private readonly onBallState =
(state: floatingBall.FloatingBallState): void => {
console.info(`闪控球状态:${state}`);
};
attach(view: floatView.FloatViewController,
ball: floatingBall.FloatingBallController, bound: boolean): void {
this.detach();
this.view = view;
this.ball = ball;
this.bound = bound;
this.stopping = false;
view.onStateChange(this.onViewState);
ball.on('stateChange', this.onBallState);
}
async stop(): Promise<void> {
if (this.stopping) return;
this.stopping = true;
try {
if (this.bound && this.view) {
await this.view.stop();
} else {
if (this.view) await this.view.stop();
if (this.ball) await this.ball.stopFloatingBall();
}
} catch (error) {
this.stopping = false;
console.error(`停止轻量窗口失败:${String(error)}`);
}
}
detach(): void {
this.view?.offStateChange(this.onViewState);
this.ball?.off('stateChange', this.onBallState);
this.view = undefined;
this.ball = undefined;
this.bound = false;
}
}
类中 bound 是业务层保存的绑定关系标记,并非系统接口自动提供的属性。采用已绑定模式时,调用一次 view.stop() 即可,避免对闪控球重复发出停止请求。未绑定模式需要分别停止,两者的实际状态仍以对应回调为准。工程可以进一步把已停止、正在停止、已启动等状态整理为枚举,针对多次快速点击实施更严格的串行锁。
detach() 只移除窗口监听器,不删除 QueueStore 的业务快照。停窗后,主页面仍可以通过既有仓储继续展示进度。如果项目还为窗口单独注册了 queueStore.listen(),应保存该函数并在窗口停止时执行;窗口事件监听、业务消息订阅和窗口内容页的 LocalStorage 是不同资源,应该分别清理。
三、让窗口内容订阅与停止事件成对出现
上篇的事件仓储已经返回取消订阅的函数。新增一个较小的会话管理器可以集中持有它,避免 FloatViewPage、主页面和网关分别创建多个长期监听。窗口成功启动后绑定当前凭证;收到 STOPPED 或用户主动关闭时,清理订阅与旧投影引用。
// logic/QueueWindowSession.ets
import { QueueSnapshot } from '../model/QueueModel';
import { queueStore } from './QueueStore';
export class QueueWindowSession {
private dispose?: () => void;
private currentTicket: string = '';
private lastSeq: number = -1;
open(ticketId: string,
present: (snapshot: QueueSnapshot) => void): void {
this.close();
this.currentTicket = ticketId;
this.dispose = queueStore.listen(ticketId, (snapshot: QueueSnapshot): void => {
if (snapshot.ticketId !== this.currentTicket) return;
if (snapshot.seq <= this.lastSeq) return;
this.lastSeq = snapshot.seq;
present(snapshot);
});
}
close(): void {
this.dispose?.();
this.dispose = undefined;
this.currentTicket = '';
this.lastSeq = -1;
}
}
这里的 present 是应用自身注入的窗口更新回调,不是系统接口。闪控球可把它接到上篇的 QueueBallRelay.offer();闪控窗可用 toFloatPanel() 转换后更新连载04建立的 LocalStorage。这既避免每个窗口直接访问网络,也使窗口关闭时的数据订阅清理具备一个明确入口。
用户在窗口上点击“关闭”,close() 应与窗口停止回调协调执行。对已经停止的会话重复调用 close() 不会造成数据丢失;但是主页面的订阅是另一份,不应该共用同一个释放函数。多个凭证交替成为关注对象时,先撤销旧 ticketId 的窗口观察,再注册新凭证,防止把旧活动的序号投到新活动窗口里。

四、主应用前后台与轻量窗口的可见性策略
Stage模型下,主窗口与 UIAbility 生命周期密切相关,但闪控窗属于独立的辅助窗口。主应用切到后台时,闪控窗可以继续呈现,这正是排队轻量交互的价值所在。不能在 onBackground() 里一律停止闪控窗,否则用户刚切换应用就失去了排队入口。
闪控窗控制器提供 setFloatViewVisibilityInApp(boolean),用于控制应用在前台时闪控窗是否可见。对于进入完整排队页后避免重复遮挡的场景,可以把该选项设为 false;这是一项显示策略,并非排队业务状态。恢复到后台后的窗口展示仍须遵循系统对当前窗口状态的处理。
// logic/QueueWindowVisibility.ets
import { floatView } from '@kit.ArkUI';
export class QueueWindowVisibility {
private controller: floatView.FloatViewController;
constructor(controller: floatView.FloatViewController) {
this.controller = controller;
}
async avoidCoveringMainPage(): Promise<void> {
try {
await this.controller.setFloatViewVisibilityInApp(false);
} catch (error) {
console.error(`设置主应用内可见性失败:${String(error)}`);
}
}
async allowVisibleInMainPage(): Promise<void> {
try {
await this.controller.setFloatViewVisibilityInApp(true);
} catch (error) {
console.error(`恢复主应用内可见性失败:${String(error)}`);
}
}
}
不要把前后台事件和窗口创建动作写成无条件的一对一关系。更合适的做法是 UIAbility 的 onForeground() 触发业务快照读取和页面恢复,onBackground() 停止只属于完整页面的高频视觉刷新;轻量窗口根据其控制器状态及用户意愿决定是否继续呈现。若系统关闭关联主窗口而导致轻量窗口停止,应通过状态回调释放订阅,下次进入主应用后重新完成能力判断与控制器建立。
五、一次性处理异常、重复关闭和恢复入口
窗口停止时容易出现三类竞态:用户连续两次点击关闭,系统状态已变为 STOPPED 但代码还准备更新内容,或主应用恢复前台时旧窗口的回调晚到。治理的关键是保存窗口状态、让停止操作具备幂等性,并以最新业务快照作为重建时的数据来源。
对于 FloatViewState.IN_FLOATING_BALL,说明用户正在切换到闪控球,不应把它视为完全停止;对于 HIDDEN,也不应自动删除凭证缓存。只有真正进入 STOPPED 且相关会话结束后,才释放该窗口的监听和页面投影。业务层的 QueueStore 可以继续保留当前版本,避免重开后短暂回退到旧状态。
绑定的两个窗口在用户展开、收起时可能发生状态切换。此时若同时向两个控制器发送更新,应核对各自支持的当前操作状态,避免对已经停止的控制器执行更新。窗口更新失败可记录错误类别并等待重新建立窗口会话,不能因为一个视觉容器异常就覆盖业务快照,更不能反向修改排队状态。
建议沿着“创建—订阅—切换—停止—清理—重新进入”这条路径检查。关闭后重点确认旧回调没有重复触发、业务事件仍可进入仓储、重新打开时读取的是最新 seq。系统窗口状态与业务服务响应分别记录:一个负责“窗口能否显示”,另一个负责“用户是否可以入场”。

六、收束到可以持续维护的责任划分
经过本篇整合,排队状态不再寄托于某个页面或悬浮控件。QueueStore 保存可信业务快照;闪控球和闪控窗各有投影层;QueueWindowSession 管理业务订阅;QueueWindowLifecycle 处理窗口状态及监听释放。新增页面、改变展示样式或切换排队数据渠道时,不必重新定义“已入场”和“已失效”的含义。
特别需要坚持的是:用户关闭窗口与主动取消预约必须是不同的操作。前者只回收呈现资源,后者需要明确的业务请求与服务端确认。这一划分既让系统窗口管理更稳定,也减少了用户最关心的票务信息被界面操作意外修改的可能性。
更多推荐


所有评论(0)