ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HarmonyOS 7 Spatial Recon Kit + Preferences:重建会话中断恢复与脏任务回收【鸿蒙心迹】

HarmonyOS 7 Spatial Recon Kit + Preferences:重建会话中断恢复与脏任务回收【鸿蒙心迹】 端侧重建最麻烦的并不是把任务跑起来而是跑到一半接电话、锁屏或被系统回收以后应用还能不能解释清楚刚才采到哪里、哪些文件有效、旧任务是否已经释放以及用户下一步应该继续还是重来。我最近给一个室内陈列采集 Demo 加“继续上次任务”时第一次实现看起来很顺页面把进度写进 Preferences重新进入后再把进度读出来进度条也能回到 64%。真正运行才发现这种恢复只是把 UI 演明白了。底层重建句柄已经失效帧目录里还混着未写完的文件再点击继续页面会同时收到旧回调和新回调。这类问题很容易被误判成 Spatial Recon Kit 的调用失败。实际拆开看它是三层状态没有对齐原生流水线有自己的生命周期磁盘上的采集素材有自己的完整性ArkUI 页面又有一套可观察状态。只保存一个progress恰好绕开了最重要的部分。本文使用的 Demo 叫ScanResume Lab。固定任务为recon_20261001_09目标采集 600 帧应用进入后台时已经确认 384 帧恢复页面显示 64%。最终输出文件为livingroom_20261001_09.splat。这里不假设原生句柄可以跨进程复用而是把恢复定义为核验检查点、清理脏任务、重新创建流水线再从可复用输入继续。一、恢复的不是句柄而是一份可验证的事实重建任务运行时通常会同时存在四类信息任务标识、采集素材、流水线实例和页面状态。前两类可以落盘原生实例与回调对象只能活在当前进程里页面状态则随组件销毁而结束。我最初把nativeHandle转成字符串写入本地后来很快删掉了。句柄值只是当前进程里的索引重新启动以后即使数字相同也不代表它仍指向原来的资源。正式项目里把这种值当恢复依据轻则调用返回无效参数重则让清理逻辑释放错误对象。因此检查点只记录可以复核的数据任务 ID、素材目录、已确认帧数、最后一帧摘要、配置版本和更新时间。状态机也不直接从CHECKPOINTED跳到RUNNING中间必须经过RESTORING与REBUILDING。下面这段定义解决的是“页面进度和真实任务混成一个字段”的问题。持久化结构不保存任何 native 对象只保存重新构建流水线所需的最小事实。exportenumReconState{IDLEIDLE,CAPTURINGCAPTURING,CHECKPOINTEDCHECKPOINTED,RESTORINGRESTORING,REBUILDINGREBUILDING,COMPLETEDCOMPLETED,FAILEDFAILED}exportinterfaceReconCheckpoint{schemaVersion:2taskId:stringframeDir:stringconfirmedFrames:numbertargetFrames:numberlastFrameSha256:stringstate:ReconState updatedAt:number}confirmedFrames不是“相机回调过多少次”而是素材写入成功、文件长度满足要求并进入索引的数量。当前 Demo 在第 384 帧完成确认后才生成检查点所以恢复时显示384 / 600和 64%。如果第 385 帧只写了一半它不会被计入启动核验时还会被清理。schemaVersion也很重要。重建参数、目录组织或摘要算法调整以后旧检查点未必还能使用。正式产品不应该为了“尽量恢复”而硬读未知版本更稳妥的做法是保留原始素材提示用户重新建立索引。二、检查点写入要晚于素材确认第一次出现脏任务是因为我在图像写入前更新了帧数。锁屏恰好发生在文件流尚未关闭的时候Preferences 里写着 384目录里第 384 张却是零字节。恢复页相信了数字原生侧读取到损坏输入后才报错故障位置离真正原因已经很远。当前实现把顺序改成写入临时文件、关闭流、校验、原子改名、更新索引最后才刷新检查点。Preferences 负责保存小体积元数据不承担大文件存储。这段代码解决的是“检查点领先于素材”的问题。每次不是都强制落盘而是每 24 帧或进入后台时写一次减少高频序列化带来的抖动。import{preferences}fromkit.ArkDataimporttype{common}fromkit.AbilityKitconstSTORE_NAMEscan_resume_storeconstKEY_ACTIVEactive_checkpointexportclassCheckpointStore{constructor(privatecontext:common.UIAbilityContext){}asyncsave(checkpoint:ReconCheckpoint):Promisevoid{conststoreawaitpreferences.getPreferences(this.context,STORE_NAME)awaitstore.put(KEY_ACTIVE,JSON.stringify(checkpoint))awaitstore.flush()}asyncload():PromiseReconCheckpoint|undefined{conststoreawaitpreferences.getPreferences(this.context,STORE_NAME)constrawawaitstore.get(KEY_ACTIVE,)asstringif(!raw)returnundefinedconstvalueJSON.parse(raw)asReconCheckpointreturnvalue.schemaVersion2?value:undefined}asyncclear():Promisevoid{conststoreawaitpreferences.getPreferences(this.context,STORE_NAME)awaitstore.delete(KEY_ACTIVE)awaitstore.flush()}}flush()放在关键节点显式执行因为这里需要的是可恢复性不只是内存中的最新值。另一方面也不能每来一帧就flush()。当前 Demo 目标 600 帧如果每帧都同步检查点会把采集链路和持久化链路绑在一起。正式项目还应该把异常分类存储空间不足、JSON 损坏、目录不可访问分别处理不能统一显示“恢复失败”。检查点保存完成后页面状态从CAPTURING进入CHECKPOINTEDHiLog 固定输出[ScanResume] taskrecon_20261001_09 CAPTURING - CHECKPOINTED [ScanResume] confirmed384 target600 progress64% [ScanResume] checkpoint flushed at 2026-10-01 14:36:18三、先处理脏目录再重建流水线恢复按钮不能一上来就创建新任务。它要先确认三件事任务目录仍存在索引中的 384 个文件都可读目录尾部是否有未登记的临时文件。只有检查通过才把状态切到REBUILDING。这里还有一个容易忽略的边界原进程如果没有真正死亡而只是页面被重建旧回调可能仍在队列里。新控制器启动后旧回调晚到几百毫秒就会把 64% 改回 3%或者把新任务误标为失败。下面的控制器用generation解决回调串线。每次创建或恢复都会递增代次回调携带创建时的代次不一致就丢弃。它同时在恢复前调用cancelAndDispose()把仍存活的旧流水线收口。exportclassReconSessionController{privategeneration:number0privatestate:ReconStateReconState.IDLEasyncrestore(cp:ReconCheckpoint):Promisevoid{constcurrentthis.generationthis.stateReconState.RESTORINGawaitnativeRecon.cancelAndDispose()constvalidFramesawaitFrameIndex.verify(cp.frameDir,cp.confirmedFrames)awaitFrameIndex.removeUncommittedTail(cp.frameDir)if(validFrames!cp.confirmedFrames){this.stateReconState.FAILEDthrownewError(FRAME_INDEX_MISMATCH:${validFrames}/${cp.confirmedFrames})}this.stateReconState.REBUILDINGawaitnativeRecon.createPipeline({taskId:cp.taskId,inputDir:cp.frameDir,expectedFrames:cp.targetFrames},(event:ReconEvent){if(current!this.generation)returnthis.consume(event)})awaitnativeRecon.feedExistingFrames(cp.confirmedFrames)awaitnativeRecon.startCaptureFrom(cp.confirmedFrames1)}asyncdispose():Promisevoid{this.generationawaitnativeRecon.cancelAndDispose()}}这段代码里的nativeRecon是 ArkTS 对 Native Bridge 的工程封装方法名属于 Demo不是把系统 C API 原样搬进页面。真正的 Spatial Recon Kit 流水线仍在 C/C 层创建、喂入数据和释放ArkTS 负责会话编排、检查点与 UI 状态。这样写的好处是页面不接触裸指针恢复策略也可以独立测试。feedExistingFrames()不等于从 64% 的优化迭代现场继续。它是重新构建输入上下文并复用已确认素材。底层能力若不承诺跨进程保存训练态就不能在文章里把它包装成“无损断点续训”。当前 Demo 恢复的是采集会话与输入集合计算阶段可能需要重新执行这个边界必须向用户说明。图中的工程目录将CheckpointStore、FrameIndex和ReconSessionController分开。右侧模拟器显示任务recon_20261001_09正在REBUILDING底部日志则对应 384 帧校验、脏尾清理和新流水线创建。这样排查时能快速判断失败发生在存储、索引还是 native 管线。四、前后台切换只发信号不在生命周期里做重活另一版代码把目录扫描和流水线释放全部放进UIAbility.onBackground()。实际用下来不稳生命周期回调应尽快结束复杂 I/O 会让切后台变慢如果释放过程与相机回调并发还可能出现检查点尚未完成、句柄已经销毁的次序问题。我最后只在 Ability 层发出“需要收口”的信号具体工作由控制器串行处理。页面可见性恢复时同样只刷新状态不自动弹窗、不自动启动相机。这段代码解决的是生命周期回调承担过多业务的问题。exportdefaultclassEntryAbilityextendsUIAbility{onBackground():void{this.context.eventHub.emit(recon:lifecycle,BACKGROUND)}onForeground():void{this.context.eventHub.emit(recon:lifecycle,FOREGROUND)}onDestroy():void{this.context.eventHub.emit(recon:lifecycle,DESTROY)}}// Controller 内部使用串行队列消费// BACKGROUND - commitFrameTail - saveCheckpoint - pauseOrDispose// FOREGROUND - loadCheckpoint - verify - exposeResumeAction// DESTROY - invalidateGeneration - cancelAndDispose页面返回前台后只展示“发现可恢复任务”由用户点击“继续重建”。这是一个产品取舍相机、算力和磁盘读写都属于用户能感知的行为不应该因为一次前后台切换就静默恢复。重复进入前台时控制器还会检查是否已有恢复 Promise避免两次点击或两次生命周期事件创建两条流水线。五、64% 只是入口恢复过程要让人看懂最终页面没有只放一个进度条而是显示三层信息检查点是 384/600 帧当前正在REBUILDING恢复动作已经完成“索引校验”和“脏帧清理”正在“重建原生流水线”。用户可以区分“素材还在”和“计算已继续”开发者也能从截图复核状态。恢复成功后的状态顺序是CHECKPOINTED - RESTORING - REBUILDING - CAPTURING - COMPLETED最终确认 600 帧导出livingroom_20261001_09.splat。如果核验只找到 383 个有效文件页面不会把进度偷偷改成 63% 然后继续而是进入FAILED显示FRAME_INDEX_MISMATCH:383/384保留素材并提供“重建索引”和“新建任务”两个操作。这个判断看起来保守却能避免最难查的半恢复状态。对重建类任务来说进度条好看不重要输入集合能否解释才重要。六、正式项目还要补齐的四个边界第一存储空间。开始任务前要估算剩余空间恢复前也要重新检查。用户拍到 64% 时空间不足不能继续写临时文件更不能覆盖最后一个有效检查点。第二配置兼容。模型版本、相机内参策略、图像方向处理方式发生变化后应提升schemaVersion或单独记录pipelineConfigHash。配置不一致时保留原素材禁止直接混入新帧。第三热状态与冷状态要区分。页面销毁但进程仍在属于热恢复可以复用仍被控制器持有的实例进程重启属于冷恢复必须重建管线。两条路径最终都要走同一套状态机不能各写一套 UI。第四资源释放必须可重复。cancelAndDispose()要做到幂等没有任务时调用不报错任务取消后再次调用不释放其他实例。回调、纹理、文件描述符和 native 内存都要在同一个所有权模型里收口。七、这次改动真正解决了什么最后回看这次工作并没有创造一个神奇的“断点续训 API”而是把原来含糊的“继续任务”拆成了一条可验收的工程链路素材先确认检查点后写入恢复先核验流水线后重建旧回调靠代次隔离前后台只传递事件失败时保留证据不伪造进度。实际用下来最明显的变化不是恢复速度而是每一种失败都能落到具体阶段。看到RESTORING就查目录和索引看到REBUILDING就查 Native Bridge看到CAPTURING卡住再查相机输入。状态拆清楚以后3DGS 端侧重建才从一次性 Demo 变成能承受真实手机生命周期的工程能力。参考资料HarmonyOS Spatial Recon Kit重建三维场景C/CHarmonyOS Spatial Recon Kit 术语说明
返回列表