ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HarmonyOS 7 floatView:闪控窗重复启动1300031与会话代次隔离

HarmonyOS 7 floatView:闪控窗重复启动1300031与会话代次隔离 9 月底把派送路线做进闪控窗时我原以为最难的是窗口尺寸。真正把测试包卡住的却是一条很短的日志用户连续点两次“开启随行窗口”第一次start()已经返回第二次仍然报 1300031偶尔主页面明明显示“已停止”旧控制器的STARTED回调又把状态改回“运行中”。窗口本身没有复制出两个但业务层已经同时相信两个互斥事实。这次 Demo 叫FloatCourier入口页是ControlDeskPage闪控窗内容页是FloatRoutePage任务号固定为FV-1012。路线为“东门仓 → 科技园 B3”共 7 件包裹当前进度 68%预计 12 分钟。最后一次联调发生在 10:12使用 HarmonyOS 7 / API 26.0.0窗口为 720×480 px。文章不讨论如何画一个漂亮的小窗而是只解决三件事重复启动怎么合并、旧回调怎么隔离、停止时怎么把监听和页面状态一起收口。一、start()返回并不等于窗口已经启动window.floatView从 26.0.0 起提供闪控窗能力只能用于 Stage 模型。官方接口的关键提醒是FloatViewController.start()的 Promise 完成不代表启动流程结束真正的成功信号是onStateChange收到FloatViewState.STARTED。如果把 Promise 完成当成“窗口已运行”按钮会过早恢复可点第二次调用可能撞上“正在启动”或“正在停止”的中间态。这也是最初实现的根因。页面把await controller.start()后的状态直接写成RUNNING而系统回调可能晚几十毫秒。快速双击时第二个调用看见的仍是可启动状态页面退出再进入时新页面还会收到旧控制器的状态变化。1300030 更常见于重复启动1300031 表示当前状态不支持该操作1300033 还可能来自同时启动多个闪控窗或关联主窗不在前台。它们都不是适合靠“延时 500 ms 再试一次”修掉的错误。FloatCourier 因而把业务态限定为IDLE → CREATING → READY → STARTING → RUNNING → STOPPING → STOPPED。只有系统回调可以把STARTING推进到RUNNING只有STOPPED回调可以确认会话结束。按钮只触发意图不直接宣告结果。二、先把控制器、页面和数据放进同一个会话工程目录中与这条链路有关的文件只有四个pages/ControlDeskPage.ets负责主页面pages/FloatRoutePage.ets读取小窗状态service/FloatSessionCoordinator.ets持有控制器model/RouteSnapshot.ets定义跨页面数据。FloatRoutePage必须登记在main_pages.json因为setUIContext()不接受随意拼出来的相对路径。第一段代码解决“每次点击都新建控制器”的问题。协调器先检查设备能力再创建唯一控制器把快照和代次写进同一个LocalStorage。LocalStorage不是进程级持久化工具它只承担主页面与当前闪控窗 UIContent 的页面级状态传递真正需要跨进程恢复的数据仍应落库。import{common}fromkit.AbilityKit;import{floatView,window}fromkit.ArkUI;exporttypeFloatStateIDLE|CREATING|READY|STARTING|RUNNING|STOPPING|STOPPED;exportclassFloatSessionCoordinator{privatecontroller?:floatView.FloatViewController;privatestoragenewLocalStorage({});privatestate:FloatStateIDLE;privateepoch12;privatestartFlight?:Promisevoid;constructor(privatecontext:common.UIAbilityContext){}asyncprepare():Promisevoid{if(this.controller||this.stateCREATING)return;if(!floatView.isFloatViewEnabled())thrownewError(FLOAT_VIEW_UNSUPPORTED);this.stateCREATING;constctlawaitfloatView.create({context:this.context,templateType:floatView.FloatViewTemplateType.ROUNDED_RECTANGLE,isConfirmOnClose:true});this.storage.setOrCreate(taskId,FV-1012);this.storage.setOrCreate(route,东门仓 → 科技园 B3);this.storage.setOrCreate(progress,68);this.storage.setOrCreate(epoch,this.epoch);awaitctl.setUIContext(pages/FloatRoutePage,this.storage);awaitctl.setWindowSize({width:720,height:480}aswindow.Size);this.controllerctl;this.stateREADY;}}这里故意把controller赋值放在页面和尺寸都配置完成之后。若setUIContext()因路径未注册抛出 1300016半成品控制器不会被后续启动。窗口尺寸使用 px并应先通过getFloatViewLimits()校验Demo 的 720×480 px 落在当前圆角矩形模板限制内。prepare()可重复调用但同一时刻只有一个创建流程能进入真实工作区。三、把连续点击合并成一次飞行中的启动第二段代码是真正拦住 1300031 的地方。startOrReuse()不做固定时长节流而是复用飞行中的 Promise三次连续点击只产生一次系统调用。已经运行时再次点击直接返回停止中则明确拒绝避免start()与stop()交叉。privateonStateChange(info:floatView.FloatViewStateChangeInfo):void{constcallbackEpochthis.storage.getnumber(epoch)??-1;if(callbackEpoch!this.epoch){console.info([FloatCourier] stale callback drop epoch${callbackEpoch});return;}if(info.statefloatView.FloatViewState.STARTED){this.stateRUNNING;console.info([FloatCourier] FV-1012 STARTED 720x480 progress68);}elseif(info.statefloatView.FloatViewState.STOPPED){this.stateSTOPPED;console.info([FloatCourier] FV-1012 STOPPED reason${info.stopReason});}};asyncstartOrReuse():Promisevoid{if(this.stateRUNNING||this.stateSTARTING){returnthis.startFlight??Promise.resolve();}if(this.stateSTOPPING)thrownewError(FLOAT_VIEW_STOPPING);awaitthis.prepare();constctlthis.controller!;ctl.onStateChange(this.onStateChange);this.stateSTARTING;this.startFlightctl.start().finally((){this.startFlightundefined;});returnthis.startFlight;}这里还要区分“系统回调代次”和“业务序号”。epoch12标识控制器会话路线数据的seq37标识第 37 次业务更新。旧会话的回调即使晚到也只能被丢弃不能覆盖新会话。相反同一会话里的第 38 条路线更新应该被接受而不是因为 taskId 相同就被误判为重复。调试时我在 HiLog 同时打印 state、epoch、seq 和系统错误码。修复前一次三连点能看到两条 start 调用与一条 1300031修复后只剩一条 startduplicateStartBlocked3系统错误为 0。这个指标比“按钮置灰了”可靠因为自动化脚本、辅助功能或业务重试都可能绕过视觉上的置灰。四、停止不是把引用设成undefined第三段代码处理退出路径。停止前先进入STOPPING等待系统确认后注销监听再增加代次。若用户从标题栏关闭、拖进垃圾桶或主窗销毁stopReason会不同业务可以记录原因但不要对所有原因再次调用stop()。asyncstopAndRelease():Promisevoid{constctlthis.controller;if(!ctl)return;constoldEpochthis.epoch;if(this.stateRUNNING||this.stateSTARTING){this.stateSTOPPING;awaitctl.stop();}ctl.offStateChange(this.onStateChange);this.epocholdEpoch1;this.storage.setOrCreate(epoch,this.epoch);this.controllerundefined;this.stateSTOPPED;console.info([FloatCourier] release epoch12→13 listener0);}一个容易忽略的边界是stop()的 Promise 与STARTED一样也不应替代状态回调成为唯一事实来源。生产代码可以为STOPPED回调加超时兜底但超时后应进入UNKNOWN并重新探测不应直接假装停止成功。另一个边界是onStateChange()重复注册会触发 1300030所以回调必须保存为稳定的函数引用注销时传回同一个引用。如果应用还绑定了闪控球生命周期会更严格floatView.bind()要求两个控制器都未启动绑定后任一侧停止都会销毁两侧窗口。FloatCourier 这版没有绑定闪控球避免把两个状态机同时引入后续接入时会把bind/unbind也纳入同一个串行队列而不是在页面层散落调用。五、结果不是“没崩”而是状态可解释最终回归脚本执行了 30 轮“启动—快速三击—退后台—回前台—主动停止”。本轮记录是任务FV-1012会话代次 12业务序号 37窗口 720×480 px位置变化回调 4 次启动合并 3 次旧回调丢弃 1 次1300031 为 0。状态严格经过READY → STARTING → RUNNING → STOPPING → STOPPED停止原因为APP_STOP释放后监听计数为 0。手机页面保留了最有价值的信息路线、进度、预计时间、控制器状态和诊断计数。它不是为了证明 UI 能显示而是让测试人员在不接电脑时也能判断当前窗口属于哪一代会话以及某次点击到底被执行还是被合并。六、这套收口方法的适用边界会话代次适合解决“异步资源被销毁后仍有回调”的问题但不能代替权限和能力判断。闪控窗需要ohos.permission.FLOAT_VIEW且设备必须支持SystemCapability.Window.SessionManager不支持时应该回退到应用内悬浮组件或普通页面而不是循环重试。LocalStorage也只服务于当前 UIContent不保证进程被回收后的恢复。最值得保留的不是某个if而是三个判断Promise 完成不等于系统状态完成重复意图应该合并不应靠时间节流碰运气任何异步回调都必须携带可比较的会话代次。把这三点守住后闪控窗才从一个“能启动的小窗口”变成可维护的工程能力。参考资料HarmonyOSwindow.floatViewAPI 参考HarmonyOS 智慧多窗应用开发指导
返回列表