HarmonyOS 7 + Spatial Recon Kit-Core File Kit:3DGS 重建产物的原子发布与中断恢复【鸿蒙心迹】
作者:李游
一份 3DGS 重建结果从“算法已经算完”到“用户下一次还能打开”,中间隔着一段很容易被忽略的工程距离。空间重建管线关心输入、计算与输出,应用层还要处理目录、断点、校验、进程终止和最终可见性。只要在错误时机把半成品目录当成成品展示,重建成功率再高,用户看到的仍然是损坏场景。
本文围绕示例工程 SceneCommit 展开。页面名为 ReconArchivePage,演示任务为 RECON-0047:任务在 74% 时被中断,恢复后继续补齐 50 个分块,最后把 184 MB 产物发布为 scene_0047。这些数字用于说明状态与文图一致性,不是某台设备的性能实测。文中的 model.bin、preview.jpg 与 manifest.json 也是应用层归档约定,并非 Spatial Recon Kit 强制规定的输出格式。

一、真正危险的窗口在“完成”之后
重建页面通常有一个很顺眼的状态流:RECONSTRUCTING → READY。问题是,算法回调返回和产物可用并不是同一个瞬间。应用可能还在写预览图、刷新清单、同步文件、计算摘要或者搬移目录。若此时直接把任务标记为 READY,下一页就可能读到缺文件的目录。
SceneCommit 把中间状态拆成 RECONSTRUCTING → VERIFYING → COMMITTING → READY。只有最终目录改名成功,任务才进入 READY。任何中断都只会留下 .pending 目录,不会污染成品列表。
任务目录的约定如下:
- 临时目录:
recon_0047.pending - 最终目录:
scene_0047 - 分块总数:50
- 中断点:37/50,也就是界面显示的 74%
- 最终产物:184 MB
- 摘要短码:
c9a8...72e1
这里刻意不用“存在目录就算成功”的判断。目录存在只能说明创建动作发生过,不能证明分块齐全,也不能证明写入已经落盘,更不能证明清单与实际文件相符。
1. 状态必须能跨进程重建
页面里的 @State 适合驱动 UI,却不是恢复依据。进程被系统回收后,内存状态消失;真正能让任务续跑的是磁盘上的检查点。检查点至少需要任务 ID、总分块数、已完成分块、阶段、文件清单与更新时间。它不需要保存每一帧 UI 信息,但必须足以回答三个问题:现在写到哪里、还缺什么、这个目录能不能发布。
这段代码解决什么问题:定义一个可以落盘、可以校验、也可以向前兼容的检查点结构。
export type ReconStage =
'RECONSTRUCTING' | 'VERIFYING' | 'COMMITTING' | 'READY' | 'FAILED';
export interface ArtifactEntry {
name: string;
size: number;
sha256: string;
}
export interface ReconCheckpoint {
schemaVersion: 1;
taskId: 'RECON-0047' | string;
stage: ReconStage;
completedChunks: number;
totalChunks: number;
outputBytes: number;
files: ArtifactEntry[];
updatedAt: number;
}
export function progressOf(cp: ReconCheckpoint): number {
if (cp.totalChunks <= 0) return 0;
return Math.floor(cp.completedChunks * 100 / cp.totalChunks);
}
这里没有把进度单独当作可信字段,因为 74% 可以由 37/50 推导出来。减少冗余字段,能避免“分块数更新了、百分比没更新”的双写不一致。schemaVersion 也不是装饰,它给后续增加字段留下迁移入口。真实项目还应记录重建算法版本和产物格式版本,避免新版本误读旧目录。
易错点在于把 stage 当成唯一事实。COMMITTING 只表示流程走到提交阶段,并不证明改名已经完成;恢复时仍要观察 .pending 与最终目录谁存在,再结合清单做判断。
二、检查点要写成“旧的或新的”,不能写成“一半新的”
直接覆盖 manifest.json 很省代码,但崩溃窗口也最大:JSON 写到一半时进程终止,下一次启动连已完成分块都读不出来。更稳妥的办法是先写 manifest.json.tmp,同步到存储,再用同目录改名替换正式文件。这样读取者看到的是上一版完整清单或下一版完整清单,而不是半截字符串。
这段代码解决什么问题:用临时文件、fsyncSync 和同目录改名更新检查点,缩小断电或进程终止造成的清单损坏窗口。
import { fileIo } from '@kit.CoreFileKit';
export function saveCheckpointAtomically(
pendingDir: string,
checkpoint: ReconCheckpoint
): void {
const target = `${pendingDir}/manifest.json`;
const temp = `${pendingDir}/manifest.json.tmp`;
const payload = JSON.stringify(checkpoint);
const file = fileIo.openSync(
temp,
fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.TRUNC
);
try {
fileIo.writeSync(file.fd, payload);
fileIo.fsyncSync(file.fd);
} finally {
fileIo.closeSync(file);
}
fileIo.renameSync(temp, target);
}
先写临时文件的目的,是让正式清单始终保持可解析状态;fsyncSync 用来明确文件内容同步点;finally 保证句柄成对关闭。TRUNC 很关键,没有它时短的新内容可能残留旧尾巴。实际项目需要确认目标文件系统对同目录改名的语义,并处理目标已存在时的替换策略;不同 API 版本的覆盖行为应以当前 SDK 文档和真机验证为准。
这段同步 I/O 不应运行在频繁刷新的 UI 主路径上。示例只在分块提交完成后写检查点;若分块很密,可以节流为每 N 个分块或每个稳定阶段写一次。写得越频繁,恢复粒度越细,但闪存写放大和调度开销也越高。
三、发布前校验的是“契约”,不是一个文件名
任务 RECON-0047 在 74% 中断时,model.bin 已经存在。如果恢复逻辑只检查这个文件,它会把 37 个分块误判为完整产物。SceneCommit 的校验顺序是:先校验清单结构,再确认 completedChunks === totalChunks,再逐项比对大小和摘要,最后才允许进入 COMMITTING。
摘要计算被封装在 HashService 中。它可以由经过验证的原生模块或平台可用的加密能力实现;本文不虚构一个 Spatial Recon Kit 的哈希接口。重点是调用边界:摘要结果必须与清单比较,失败时保持 pending,不要边校验边发布。
这段代码解决什么问题:把分块完整性、文件大小和摘要检查收敛为一个明确的发布门槛。
export interface HashService {
sha256File(path: string): Promise<string>;
}
export async function verifyArtifacts(
pendingDir: string,
cp: ReconCheckpoint,
hash: HashService
): Promise<void> {
if (cp.completedChunks !== cp.totalChunks) {
throw new Error(`CHUNKS_INCOMPLETE:${cp.completedChunks}/${cp.totalChunks}`);
}
for (const entry of cp.files) {
const path = `${pendingDir}/${entry.name}`;
const stat = fileIo.statSync(path);
if (stat.size !== entry.size) {
throw new Error(`SIZE_MISMATCH:${entry.name}`);
}
const actual = await hash.sha256File(path);
if (actual !== entry.sha256) {
throw new Error(`HASH_MISMATCH:${entry.name}`);
}
}
}
状态变化必须发生在验证边界之外:进入函数前写入 VERIFYING,函数全部成功后才能写 COMMITTING。任何异常都回到可恢复状态,并把错误码记录到诊断日志。不要在第一个文件校验通过后就更新局部 READY,否则列表页和详情页会得到不同结论。
对 184 MB 产物做全量摘要会消耗时间与电量。工程上可以在分块写入时增量记录摘要,最终只验证清单与尾部;也可以只对关键文件做强校验、对可再生预览图做大小检查。取舍的依据不是“快不快”,而是文件损坏后的代价:不可重算的几何数据应更严格,可重建缓存可以更宽松。

