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

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

摘要:在《灵犀厨房》App的开发过程中,我遇到了一个看似简单却反复折腾了数日的问题------多道菜同时烹饪时,任务会被意外终止、服务卡片数据紊乱、设备状态不一致。这篇文章将完整复盘这次从"头痛医头"到"全局重构"的排查历程,分享我在并发状态管理、定时器设计、服务卡片推送等方面的思考与最终方案。如果你也在做HarmonyOS应用的状态管理,相信这篇文章能帮你少走一些弯路。


一、问题现象

当用户按以下流程操作时,会出现严重的数据不一致:

  1. 选择第一道菜(回锅肉),绑定电磁炉01,启动烹饪(5分钟定时器正常运行)
  2. 返回首页,选择第二道菜(木须肉),绑定电磁炉02,确认启动
  3. 预期行为:两道菜各自独立倒计时,互不干扰
  4. 实际现象
    • 第一道菜的设备被错误释放,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[] 数组 MockDeviceDataSource(KitchenDeviceSimulator 内部) 设备状态 + 定时器倒计时
activeCookingTasks Map CookingProgressManager 烹饪任务绑定关系
kitchen_devices + user_cooking_history RelationalStoreHelper 数据库持久化

核心矛盾CookingProgressManager 自己维护任务列表,却从 KitchenDeviceSimulator 读取定时器数据。多任务并发时,KitchenDeviceSimulator 内部的 setInterval 对第二道菜的定时器初始化失败(timerSeconds 始终为 0),导致 CookingProgressManager 读取到不可靠的数据,误判为"自然结束"。

2.2 为什么定时器初始化失败?

KitchenDeviceSimulatorstartTimer 方法中,每台设备的定时器由独立的 setInterval 管理。但当多个定时器同时运行时,this.devices.find() 在回调中查找设备时出现了竞态问题------第二道菜的定时器启动时,第一道菜的定时器回调正在执行 this.devices.find(),导致第二个定时器的设备状态被意外覆盖。

2.3 反思:我们在修改过程中犯的错误

在长达数日的修改过程中,我多次陷入"打地鼠"式的修复:

  1. 第一次尝试 :在 CookingProgressManager.checkAllTasks() 中增加"启动保护期"------新任务启动后 2 秒内不检测"自然结束"。这解决了误判问题,但治标不治本。
  2. 第二次尝试 :让 CookingProgressManager 自己计算剩余时间(targetTimerSeconds - (Date.now() - startTime) / 1000),不再依赖 KitchenDeviceSimulator 的定时器。这个方向是对的,但第一次实现时没有彻底清理旧代码,导致两套定时器逻辑并存。
  3. 第三次尝试 :彻底删除 KitchenDeviceSimulator 中的定时器逻辑,让 CookingProgressManager 成为唯一的定时器管理者。但此时又因为接口变更导致全量编译错误,花了大量时间修复调用方。

最深刻的教训:重构时应该一次到位,彻底消除数据源的不一致性,而不是在旧架构上打补丁。


三、最终方案:以数据库为唯一数据源的自管理定时器

3.1 设计原则

  • 单一数据源 :所有设备状态和烹饪记录以数据库(kitchen_devices + user_cooking_history)为唯一真相来源
  • 定时器自管理CookingProgressManager 使用 Map<deviceId, 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
  • 数据一致性 :所有展示页面(DeviceTabContentCookingMonitorPage、服务卡片)都从 CookingProgressManagerstoreHelper 读取数据,同一个数据源
  • 多任务支持 :每个任务有独立的 startTime 和定时器句柄,互不干扰

3.3 核心代码实现

CookingProgressManager.startTask()

typescript 复制代码
async startTask(
  recipeId: number, recipeName: string, deviceId: string,
  deviceName: string, totalSteps: number, targetTimerSeconds: number
): Promise<number> {
  const userId = authViewModel.userId;
  await storeHelper.setDeviceBusy(deviceId, recipeId);
  const recordId = await storeHelper.insertCookingRecord(
    userId, recipeId, recipeName, deviceId, deviceName, totalSteps, targetTimerSeconds
  );

  // 启动自管理定时器
  const startTime = Date.now();
  const handle = setInterval(async () => {
    const elapsed = Math.floor((Date.now() - startTime) / 1000);
    if (elapsed >= targetTimerSeconds) {
      clearInterval(handle);
      this.activeTimers.delete(deviceId);
      await storeHelper.completeCookingRecord(recordId);
      await storeHelper.setDeviceIdle(deviceId);
      if (this.activeTimers.size === 0) {
        this.stopPolling();
      }
    }
  }, 1000);
  this.activeTimers.set(deviceId, handle);

  this.startPolling();
  return recordId;
}

