HarmonyOS 6.1 开发者实战 | 《灵犀厨房》多设备烹饪并发:从反复踩坑到架构重构

HarmonyOS 6.1 开发者实战 | 《灵犀厨房》多设备烹饪并发:从反复踩坑到架构重构 HarmonyOS 6.1 开发者实战 | 《灵犀厨房》多设备烹饪并发从反复踩坑到架构重构摘要在《灵犀厨房》App的开发过程中我遇到了一个看似简单却反复折腾了数日的问题——多道菜同时烹饪时任务会被意外终止、服务卡片数据紊乱、设备状态不一致。这篇文章将完整复盘这次从头痛医头到全局重构的排查历程分享我在并发状态管理、定时器设计、服务卡片推送等方面的思考与最终方案。如果你也在做HarmonyOS应用的状态管理相信这篇文章能帮你少走一些弯路。一、问题现象当用户按以下流程操作时会出现严重的数据不一致选择第一道菜回锅肉绑定电磁炉01启动烹饪5分钟定时器正常运行返回首页选择第二道菜木须肉绑定电磁炉02确认启动预期行为两道菜各自独立倒计时互不干扰实际现象第一道菜的设备被错误释放is_busy变为 0第二道菜的设备也被随后释放烹饪记录丢失服务卡片被复位显示等待连接…控制台出现[CookingProgress] 检测到烹饪自然结束自动清理状态的误判日志从日志中可以看到问题发生的时间线非常清晰10:09:33.695 [CookingProgress] 开始烹饪: 木须肉, 初始时间: 300秒 10:09:33.695 [CookingProgress] 当前活跃任务数: 2 10:09:33.877 [CookingProgress] 检测到设备 DE:VI:CE:IH:CT:02 的烹饪自然结束 10:09:33.878 [CookingProgress] 停止设备 DE:VI:CE:IH:CT:02 的烹饪第二道菜从启动到被判定为自然结束仅用了 182 毫秒。二、根因分析三重数据源导致的真相分裂2.1 最初的架构问题这张图展示了重构前的三重数据源问题——CookingProgressManager既维护自己的任务列表又从KitchenDeviceSimulator读取定时器数据两个数据源之间没有同步机制。图例说明橙色节点存在数据不一致的两套数据源红色节点问题触发点——checkAllTasks()读取到不可靠的timerSeconds后误判虚线箭头不可靠的数据依赖关系在问题发生时App存在三套互相独立的数据源数据源维护者职责this.devices[]数组MockDeviceDataSourceKitchenDeviceSimulator 内部设备状态 定时器倒计时activeCookingTasksMapCookingProgressManager烹饪任务绑定关系kitchen_devicesuser_cooking_history表RelationalStoreHelper数据库持久化核心矛盾CookingProgressManager自己维护任务列表却从KitchenDeviceSimulator读取定时器数据。多任务并发时KitchenDeviceSimulator内部的setInterval对第二道菜的定时器初始化失败timerSeconds始终为 0导致CookingProgressManager读取到不可靠的数据误判为自然结束。2.2 为什么定时器初始化失败KitchenDeviceSimulator的startTimer方法中每台设备的定时器由独立的setInterval管理。但当多个定时器同时运行时this.devices.find()在回调中查找设备时出现了竞态问题——第二道菜的定时器启动时第一道菜的定时器回调正在执行this.devices.find()导致第二个定时器的设备状态被意外覆盖。2.3 反思我们在修改过程中犯的错误在长达数日的修改过程中我多次陷入打地鼠式的修复第一次尝试在CookingProgressManager.checkAllTasks()中增加启动保护期——新任务启动后 2 秒内不检测自然结束。这解决了误判问题但治标不治本。第二次尝试让CookingProgressManager自己计算剩余时间targetTimerSeconds - (Date.now() - startTime) / 1000不再依赖KitchenDeviceSimulator的定时器。这个方向是对的但第一次实现时没有彻底清理旧代码导致两套定时器逻辑并存。第三次尝试彻底删除KitchenDeviceSimulator中的定时器逻辑让CookingProgressManager成为唯一的定时器管理者。但此时又因为接口变更导致全量编译错误花了大量时间修复调用方。最深刻的教训重构时应该一次到位彻底消除数据源的不一致性而不是在旧架构上打补丁。三、最终方案以数据库为唯一数据源的自管理定时器3.1 设计原则单一数据源所有设备状态和烹饪记录以数据库kitchen_devicesuser_cooking_history为唯一真相来源定时器自管理CookingProgressManager使用MapdeviceId, setInterval句柄自己管理所有定时器剩余时间实时计算remainingSeconds targetTimerSeconds - (Date.now() - startTime) / 1000不依赖任何外部可变状态数据库与内存同步定时器归零时自动调用storeHelper.completeCookingRecord()storeHelper.setDeviceIdle()3.2 重构后的数据流RecipeDetailPage.confirmStartCooking() │ ├── 1. storeHelper.setDeviceBusy(deviceId, recipeId) // 更新设备忙碌状态 ├── 2. storeHelper.insertCookingRecord(...) // 写入烹饪记录 └── 3. cookingProgressManager.startTask(...) // 启动任务 │ └── activeTimers.set(deviceId, setInterval句柄) │ ├── 每秒计算剩余时间 → 服务卡片推送 └── 归零时clearInterval completeCookingRecord setDeviceIdle 所有展示页面DeviceCookingPage / CookingMonitorPage / DeviceTabContent └── 直接从 storeHelper 或 cookingProgressManager 查询不经过任何中间层如下这张图展示了重构后的完整数据流——所有状态以数据库为唯一数据源CookingProgressManager自管理定时器不依赖任何外部可变状态。图例说明核心改动定时器由CookingProgressManager自管理setInterval不再依赖KitchenDeviceSimulator数据一致性所有展示页面DeviceTabContent、CookingMonitorPage、服务卡片都从CookingProgressManager或storeHelper读取数据同一个数据源多任务支持每个任务有独立的startTime和定时器句柄互不干扰3.3 核心代码实现CookingProgressManager.startTask()asyncstartTask(recipeId:number,recipeName:string,deviceId:string,deviceName:string,totalSteps:number,targetTimerSeconds:number):Promisenumber{constuserIdauthViewModel.userId;awaitstoreHelper.setDeviceBusy(deviceId,recipeId);constrecordIdawaitstoreHelper.insertCookingRecord(userId,recipeId,recipeName,deviceId,deviceName,totalSteps,targetTimerSeconds);// 启动自管理定时器conststartTimeDate.now();consthandlesetInterval(async(){constelapsedMath.floor((Date.now()-startTime)/1000);if(elapsedtargetTimerSeconds){clearInterval(handle);this.activeTimers.delete(deviceId);awaitstoreHelper.completeCookingRecord(recordId);awaitstoreHelper.setDeviceIdle(deviceId);if(this.activeTimers.size0){this.stopPolling();}}},1000);this.activeTimers.set(deviceId,handle);this.startPolling();returnrecordId;}CookingProgressManager.getAllActiveSnapshots()asyncgetAllActiveSnapshots():PromiseCookingProgressSnapshot[]{constrecordsawaitstoreHelper.getActiveCookingRecords(authViewModel.userId);constsnapshots:CookingProgressSnapshot[][];for(constrecordofrecords){consttargetTimerSeconds(record[target_timer_seconds]asnumber)??0;conststartedAt(record[started_at]asnumber)??0;constelapsedMath.floor(Date.now()/1000)-startedAt;constremainingSecondsMath.max(0,targetTimerSeconds-elapsed);// ... 组装 snapshot计算步骤进度snapshots.push({...});}returnsnapshots;}四、服务卡片推送的独立问题排查在架构重构完成后服务卡片推送出现了独立的问题——EntryAbility读取到的卡片ID列表始终为空数据库原始字符串: []。4.1 排查过程初期误判一度怀疑是跨进程Preferences同步问题EntryFormAbility写、EntryAbility读但代码未做任何修改关键日志onNewWant和onAddForm均无日志输出说明系统根本没有触发卡片事件真相模拟器环境问题——多次部署后应用沙箱与系统服务的绑定关系错乱4.2 解决方案最终通过清除模拟器数据 重启模拟器 重新部署解决了卡片推送问题。这也提醒我们遇到间歇性、不可复现的问题时优先检查运行环境而非代码逻辑。五、经验总结5.1 架构层面单一数据源是并发安全的基石。多个数据源必然导致一致性维护困难尤其在多任务场景下。定时器应该由业务逻辑层自己管理而非依赖模拟器或外部模块。基于时间戳的计算方式比setInterval递减更可靠。数据库持久化比内存缓存更适合需要恢复的场景如 App 被杀后重启。烹饪任务是临时状态但绑定关系需要持久化以支持状态恢复。5.2 调试层面日志要精确到每次状态变更的上下文。这次排查中timerSeconds从 300 突变为 0 的关键线索就是在日志中发现的。区分代码问题和环境问题。当功能间歇性失效时优先检查环境模拟器缓存、签名、系统服务状态。重构要彻底不要打补丁。多次头痛医头的修改浪费了大量时间最终仍然是完全重构解决了问题。5.3 最终成果经过架构重构后多任务并发烹饪功能运行稳定第一道菜烹饪中启动第二道菜后两道菜各自独立倒计时互不干扰服务卡片同时显示多道菜的进度任一道菜定时结束后只释放对应设备不影响其他菜KitchenDeviceManager原 Simulator只保留设备基础操作连接/断开/温度/功率不再管理任何定时器 本系列持续更新中下一篇将继续完善设备管理、烹饪监控等UI细节并准备上架材料。专栏入口[《HarmonyOS6.1全场景实战》合集] 获取基线版本源码包包括第1-15篇所有代码 架构文档 Flask 后端如果你发现本文还有任何不严谨之处欢迎随时指出我们一起共建最优质的 HarmonyOS 6.1 学习内容如果觉得有帮助请不要吝啬你的点赞 、收藏 ⭐ 和评论 我们下一篇见