尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
HarmonyOS 7 QuickDock 闪控窗开发实录 05:floatView × Stage:重复创建、监听冲突与窗口资源治理【鸿蒙心迹】
04 结束时QuickDock 已经能处理两个任务、前后台切换和一次FLOAT_SURFACE_LOST恢复。看起来功能已经很完整我真正开始做长时间测试以后问题反而集中出现了。最明显的一次是连续显示 / 隐藏闪控窗十几轮再从闪控球恢复回来屏幕上明明只有一个窗口HiLog 里同一条进度却打印了两次。继续跑几轮以后同一条任务状态变化会触发三次 Adapter update。这类问题比“窗口打不开”更麻烦。因为用户看到的是正常 UI后台却已经悄悄积累重复 listener 重复 timer 失效 adapter 隐藏窗口实例 旧 ball 实例第五篇不再加新能力只解决资源归属。本轮固定数据taskId: float_20261002_05 activeJob: upload_release_02 progress: 76% cycles: 12 showCalls: 24 duplicateCreateBlocked: 11 floatWindowId: quickdock_float_01 ballId: quickdock_ball_01 销毁前: window1 ball0 listener1 timer1 adapter1 销毁后: window0 ball0 listener0 timer0 adapter0 disposeCost: 33ms status: RESOURCE_CLEAN一、屏幕上只有一个窗口不代表工程里只有一个资源04 的异常恢复让 Float View 可以重建。如果只看视觉结果旧窗口消失 新窗口出现很容易觉得资源已经替换完成。但真正需要检查的是旧 Window 对象 旧 Adapter 旧 Task listener 旧 UpdateScheduler timer 旧 Floating Ball有没有一起退出。我后来把 QuickDock 的显示层资源归成五类FLOAT_WINDOW FLOATING_BALL TASK_LISTENER UPDATE_TIMER ADAPTER任何一个遗漏都可能在后续切换里放大。所以 05 新增registry/ └── QuickDockResourceRegistry.ets不是为了做一个“万能资源管理器”而是给所有显示层资源一个统一可观测入口。二、谁创建资源谁负责注册第一条规则非常简单创建点和注册点必须在同一个 Owner 里。以前 FloatManager 创建 AdapterDisplayModeCoordinator 负责 listenerUpdateScheduler 又自己创建 timer。销毁时需要跨三个对象猜谁还活着。现在资源创建后立即登记exporttypeResourceTypeFLOAT_WINDOW|FLOATING_BALL|TASK_LISTENER|UPDATE_TIMER|ADAPTERexportclassQuickDockResourceRegistry{privateresources:MapResourceType,SetstringnewMap()register(type:ResourceType,id:string):void{letbucketthis.resources.get(type)if(bucketundefined){bucketnewSetstring()this.resources.set(type,bucket)}bucket.add(id)}unregister(type:ResourceType,id:string):void{this.resources.get(type)?.delete(id)}}这段代码不负责真正销毁系统资源。它只记录现在应该还有什么真正释放仍然由资源 Owner 完成。三、重复 show 必须在创建之前就被挡住本轮总共调用showCalls24其中很多来自主页面进入 从闪控球恢复 Surface 恢复 前台恢复 用户再次点显示如果每个入口都直接create()窗口实例一定会越来越多。所以QuickDockFloatManager的 create 逻辑现在有单实例守卫exportclassQuickDockFloatManager{privatecreated:booleanfalseprivatereadonlywindowId:stringquickdock_float_01asyncensureShown(snapshot:QuickTaskSnapshot):Promisevoid{if(this.created){awaitthis.update(snapshot)QuickDockMetrics.recordDuplicateBlocked()return}awaitthis.adapter.create(this.windowId)this.createdtrueQuickDockResourceRegistry.shared().register(FLOAT_WINDOW,this.windowId)awaitthis.adapter.show(snapshot)}}当前 12 轮压力测试里duplicateCreateBlocked11这不是 11 次失败。它说明 11 次“本来可能重复创建”的调用被正确降级成 update。四、listener 最容易被“异常恢复”悄悄复制窗口重建以后我曾经写过rebuild → bind listener却忘了旧 listener 是否已经解除。于是listenerCount 1 → 2页面仍然正常直到下一次 TaskStore 更新才暴露。所以 listener 现在也有显式 IDexportclassTaskSubscriptionOwner{privatereadonlylistenerId:stringquickdock_task_listenerprivatebound:booleanfalsebind():void{if(this.bound){return}FloatTaskStore.shared().on(this.listenerId,this.onSnapshot)QuickDockResourceRegistry.shared().register(TASK_LISTENER,this.listenerId)this.boundtrue}unbind():void{if(!this.bound){return}FloatTaskStore.shared().off(this.listenerId)QuickDockResourceRegistry.shared().unregister(TASK_LISTENER,this.listenerId)this.boundfalse}}这条写法的重点不是 ID 字符串而是bind / unbind 幂等同一个 Owner 重复 bind 不会多注册一份。五、Timer 不能因为 hide 就忘记QuickDock 的进度 UI 还有一个 250ms 合并 Timer。它属于显示更新调度而不是业务任务窗口隐藏以后如果 Scheduler 仍然留着 pending timer可能发生窗口已经 hide timer 到点 Adapter update轻则多一条错误日志重则访问已经 dispose 的 Adapter。所以 05 把 Timer Owner 也收进 RegistryexportclassUpdateScheduler{privatetimer:number-1schedule(callback:()void):void{if(this.timer0){return}this.timersetTimeout((){this.timer-1QuickDockResourceRegistry.shared().unregister(UPDATE_TIMER,progress_update)callback()},250)QuickDockResourceRegistry.shared().register(UPDATE_TIMER,progress_update)}cancel():void{if(this.timer0){return}clearTimeout(this.timer)this.timer-1QuickDockResourceRegistry.shared().unregister(UPDATE_TIMER,progress_update)}}现在 Timer 不再是“某个工具类内部看不见的状态”。六、窗口、球、Adapter 的释放顺序不能随便这一篇我把最终收口顺序固定成停止 UI Timer → 解除 Task listener → hide / dispose Floating Ball → hide / dispose Float Window → dispose Adapter → 清 Registry为什么 Adapter 最后因为 Window / Ball 的 hide 和 dispose 可能还需要 Adapter 提供系统调用。如果 Adapter 先销毁后面的释放动作就只能进入异常分支。所以最终disposeAll()asyncdisposeAll():Promisevoid{this.scheduler.cancel()this.subscriptionOwner.unbind()awaitthis.ballManager.dispose()awaitthis.floatManager.dispose()awaitthis.adapter.dispose()QuickDockResourceRegistry.shared().assertEmpty()}本轮销毁耗时33ms这只是当前测试基线。真正的验收是count0七、hide 和 dispose 必须继续保持两个语义第二篇为了闪控窗 / 闪控球切换已经明确hide ! dispose05 不能为了“释放彻底”把这条边界破坏掉。当前切换到 Floating Ball → Float Window hide → Float Window 仍登记为已创建资源 任务真正结束 / Ability 收口 → Float Window dispose → Registry unregister所以销毁前的资源计数window1 ball0 listener1 timer1 adapter1是合理状态。Ball 当前不显示所以ball0但 Float Window 仍然存在。只有最终 dispose 后才要求全部归零。八、资源 Registry 不是“看到 0 就完事”还要看归属如果所有资源都放进一个总数count4根本不知道是谁没释放。所以 Registry 必须按类型统计FLOAT_WINDOW: 1 FLOATING_BALL: 0 TASK_LISTENER: 1 UPDATE_TIMER: 1 ADAPTER: 1出现异常时日志直接能定位UPDATE_TIMER leak而不是只显示RESOURCE_LEAK这种粒度对窗口能力非常重要。九、Task 完成以后UI 资源必须自动进入销毁流程第五篇还测试upload_release_02 → COMPLETED任务进入终态以后显示完成状态 → 保留短时间结果 → cancel timer → unbind listener → dispose display resources不能等用户手工关应用才释放。Task Registry 可以保留任务历史但显示资源不应该跟着历史一直存在。这也是业务历史和活动 UI 资源的边界。十、前后台切换不会重复创建资源04 有onBackground onForeground如果每次 Foreground 都执行create Float Window连续 12 轮前后台切换一定会出问题。所以现在 Foreground 做Registry 查询 → Window 是否仍存在 → Adapter 是否可用 存在 → update Surface lost → RecoveryCoordinator rebuild 不存在 → create once创建、恢复和更新三个分支明确分开。十一、异常恢复后旧 Adapter 要进入 DISPOSEDFLOAT_SURFACE_LOST重建时我给 Adapter 自己也加状态CREATED ATTACHED LOST DISPOSING DISPOSED新的 Adapter 只有在旧 Adapter 进入 DISPOSED 后才登记为活动实例。这样adapterCount才能长期保持1而不是视觉上只有一份Registry 里却两份。十二、DevEco 图里只看“12 轮以后资源是否还是可解释的”开发图统一 HiLogtaskIdfloat_20261002_05 resource probe cycles12 showCalls24 duplicateCreateBlocked11 floatWindowId quickdock_float_01 ballId quickdock_ball_01 before dispose: window1 ball0 listener1 timer1 adapter1 dispose order: timer → listener → ball → window → adapter disposeCost33ms after dispose: all0 statusRESOURCE_CLEAN这比“没有崩溃”更接近工程验收。十三、运行图直接展示销毁前和销毁后最终运行图当前任务float_20261002_05 upload_release_02 76%压力数据cycles12 showCalls24 blocked11销毁前window 1 listener 1 timer 1 adapter 1销毁后全部 0最终RESOURCE_CLEAN这张图真正回答的是QuickDock 已经不仅会创建系统窗口也知道什么时候、按什么顺序把所有展示资源还回去。十四、为什么 05 还不直接做 25 轮最终回归因为资源治理刚刚完成。这一篇先把计数口径 归属规则 幂等规则 释放顺序定死。如果直接把 25 轮数据堆进来出现 leak 时还要反过来补 Registry很难定位。所以 05 只跑 12 轮资源压力测试专门验证资源模型。06 再把这个模型带进全场景回归。十五、资源 Registry 也不能拥有业务资源这里我再强调一个边界。Registry 管理的是Window Ball Listener Timer Adapter不管理Task Runner 上传任务 压缩任务 业务 Repository任务生命周期仍归 TaskRegistry。否则一次窗口 dispose 很容易顺手把业务任务也销毁回到第一篇就已经否定的错误模型。十六、生命周期冲突最终都要变成“可失败指标”第五篇给下一轮留下的验收指标已经明确duplicateWindow listenerLeak timerLeak adapterLeak ballLeak stateLoss这些都不能靠肉眼判断。如果第六篇 25 轮以后任何一个不是 0就不允许最终 PASS。这也是 05 结束时最重要的工程变化资源问题从“偶发感觉不对”变成了“有明确计数器”。十七、RESOURCE_CLEAN 的含义最终状态RESOURCE_CLEAN至少代表重复创建被阻止 单例窗口稳定 监听不会重复 Timer 可取消 Ball 可释放 Window 可释放 Adapter 最后释放 所有 Registry 计数归零下一篇不会再加新功能只会拿这套资源模型去跑完整 25 轮回归。十八、资源释放不能只发生在“任务正常结束”如果只在COMPLETED里调用disposeAll()工程看起来很干净但真正的异常路径还没覆盖。QuickDock 还可能从这些路径退出用户取消任务 UIAbility 销毁 Surface 丢失后恢复失败 应用主动退出 后台能力申请失败后回退主页面这些路径里任何一条忘记调用资源收口最后都会留下不一致状态。所以第五篇把资源回收触发点从“任务结果”提升成Display Session 结束只要当前这组 Float View / Ball 显示会话结束就必须进入同一套释放流程。业务任务是否保留历史是 TaskRegistry 的事。显示资源是否归零是 ResourceRegistry 的事。两者不再互相借生命周期。十九、disposeAll 本身也必须幂等资源治理里很容易出现另一个反直觉问题正常结束调用一次 disposeAll Ability destroy 又调用一次 disposeAll如果第二次释放已经销毁的 Window 或 Adapter可能反而产生新的异常。所以disposeAll()不只是“按顺序释放”还必须可以重复调用当前做法是每个资源 Owner 自己判断状态exportclassFloatOwner{privatestate:ACTIVE|DISPOSING|DISPOSEDACTIVEasyncdispose():Promisevoid{if(this.stateDISPOSED||this.stateDISPOSING){return}this.stateDISPOSINGtry{awaitthis.adapter.hide()awaitthis.adapter.dispose()}finally{this.stateDISPOSED}}}这样生命周期回调重复到达也不会把资源释放链重新执行一遍。二十、资源治理里最怕“Owner 不清楚”早期代码里最危险的一种写法是Page 创建 listener Manager 保存 listener Coordinator 负责 off每个人都知道一点但没有一个对象完整拥有生命周期。第五篇以后QuickDock 给每类资源都指定 OwnerFloat Window → QuickDockFloatManager Floating Ball → QuickDockBallManager Task Listener → TaskSubscriptionOwner Update Timer → UpdateScheduler Adapter → DisplayAdapterOwnerResourceRegistry 只做登记和验收不越权释放。这个规则看起来像普通代码整洁但对系统窗口尤其重要。因为系统资源通常是异步创建、异步销毁一旦 Owner 不明确就很容易出现“我以为别人会释放”。二十一、隐藏资源和活动资源要分开统计第五篇销毁前window1 ball0并不意味着 Floating Ball 从来没创建过。它只是当前不处于活动显示资源集合。所以我把资源计数分成created visible active disposed最终验收主要看active而不是历史创建次数。例如连续 12 轮切换以后Float Window historicalCreate1 Floating Ball historicalCreate1 active: window1 ball0这种状态是健康的。如果历史创建次数每轮都增加哪怕 active 只有 1也说明单实例守卫失效。二十二、重复创建被阻止以后日志不能刷成“错误”本轮duplicateCreateBlocked11这些其实是成功保护。所以日志级别不应该用 ERROR。QuickDock 现在区分INFO duplicate show converted to update WARN unexpected resource state ERROR resource cannot be disposed如果把 11 次保护行为全打成红色 ERROR最终日志会让人误以为系统非常不稳定。诊断日志本身也要表达正确语义。二十三、Surface Recovery 和 Resource Dispose 会竞争同一个 Adapter第四篇有恢复流程FLOAT_SURFACE_LOST → rebuild第五篇又有销毁流程dispose如果两者同时发生就可能出现Recovery 正在创建新 Adapter Dispose 正在销毁旧 Adapter甚至新 Adapter 刚注册就被 dispose。所以资源 Owner 增加一个会话序号displaySessionIdRecovery 只有在session still active时允许提交新 Adapter。如果会话已经进入 DISPOSINGrebuild result discard这条保护在普通测试里很难看到但一旦没有就会出现“应用退出时又闪一下窗口”的诡异现象。二十四、Timer 归零还不够pending Snapshot 也要清UpdateScheduler 即使timer0内部如果还保留pendingSnapshot下一次新的任务会话创建 Scheduler 时可能误用旧任务数据。所以 cancel 还需要timer-1 pendingnullResourceRegistry 统计资源数解决“有没有活动 Timer”Owner 自己还要保证内部缓存状态清空。资源治理不只是释放系统句柄也包括清掉跨会话不应该残留的内存状态。二十五、05 最后的 12 轮是“资源模型验收”不是性能跑分这 12 轮每轮都执行show hide switch show surface rebuild dispose目标不是得到更漂亮的耗时而是确认每个资源在每轮结束以后都回到可解释状态。最终稳定结果Float Window: 1 → 0 Listener: 1 → 0 Timer: 1 → 0 Adapter: 1 → 0 duplicate create: 被守卫拦截只要其中任何一项不能解释我宁可先停在第五篇也不会直接进入最终 25 轮回归。这也是 QuickDock 从“能跑”走向“能长期跑”的真正分界。二十六、资源诊断页也不能反过来持有业务对象为了看计数我做了一个 Resource Probe 页面。最开始这个页面直接持有 FloatManager 和 TaskStore结果测试页一打开反而多注册了一份监听。后来诊断页只读 Registry 的不可变快照windowCount ballCount listenerCount timerCount adapterCount它不创建资源也不拥有资源。这样测试工具本身不会污染被测试对象。这条约束很容易忽略监控代码如果改变了资源数量最终看到的“资源健康”就失去可信度。二十七、第五篇的结果要能被下一篇自动读取05 结束后我把这组基线固化成 Acceptance 前置条件duplicateCreateBlocked 可观察 disposeAll 幂等 Registry 支持快照 资源 Owner 可查询状态06 启动前会先跑一次resource preflight。只要窗口、listener、timer 或 adapter 在测试开始前就不是 0整轮回归直接停止。这样可以避免“带着上一轮残留资源进入下一轮测试”让最终 25 轮数据更可信。参考资料HarmonyOS 7 闪控窗开发指南https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/float-view-guideHarmonyOS 文档中心Window / Background Tasks Kithttps://developer.huawei.com/consumer/cn/doc/Stage 模型与 Ability 生命周期https://developer.huawei.com/consumer/cn/arkui/arkui-stage
RELATED