CookingProgressManager.getAllActiveSnapshots()

typescript 复制代码
async getAllActiveSnapshots(): Promise<CookingProgressSnapshot[]> {
  const records = await storeHelper.getActiveCookingRecords(authViewModel.userId);
  const snapshots: CookingProgressSnapshot[] = [];

  for (const record of records) {
    const targetTimerSeconds = (record['target_timer_seconds'] as number) ?? 0;
    const startedAt = (record['started_at'] as number) ?? 0;
    const elapsed = Math.floor(Date.now() / 1000) - startedAt;
    const remainingSeconds = Math.max(0, targetTimerSeconds - elapsed);

    // ... 组装 snapshot,计算步骤进度
    snapshots.push({ ... });
  }
  return snapshots;
}

四、服务卡片推送的独立问题排查

在架构重构完成后,服务卡片推送出现了独立的问题------EntryAbility 读取到的卡片ID列表始终为空(数据库原始字符串: [])。

4.1 排查过程

  1. 初期误判 :一度怀疑是跨进程 Preferences 同步问题(EntryFormAbility 写、EntryAbility 读),但代码未做任何修改
  2. 关键日志onNewWantonAddForm 均无日志输出,说明系统根本没有触发卡片事件
  3. 真相:模拟器环境问题------多次部署后应用沙箱与系统服务的绑定关系错乱

4.2 解决方案

最终通过清除模拟器数据 + 重启模拟器 + 重新部署解决了卡片推送问题。这也提醒我们:遇到间歇性、不可复现的问题时,优先检查运行环境而非代码逻辑。


五、经验总结

5.1 架构层面

  1. 单一数据源是并发安全的基石。多个数据源必然导致一致性维护困难,尤其在多任务场景下。
  2. 定时器应该由业务逻辑层自己管理 ,而非依赖模拟器或外部模块。基于时间戳的计算方式比 setInterval 递减更可靠。
  3. 数据库持久化比内存缓存更适合需要恢复的场景(如 App 被杀后重启)。烹饪任务是临时状态,但绑定关系需要持久化以支持状态恢复。

5.2 调试层面

  1. 日志要精确到每次状态变更的上下文 。这次排查中,timerSeconds 从 300 突变为 0 的关键线索就是在日志中发现的。
  2. 区分代码问题和环境问题。当功能间歇性失效时,优先检查环境(模拟器缓存、签名、系统服务状态)。
  3. 重构要彻底,不要打补丁。多次"头痛医头"的修改浪费了大量时间,最终仍然是完全重构解决了问题。

5.3 最终成果

经过架构重构后,多任务并发烹饪功能运行稳定:

  • 第一道菜烹饪中,启动第二道菜后,两道菜各自独立倒计时,互不干扰
  • 服务卡片同时显示多道菜的进度
  • 任一道菜定时结束后,只释放对应设备,不影响其他菜
  • KitchenDeviceManager(原 Simulator)只保留设备基础操作(连接/断开/温度/功率),不再管理任何定时器

📚 本系列持续更新中:下一篇将继续完善设备管理、烹饪监控等UI细节,并准备上架材料。

🔗 专栏入口《HarmonyOS6.1全场景实战》合集

📦 获取基线版本源码包包括第1-15篇所有代码 + 架构文档 + Flask 后端

如果你发现本文还有任何不严谨之处,欢迎随时指出,我们一起共建最优质的 HarmonyOS 6.1 学习内容!如果觉得有帮助,请不要吝啬你的点赞 👍、收藏 ⭐ 和评论 💬,我们下一篇见~

相关推荐
若兰幽竹9 小时前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(番外篇):【打包上架】三模块一体化工程的 Release 包构建与元服务独立分发
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹9 小时前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(三十三):权限管理——用一套“安检系统”告别散装代码
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹1 天前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(三十二):【数据一致性】个人档案的“三重持久化”修复——让偏好、健康与头像真正同步
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹2 天前
HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(三十):【社区分享】本地社区功能——让菜谱从“独享”走向“共享”
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹2 天前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十九):【偏好持久化】偏好设置与推荐引擎联动——让 App 越用越“懂你”
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹5 天前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十七):告别 UI 冻结——使用 TaskPool 实现高性能并发图像分析
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹5 天前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十八):【数据持久化】收藏与浏览历史——让数据在 App 重启后依然“活着”
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹6 天前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十六):【响应式布局】折叠屏与平板完美适配——一套代码,多端呈现
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹7 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(二十五):【深色模式】一键切换暗色主题——让 App 在深夜也温柔
华为鸿蒙系统·灵犀厨房·harmonyos6.1