图中的 DevEco Studio 画面是与本文字段一致的演示配图,不是一次真实 IDE 执行截图。左侧工程目录把检查点、校验与恢复拆开;中间标出 fsyncSync 与 renameSync;右侧模拟器停在 VERIFYING 74%;底部 HiLog 使用任务 RECON-0047 和 chunks=37/50。它要表达的是可观察性设计,而不是宣称某台设备已跑出这些数据。
四、恢复扫描不能把所有 pending 都一股脑续跑
应用启动后发现 .pending 目录,只能说明上次流程没有正常收尾。它可能是可续跑任务,也可能是格式过旧、清单损坏、最终目录已经存在,甚至是另一个进程留下的孤儿目录。恢复扫描应该先分类,再决定续跑、补提交、隔离还是清理。
SceneCommit 采用四条规则:
- 最终目录存在:不再续跑 pending,先比较任务 ID 和清单版本。
- 清单不可解析:移动到隔离区,避免自动删除造成数据损失。
- 分块不足:从
completedChunks继续,示例为 37/50。 - 分块齐全且校验通过:跳过重建,直接补做原子提交。
这段代码解决什么问题:在启动恢复时区分“继续计算”和“补做提交”,并保证只有最终改名成功才进入 READY。
export async function recoverOrCommit(
root: string,
cp: ReconCheckpoint,
hash: HashService,
resume: (fromChunk: number) => Promise<ReconCheckpoint>
): Promise<ReconCheckpoint> {
const pending = `${root}/recon_0047.pending`;
const finalDir = `${root}/scene_0047`;
let current = cp;
if (current.completedChunks < current.totalChunks) {
current = await resume(current.completedChunks); // 37 -> 50
saveCheckpointAtomically(pending, current);
}
current.stage = 'VERIFYING';
saveCheckpointAtomically(pending, current);
await verifyArtifacts(pending, current, hash);
current.stage = 'COMMITTING';
saveCheckpointAtomically(pending, current);
fileIo.renameSync(pending, finalDir);
return { ...current, stage: 'READY' };
}
为什么 READY 没有再写回 pending?因为目录改名后,pending 已不存在。实际实现应在最终目录内写一份 ready.marker 或更新清单,再让索引器读取;也可以把 READY 存在独立任务数据库中。关键是确定单一事实源,避免目录名说 READY、清单仍是 COMMITTING 的歧义。
函数里的 resume 是应用对空间重建管线的适配层。它接收恢复位置并返回新的检查点,不代表 Spatial Recon Kit 原生接口就接受 fromChunk。若底层能力不支持分块续算,应用仍可保留已完成输入与中间缓存,但必须从官方支持的边界重新启动重建,不能假装有不存在的断点参数。
五、把 74% 中断变成可解释的用户状态
恢复体验不该只是一个旋转进度条。用户需要知道系统是在继续计算、验证旧产物,还是整理目录。示例运行页把状态、任务 ID、分块数、产物大小和摘要短码放在同一屏,并把“恢复点 74%”作为来源说明。

