磁刻技术实录 · 第 26 篇 · 工程实践 · 技术基线 2026-10-31
我们正在做的「磁刻(Cike)」,是一款鸿蒙原生的生活记录与个性化创作应用。用户可以把照片做成数字冰箱贴,在洞洞板上自由摆放、组合自己的生活片段,也可以把喜欢的内容做成桌面卡片,用来回看、互动和分享。
在空间记录上,我们引入了 3D Gaussian Splatting(3DGS,三维高斯泼溅),把拍摄的视频重建为可以转动、靠近观察的三维场景。为此,我们重构了视频重建链路,实现自己的 C++ 训练核心,并结合鸿蒙原生的媒体处理、模型查看和分享能力,希望让用户保存一段经历时,也能留下当时的空间关系。
用户等待空间重建时,需要看到进度,也需要能够离开或停止任务。我们分享 N-API 长任务的组织方式,是为了讲清楚磁刻怎样连接 ArkTS 页面与 C++ 计算,以及异步结果、线程边界和任务状态如何共同保证界面响应。
一次端侧训练可能持续很久,ArkTS 页面不能同步等待 C++ 函数返回。把计算放入异步线程只是第一步,还需要定义参数校验、进度读取、取消、异常和最终完成的关系。磁刻通过 N-API Promise 为视频训练建立明确的任务协议。
以下代码按当前实现整理,保留关键运算与控制顺序;省略导入、日志和完整类型,部分辅助函数采用说明性命名,片段需结合上下文阅读。
调用方传入工作目录与输出 PLY 路径,原生层创建任务对象和异步工作。训练在工作线程执行,完成回调回到可以操作 N-API 返回值的环境,构造结果或拒绝 Promise。页面因此能够等待最终结果,同时继续响应交互。
图 1:工作线程负责计算,完成回调负责语言边界
参数检查保护跨语言边界
ArkTS 类型声明不能替代原生检查。调用参数可能缺失、类型不对、字符串过长或包含嵌入空字符。当前桥接读取字符串长度,限制大小,并拒绝内部空字符,避免传到以零结尾字符串为基础的文件接口后发生语义截断。
任务还会检查路径形式、输出后缀和已有文件。桥接层负责调用协议,任务层负责计算与产物约束,两者有不同检查范围。错误应尽早以可理解信息返回,而不是留给深层文件操作产生模糊失败。
工作线程不直接操作页面对象
执行函数调用 VideoTrainingJob.Run,并捕获异常保存结果状态。它不创建 ArkTS 对象,也不尝试导航页面。完成回调读取成功标志和结果,再通过 N-API 构造 modelPath、gaussians、iterations、psnr 等字段。
这个分工减少线程相关错误。N-API 环境和 JS 对象的操作需要遵守对应线程与生命周期约束,不能因为手里有一个指针,就在任意后台线程调用语言接口。
并发限制需要在原生层成立
流程服务有 busy 状态,但原生入口仍维护活动任务锁。这样即使调用方误发第二次请求,或者出现多个页面入口,也不会启动两个争用全局资源的训练任务。完成时仅清理属于自身的活动指针,避免旧任务回调误清新状态。
任务用共享所有权保证执行期间存活。完成之后保留最近一次轻量进度记录,允许页面查询终态;大块训练器资源则在计算结束后释放。终态可观察不等于继续常驻全部训练内存。
Promise 的结束由原生完成回调负责
napi_value promise;
napi_create_promise(env, &job->deferred, &promise);
napi_create_async_work(env, nullptr, resource_name,
ExecuteTraining, CompleteTraining,
job, &job->work);
napi_queue_async_work(env, job->work);
return promise;
示例只展示成功路径,正式实现还必须检查每个 N-API 返回状态并释放创建到一半的资源。deferred 是之后兑现或拒绝 Promise 的句柄,job 则保存跨线程需要的任务数据。返回 Promise 只表示任务已经安排,不能立即把业务状态设为成功。
计算结果在完成回调中转换为 ArkTS 可用的对象,异常也在同一交付边界变成拒绝原因。不要在工作线程里任意创建 JavaScript 对象或访问页面状态;线程能运行 C++,不代表它拥有调用全部 N-API 的正确执行环境。
图 2:发起、观察和取消承担不同职责
进度与完成是不同通道
页面每 500 毫秒查询阶段和迭代数,映射到显示进度。initializing、training 和 saving 可以分别提供文案。迭代进度应保持单调,不应因为阶段切换重置成零,让用户误以为任务重新开始。
但观察到某个阶段不能替代等待 Promise。最终返回意味着保存逻辑已经完成、工作函数已退出,调用方才可以安全整理输入文件。进度快照只是观察,不能成为资源释放的依据。
异常必须在每条退出路径收口
创建异步工作失败、排队失败、计算异常和结果对象构造失败,都需要结束 Promise 并释放已经创建的资源。若某条分支只记录日志而不 resolve 或 reject,页面会永久等待。
验证时可以模拟不同阶段失败,确认每个请求只结算一次,异步工作被删除,活动任务标记恢复。再通过 App 内诊断调用正式入口,检查真实线程和产物协议。宿主模拟不能证明平台桥接实际工作,原生诊断也不能代替真实素材质量验收。
轮询进度要与任务代次一起判断
const run = this.generation;
const timer = setInterval(() => {
if (run !== this.generation) return;
const progress = getVideoTrainingProgress(workPath);
updateProgress(progress);
}, 500);
try {
const result = await trainVideoGaussians(workPath, outputPath);
if (run !== this.generation) throw new Error('任务已失效');
return result;
} finally {
clearInterval(timer);
}
这里省略了真实接口中的其他参数和轮询异常分支,重点是“轮询只读状态,Promise 交付终态”。如果读取进度失败,完整实现会请求停止原生任务并等待它结束;不能仅停止进度条,却让后台训练失去管理。
generation 防止旧任务的回调覆盖新页面或新任务。它不负责真正停止计算,所以仍要与原生取消协议配合。finally 清理定时器覆盖成功与失败两条路径,避免用户多次进入页面后积累多个轮询器,造成进度跳动和资源浪费。
这套协议可以推广到视频处理和其他端侧模型任务。稳定的异步接口应同时说明如何开始、如何观察、如何停止以及什么时候真正结束,而不只是返回一个 Promise 类型。