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[] 数组 |
MockDeviceDataSource(KitchenDeviceSimulator 内部) |
设备状态 + 定时器倒计时 |
activeCookingTasks Map |
CookingProgressManager |
烹饪任务绑定关系 |
kitchen_devices + user_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_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 - 数据一致性 :所有展示页面(
DeviceTabContent、CookingMonitorPage、服务卡片)都从CookingProgressManager或storeHelper读取数据,同一个数据源 - 多任务支持 :每个任务有独立的
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 排查过程
- 初期误判 :一度怀疑是跨进程
Preferences同步问题(EntryFormAbility写、EntryAbility读),但代码未做任何修改 - 关键日志 :
onNewWant和onAddForm均无日志输出,说明系统根本没有触发卡片事件 - 真相:模拟器环境问题------多次部署后应用沙箱与系统服务的绑定关系错乱
4.2 解决方案
最终通过清除模拟器数据 + 重启模拟器 + 重新部署解决了卡片推送问题。这也提醒我们:遇到间歇性、不可复现的问题时,优先检查运行环境而非代码逻辑。
五、经验总结
5.1 架构层面
- 单一数据源是并发安全的基石。多个数据源必然导致一致性维护困难,尤其在多任务场景下。
- 定时器应该由业务逻辑层自己管理 ,而非依赖模拟器或外部模块。基于时间戳的计算方式比
setInterval递减更可靠。 - 数据库持久化比内存缓存更适合需要恢复的场景(如 App 被杀后重启)。烹饪任务是临时状态,但绑定关系需要持久化以支持状态恢复。
5.2 调试层面
- 日志要精确到每次状态变更的上下文 。这次排查中,
timerSeconds从 300 突变为 0 的关键线索就是在日志中发现的。 - 区分代码问题和环境问题。当功能间歇性失效时,优先检查环境(模拟器缓存、签名、系统服务状态)。
- 重构要彻底,不要打补丁。多次"头痛医头"的修改浪费了大量时间,最终仍然是完全重构解决了问题。
5.3 最终成果
经过架构重构后,多任务并发烹饪功能运行稳定:
- 第一道菜烹饪中,启动第二道菜后,两道菜各自独立倒计时,互不干扰
- 服务卡片同时显示多道菜的进度
- 任一道菜定时结束后,只释放对应设备,不影响其他菜
KitchenDeviceManager(原 Simulator)只保留设备基础操作(连接/断开/温度/功率),不再管理任何定时器
📚 本系列持续更新中:下一篇将继续完善设备管理、烹饪监控等UI细节,并准备上架材料。
📦 获取基线版本源码包 :包括第1-15篇所有代码 + 架构文档 + Flask 后端
如果你发现本文还有任何不严谨之处,欢迎随时指出,我们一起共建最优质的 HarmonyOS 6.1 学习内容!如果觉得有帮助,请不要吝啬你的点赞 👍、收藏 ⭐ 和评论 💬,我们下一篇见~