页面显示 READY、100%、50/50、184 MB 与 c9a8...72e1。这些字段都能从检查点或校验结果推导,不使用随手拼接的 UI 文案。红色标注只指出“从 74% 恢复”这一工程事实,避免把整张图标成故障海报。
更细的诊断页则按事件顺序展示:检测到 pending、读取 manifest、确认 37/50、恢复分块、校验通过、原子提交、孤儿文件为 0。这样遇到“明明 100% 却打不开”的反馈时,开发者能判断任务是否真的跨过提交边界。

诊断页的 recon_0047.pending → scene_0047 是本方案最重要的一条线。箭头左边可以失败、暂停、重试;箭头右边才允许进入成品索引。若业务需要取消任务,取消动作也只清理 pending,并在关闭重建回调、文件句柄和后台任务之后执行。资源释放必须成对:注册回调就要注销,打开文件就要关闭,创建任务就要有取消与完成路径。
六、哪些结论能直接采用,哪些必须真机验证
可以直接采用的是工程原则:半成品与成品分目录、检查点原子更新、发布前校验、最终提交作为可见性边界、恢复扫描先分类。它们不依赖具体 3DGS 算法实现。
必须在目标设备和当前 SDK 上验证的,是文件系统细节与性能成本:同目录改名是否满足预期覆盖语义,fsyncSync 在目标存储上的耗时,184 MB 全量摘要是否影响温升,以及空间重建输出什么时候真正关闭写入。官方 API 文档是接口依据,真机故障注入才是行为依据。
建议至少做四类故障注入:写清单前终止、写清单后终止、校验中终止、改名前后分别终止。每次重启后检查三件事:成品列表是否只出现完整目录,恢复点是否单调前进,孤儿文件是否为 0。若底层重建不支持续算,也要验证“重新开始但不误发布旧目录”的退化路径。
本文参考的官方一手资料包括 Spatial Recon Kit 的空间重建管线指南与 HarmonyOS Core File Kit 文件管理 API:
- https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/spatial-recon-c-spatial-recon-pipeline
- https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-file-fs
七、收束:把 READY 留给真正可打开的场景
3DGS 端侧重建的可靠性,不只发生在相机、点云和渲染器里,也发生在最后几十毫秒的文件提交中。SceneCommit 的做法并不神秘:让 pending 可以被打断,让 manifest 可以被替换,让 verify 有明确门槛,让 rename 成为唯一发布动作。
当任务 RECON-0047 从 37/50 恢复到 50/50 时,UI 的 100% 只是计算完成;当 recon_0047.pending 原子切换为 scene_0047,READY 才真正成立。把这两个时刻分开,能消除大量“偶发损坏”与“重启后消失”的模糊问题,也让后续诊断有据可查。
更多推荐




所有评论(0)