相关推荐

HarmonyOS 7 QuickDock 闪控窗开发实录 04:Background Tasks Kit × Stage:前后台任务切换与窗口异常恢复【鸿蒙心迹】

HarmonyOS 7 QuickDock 闪控窗开发实录 04:Background Tasks Kit × Stage:前后台任务切换与窗口异常恢复【鸿蒙心迹】

03 把 QuickDock 的拖动、侧边暂存和位置持久化做稳定以后,窗口这条线终于没有太多悬念。 第四篇开始处理真正复杂的业务状态: 应用进入后台 两个任务并存 不同任务后台策略不同 当前闪控窗切换展示任务 Float Surface 中途丢失 回前台后恢复这一篇我故意…

📅 2026/10/5 0:38:37
Chat SDK斜杠命令与模态框实战:构建支持/command指令与表单验证的交互

Chat SDK斜杠命令与模态框实战:构建支持/command指令与表单验证的交互

Chat SDK斜杠命令与模态框实战:构建支持/command指令与表单验证的交互 【免费下载链接】chat Universal chat layer for building bots and agents. 项目地址: https://gitcode.com/gh_mirrors/chat67/chat Chat SDK 是一个跨平台的聊天机器人开发工具包&…

📅 2026/10/5 0:38:37
30 seconds of code 实战:用 JavaScript 在指定区间生成随机数与随机整数

