磁刻技术实录 · 第 27 篇 · 工程实践 · 技术基线 2026-10-31
我们正在做的「磁刻(Cike)」,是一款鸿蒙原生的生活记录与个性化创作应用。用户可以把照片做成数字冰箱贴,在洞洞板上自由摆放、组合自己的生活片段,也可以把喜欢的内容做成桌面卡片,用来回看、互动和分享。
在空间记录上,我们引入了 3D Gaussian Splatting(3DGS,三维高斯泼溅),把拍摄的视频重建为可以转动、靠近观察的三维场景。为此,我们重构了视频重建链路,实现自己的 C++ 训练核心,并结合鸿蒙原生的媒体处理、模型查看和分享能力,希望让用户保存一段经历时,也能留下当时的空间关系。
空间重建可能持续较长时间,取消必须是一个可靠的产品操作。我们分享合作式停止的处理,是因为按钮状态改变以后,底层计算未必已经退出。只有明确资源何时不再被使用,磁刻才能安全清理输入并准备下一次任务。
取消按钮看起来只有一个点击事件,背后却涉及多个执行环境:ArkTS 页面、解码 Worker、原生相机恢复和训练线程。磁刻的重建流程把取消定义为停止请求,并等待任务实际退出后再清理输入。这能避免用户退出页面后,后台仍在读取已删除文件。
以下代码按当前实现整理,保留关键运算与控制顺序;省略导入、日志和完整类型,部分辅助函数采用说明性命名,片段需结合上下文阅读。
长任务的取消通常是协作式的。工作线程在可检查位置读取标志并退出,而不是被任意强杀。这样可以执行必要清理,但响应时间取决于检查点之间的工作量。描述取消性能时,应说明测试阶段和样本,而不是承诺所有情况立即完成。
图 1:先停止使用资源,再释放资源
任务代次阻止晚到结果污染页面
服务为每次重建维护 generation。取消时递增代次,后续异步返回检查自己是否仍属于当前任务。即使某个媒体调用无法立刻中断,晚到结果也不能继续触发训练、保存或页面跳转。
代次检查保护宿主流程,但不等于后台资源已经释放。因此它需要与原生停止请求配合。仅仅忽略旧结果,可能仍然留下持续运行的训练和大量内存占用。
输入文件由最后使用者决定清理时机
相机恢复和训练都读取 RGB 文件。超时后服务先调用原生取消,再等待 Promise 结束,最后进入统一清理。若用超时 Promise 竞争直接跳出外层,原生任务可能仍在读文件,finally 却已经删除它们。
可以把安全顺序概括为:请求取消、等待结算、清理输入。Worker 的媒体资源也应释放提取器和文件,并通过明确消息报告结束或错误。不同执行环境需要自己的停止协议。
超时与取消共享机制,保留不同原因
相机恢复按帧预算设置有界超时,训练则有独立上限。超时通过相同原生取消接口结束计算,但页面应该显示超时原因,而不是误报用户取消。当前服务保存超时标记,在原生返回后优先解释真正的停止原因。
进度查询失败也可能触发停止,以避免页面持续显示不可信状态。这时错误文案应指向进度读取问题,不能改写成训练数学失败。保持原因分类有助于后续统计和排错。
超时先请求取消,随后等待原生真正退出
const timeout = setTimeout(() => {
timedOut = true;
cancelVideoCameraRecovery(workPath);
}, timeoutMs);
try {
await recoverVideoCameras(workPath, times);
if (timedOut) throw new Error('相机恢复超时');
} finally {
clearTimeout(timeout);
// 外层在此 Promise 已结束后,才清理 RGB 输入。
}
超时回调只是发出请求。C++ 可能还在读取图片、匹配特征或完成一个不可中断的计算单元。如果使用 Promise.race 让业务先行返回,然后立即删除工作目录,就可能把正常取消变成文件读取失败,甚至触发更深层的不一致。
等待原生 Promise 结束,会让取消不是瞬间完成,但资源关系更加清楚。UI 可以先显示“正在停止”,表达请求已接收,而不是宣布工作已经结束。等底层确认退出后,再删除它拥有的输入和临时资源。
图 2:相同的停止机制可以对应不同用户语义
保存阶段需要定义竞争规则
取消可能恰好发生在模型已写出、尚未正式提交时。原生任务通过单一生命周期原子状态协调取消与完成:提交前接受的取消可以撤销本次输出,完成状态已经提交后,成功结果占优。
这种规则让用户和上层流程有一致结果。否则可能同时出现“已取消”与一个被加入列表的模型,或者成功提示后文件又被异步清理。产物提交点必须与任务终态绑定。
计费也要遵守所有权交接
在重建阶段,页面负责失败或取消后的收口;完整模型交给后台持久化后,责任转移给协调流程。页面退出不能再重复退款。当前代码明确区分是否已经交接,避免一个 chargeId 同时被两个路径结束。
验证应覆盖每个阶段退出、超时、取消与成功同时发生、晚到计费结果以及重新发起任务。检查不跳转旧页面、不删除已有产物、不残留临时文件,并记录真实任务停止时间。
原生侧在安全边界观察取消标志
void Cancel() {
Lifecycle expected = Lifecycle::Active;
lifecycle.compare_exchange_strong(expected, Lifecycle::CancelRequested);
}
for (int iteration = 0; iteration < iterations; ++iteration) {
if (CancellationRequested()) return CancelledResult();
TrainOneIteration();
if (CancellationRequested()) return CancelledResult();
PublishProgress(iteration);
}
原子状态使取消与完成可以明确竞争。取消只允许从活动状态进入,已经完成的任务不会因迟到的点击重新变成取消。示例以迭代为观察点,真实实现还会在保存、数据遍历等安全位置检查;检查频率决定响应速度的一部分。
不能随意强杀正在修改共享结构的线程。合作式退出允许当前单元完成,再统一清理内存与文件。验证取消时应覆盖抽帧、相机恢复、训练与保存多个阶段,特别是保存完成与取消同时发生的边界,而不是只测试进度刚开始时的一次点击。
取消设计体现了应用对用户时间与设备资源的尊重。按钮只是入口,真正的能力是让跨线程工作在明确边界内结束,并把结果、资源和业务状态一起收口。