30 seconds of code 实战:用 JavaScript 在指定区间生成随机数与随机整数

教程文档 【免费下载链接】30-seconds-of-code Coding articles to level up your development skills 项目地址: https://gitcode.com/gh_mirrors/30/30-seconds-of-code 点击查看 免费下载 本篇指南以 30 seconds of code 仓库中的 random-number-or-integer-in-…

📅 2026/10/5 0:38:37
MORE NEWS

更多资讯

📰

Linux信号机制详解:从异步通知到sigaction实战与面试考点

写Linux系统编程,绕不开的一个坎就是信号。你程序跑得好好的,终端里按下CtrlC,进程就像被刀砍一样直接没了;有些老练的服务进程却完全不一样,收到终止信号以后会先停掉新请求、把任务队列收一收、记一笔日志&#xff0…

📰

Go语言实现OAuth2:从授权码到Token校验的完整工程实践

开始任何服务端项目之前,我总是先问自己一句:这套接口到底要暴露给谁用?如果是自家前端、自家App,那直接上 Session 或简单的 Token 就行;可一旦涉及到第三方应用接入、多端授权、甚至开放平台,OAuth2 就成…

📰

前后端分离项目实战:基于SpringBoot+Vue的厨艺交流平台开发部署

前后端分离厨艺交流平台系统做下来,我觉得最值得分享的还不是那一堆CRUD代码,而是这套从需求拆解到技术选型、再到前后端联调和最终部署的完整链路。先说清楚这个项目是什么:它本质上是一个以菜谱分享和用户互动为核心的社区型Web应用&#x…

📰

INT与gRPC网络遥测:精细化运维实战指南

简介:这份PDF文档面向HPC与下一代数据中心网络的运维工程师及架构设计人员,聚焦如何借助Network Telemetry技术打破“网络黑盒”,解决大规模复杂网络中流量精细可视、可控以及端到端秒级故障定位的难题。资源包共1个PDF文件,大小约…

📰

论文写作工具怎么选?生成型、检索型与学术规范一次讲透

这段时间又到了毕业季,后台收到不少私信,问的都是同一个问题:马上要交论文了,有没有能一键生成论文的工具?正好有人把千笔专业论文写作工具和学术猹这两个名字放在一起对比,我干脆把这事一次讲透。先说结论…

📰

Java数组全解析:基础用法、扩容原理、内存模型与面试避坑

有人问过我一个问题:都这个年头了,写“Java数组常见用法”还有人看吗?我的回答是:正因为常见,才真正值得往深里刨一刨。数组是Java里最容易被“以为会了”的基础知识点。增删改查谁都能写两行,可真到面试桌…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