HarmonyOS 7 Spatial Recon Kit 开发实录 01:支持检测、Session 创建与首条有效重建链路【鸿蒙心迹】
这次没有先做“看起来像 3D 重建”的页面,而是先把最短的一条工程链路跑通:设备支持检测、Native Session 创建、采集任务状态同步、开始重建、进度回传,以及页面退出后的资源释放。
项目叫 ReconRoom。这一轮的测试任务固定为 recon_20261001_01,场景类型是 living_room。跑到能稳定观察状态时,任务已经处理到 128 / 200 帧,页面显示 68%,状态为 RECONSTRUCTING。

一、第一版最容易犯的错,是把“按钮能点”当成“能力可用”
Spatial Recon Kit 的特殊之处在于,它不是一个适合拿模拟器先把全链路跑通的普通 UI API。当前官方资料对设备能力、芯片和运行环境都有明确约束,重建链路本身也偏 Native。也就是说,页面上出现“开始扫描”并不代表后面的 Session 一定能创建成功。
我一开始把支持检测放在按钮点击之后:用户点“开始”,ArkTS 再调用 Native。单次测试看不出问题,但一旦换到不支持的设备,UI 已经切换到扫描态,Native 才返回不支持,页面还要再倒退一次状态,整个体验很别扭。
后来我把顺序改成了:
页面进入 → 能力检测 → 创建工作目录 → 创建 Session → UI 进入 READY → 用户开始采集
这样 UI 只消费已经确认过的能力状态。
当前项目的状态枚举很简单:
IDLE
→ CHECKING
→ READY
→ CAPTURING
→ RECONSTRUCTING
→ COMPLETED / FAILED
这里没有把“暂停、恢复、热保护”塞进第一篇,因为这几个状态后面会单独处理。第一阶段只验证一件事:Session 的拥有者到底是谁,以及它什么时候能被安全销毁。
二、Native 层先做支持检测,再创建唯一 Session
这一段代码解决的是“设备不支持时仍然创建 Session”的问题。Spatial Recon Kit 的重建能力在 C++ 侧使用,项目里我把它收进 SpatialReconNative.cpp,ArkTS 页面不直接接触 HMS 的 Session 指针。
示例省略了 Node-API 参数转换,只保留和 Session 生命周期直接相关的部分:
#include "spatial/spatial_recon_interface.h"
namespace recon {
static HMS_SpatialRecon_Session* g_session = nullptr;
HMS_SpatialReconStatus PrepareSession(const char* workPath)
{
if (g_session != nullptr) {
return SPATIAL_RECON_STATUS_SUCCESS;
}
HMS_SpatialReconStatus status =
HMS_SpatialRecon_IsSupport(SPATIAL_RECON_MODEL_TYPE_GS);
if (status != SPATIAL_RECON_STATUS_SUCCESS) {
return status;
}
status = HMS_SpatialRecon_CreateSession(
SPATIAL_RECON_MODEL_TYPE_GS,
workPath,
&g_session
);
if (status != SPATIAL_RECON_STATUS_SUCCESS) {
g_session = nullptr;
}
return status;
}
void ReleaseSession()
{
if (g_session == nullptr) {
return;
}
HMS_SpatialRecon_DestroySession(g_session);
g_session = nullptr;
}
HMS_SpatialRecon_Session* GetSession()
{
return g_session;
}
}
这段代码看起来很薄,但我刻意没有让 CreateSession 散落在页面事件里。
原因是 Session 具有明确的 Native 生命周期。页面重建、路由跳转甚至 UI 状态刷新,都不应该重复生成一个新 Session。当前实现用一个进程内指针约束“只持有一个重建会话”,页面拿到的是任务状态,不是裸指针。
真正产品化时还需要把这里改成线程安全的 Manager,并处理进程内多个业务模块竞争能力的情况;第一版先把所有权收紧,比一开始就在 ArkTS 页面到处调用要稳得多。
三、ArkTS 只维护任务,不维护 Native 指针
Native Session 建好以后,我没有让页面直接调用一串 start / progress / destroy。中间加了一层 ReconManager.ets,把页面状态和底层 Session 状态分开。
这一段解决的是“页面状态和 Native 状态互相覆盖”的问题:
export type ReconState =
'IDLE' |
'CHECKING' |
'READY' |
'CAPTURING' |
'RECONSTRUCTING' |
'COMPLETED' |
'FAILED'
export interface ReconSnapshot {
taskId: string
sceneType: string
state: ReconState
progress: number
capturedFrames: number
totalFrames: number
}
export class ReconManager {
private snapshot: ReconSnapshot = {
taskId: 'recon_20261001_01',
sceneType: 'living_room',
state: 'IDLE',
progress: 0,
capturedFrames: 0,
totalFrames: 200
}
async prepare(): Promise<ReconSnapshot> {
this.snapshot.state = 'CHECKING'
const supported = await reconNative.isSupported()
if (!supported) {
this.snapshot.state = 'FAILED'
throw new Error('SPATIAL_RECON_DEVICE_NOT_SUPPORTED')
}
const workPath =
'/data/storage/el1/base/spatial_recon_files/recon_20261001_01/'
await reconNative.createSession(workPath)
this.snapshot.state = 'READY'
return this.getSnapshot()
}
getSnapshot(): ReconSnapshot {
return { ...this.snapshot }
}
}
这里有一个工程上的小取舍:taskId 不是 HMS Session ID,而是业务自己的任务 ID。Native Session 可以因为异常重建、前后台切换而重新创建,但业务任务 recon_20261001_01 不应该跟着变。
后面做任务恢复时,这个区分会非常重要。否则一旦把底层句柄当成业务标识,Session 重建之后,历史状态、文件目录和 UI 都会失去对应关系。
1. 工作目录不能只是一个临时字符串
Session 创建成功以后,我又补了一次目录检查。最开始的实现把所有重建结果都写进同一个 spatial_recon_files 目录,跑一次没有问题,第二次开始就很难区分中间文件属于哪个任务。
现在 ReconRoom 把 taskId 直接纳入目录:
spatial_recon_files/
└── recon_20261001_01/
├── temp/
├── result/
└── task.json
task.json 不保存 Native 指针,只记录业务能恢复的数据:任务 ID、场景类型、创建时间、当前状态、最后进度和结果路径。这样即使进程被系统回收,下一次启动仍然能判断“这里曾经有一个任务”,而不是面对一堆不知道属于哪一轮扫描的文件。
还有一个细节是目录创建失败不能继续往下走。存储空间不足、路径不可写、历史残留文件冲突,这些问题都应该在 CreateSession 之前被发现。否则 Native Session 已经建立,工作目录却不可用,后面的保存失败会被误判成重建失败。
目前我把目录准备也算进 CHECKING 阶段。只有“能力支持 + 工作目录可用 + Session 创建成功”全部成立,页面才进入 READY。这个边界比“API 返回成功就算准备好”更接近真实工程状态。
2. 错误码不要原样扔给页面
Native 层返回的是能力错误,页面需要的是用户能理解的业务状态。两者直接绑定以后,UI 很快就会出现大量 if/else:设备不支持怎么提示、Session 创建失败怎么提示、目录异常怎么提示、输入帧异常怎么提示。
所以 Manager 里我又加了一层错误映射。页面只认识几类业务错误:
DEVICE_UNSUPPORTED
WORKDIR_UNAVAILABLE
SESSION_CREATE_FAILED
FRAME_INPUT_INVALID
RECON_FAILED
这样做不是隐藏底层信息。HiLog 里仍然保留原始 HMS status,便于定位;UI 只消费稳定的业务枚举。后续 SDK 如果增加或调整错误码,页面不需要跟着改一轮。
这个分层在连载后面做暂停恢复时会更明显:同一个 Native status,在“首次创建”和“恢复旧任务”两个场景里,用户提示可能完全不同。把错误码直接透传给 UI,会让业务层越来越难维护。
四、真正开始重建前,我先把“帧输入”和“启动”拆开
官方链路里有 HMS_SpatialRecon_PushFrame / HMS_SpatialRecon_PushARFrame、HMS_SpatialRecon_StartSession、进度查询和结果保存。第一轮我没有把采集帧的所有参数校验都展开,而是先把调用顺序固定下来:
CreateSession
→ Push valid frames
→ Register callback / progress observer
→ StartSession
→ Read progress
→ Save result
→ DestroySession
这样做是为了避免一个很典型的问题:页面点了开始就立刻 StartSession,但 Native 侧还没有拿到足够有效的数据帧,最终很难判断是 Session 本身失败,还是输入数据不符合要求。
ReconRoom 里把采集和重建视为两个阶段。CAPTURING 阶段只负责把合法帧送入队列;当输入满足条件后才切到 RECONSTRUCTING。
这次截图里的 128 / 200 并不是说 Spatial Recon Kit 必须固定 200 帧,而是 Demo 自己定义的采集目标。它只是用来让 UI 有一个可观察的工程状态,不应该误解成系统能力规格。
五、页面生命周期只做两件事:订阅状态、释放订阅
页面层我尽量不做 Native 管理。ScanPage.ets 只负责订阅 Manager 的快照,并在页面离开时停止 UI 轮询。
这一段解决的是“页面退出后计时器还在刷新”的问题:
@Entry
@Component
struct ScanPage {
@State taskId: string = 'recon_20261001_01'
@State progress: number = 0
@State status: string = 'IDLE'
@State capturedFrames: number = 0
private manager: ReconManager = ReconManager.shared()
private timerId: number = -1
async aboutToAppear(): Promise<void> {
const snapshot = await this.manager.prepare()
this.applySnapshot(snapshot)
this.timerId = setInterval(async () => {
const latest = await this.manager.refreshProgress()
this.applySnapshot(latest)
}, 300)
}
aboutToDisappear(): void {
if (this.timerId >= 0) {
clearInterval(this.timerId)
this.timerId = -1
}
}
private applySnapshot(data: ReconSnapshot): void {
this.taskId = data.taskId
this.progress = data.progress
this.status = data.state
this.capturedFrames = data.capturedFrames
}
}
这里故意没有在 aboutToDisappear() 里直接 DestroySession。
原因很简单:页面离开不等于重建任务结束。后面要做“退到任务列表再回来”“切后台再回来”“异常恢复”,如果页面销毁时就把 Session 一并销毁,任务连续性会被 UI 生命周期绑死。
现在的规则是:页面只释放自己的订阅;真正的 Session 由 ReconManager 在任务结束、取消或不可恢复异常时统一释放。
1. 进度采样不是越快越好
我一开始把轮询间隔设成 50ms,觉得这样进度条会更“实时”。真机跑起来以后发现,这个刷新频率没有给用户带来更多信息,反而让 ArkTS 层频繁更新状态,HiLog 也被进度输出刷屏。
当前改成 300ms。一方面足够让进度组件看起来连续,另一方面不会把 UI 刷新变成重建任务之外的额外负担。真正需要高频观察的 Native 指标可以单独记录,不必全部映射到界面。
这里还做了一次去重:如果新进度和上次完全一样,就不触发页面状态更新。比如 Native 连续三次返回 68%,页面只保留第一次。这个改动很小,但长时间重建时能明显减少无意义刷新。
另外,progress 只表示当前任务阶段的可视进度,不承担生命周期判断。不能因为进度到了 100 就直接在页面写 COMPLETED,最终状态仍然以 Manager 收到的完成回调为准。否则保存结果失败时,用户会先看到“完成”,随后又突然变成“失败”,状态语义会很混乱。
六、DevEco 里的模拟器只验证 UI,Native 能力仍然看真机
为了保持整个连载的配图结构一致,开发截图右侧仍然放了 HarmonyOS 模拟器,用来确认页面状态、布局和进度组件。
但这一点必须说清楚:Spatial Recon Kit 的真实重建链路不能靠模拟器证明跑通。 当前 68%、128 / 200 的 Native 状态以支持设备上的运行日志为准,模拟器只复现同一份业务 Snapshot。

我在 HiLog 里固定打印四类信息:
taskId
session state
progress
capturedFrames
本轮最终观察到的状态是:
taskId=recon_20261001_01
scene=living_room
state=RECONSTRUCTING
progress=68
frames=128/200
如果页面数字变了但日志没变,优先检查 ArkTS 状态同步;如果日志也没有推进,再看 Native Session 和帧输入。把两层日志分开以后,定位效率比只盯着 UI 高很多。
1. 这一轮我专门做了四次反向验证
链路跑通以后,我没有只保留一次“成功截图”,而是故意把几个失败条件重新测了一遍。
第一组是不支持设备。期望结果是页面停在能力检测阶段,不创建工作目录,也不出现扫描按钮可用的假象。
第二组是重复进入页面。连续进入、退出 ScanPage 三次,业务任务 ID 保持不变,Native Session 不重复创建;页面自己的轮询每次离开都能停止。
第三组是工作目录异常。我把目标目录改成不可用路径,Manager 应该在 READY 之前失败,HiLog 记录原始错误,页面只显示“任务准备失败”,而不是进入采集页面后再报错。
第四组是任务仍在运行时返回首页。页面消失,进度轮询停止,但 Manager 仍持有任务状态。再次进入后读取的是已有 Snapshot,不会重新生成 recon_20261001_01。
这四个测试没有任何一个是在验证 3DGS 模型“好不好看”,它们验证的是工程边界。第一篇如果连这些边界都没有定下来,后面继续加热保护、保存、渲染,只会把问题越堆越深。
3. prepare() 必须是幂等的,页面重建不能重复建 Session
ArkUI 页面在真实业务里并不是“创建一次然后永远不动”。路由返回、组件重建、窗口形态变化,都可能让 aboutToAppear() 再次执行。如果 prepare() 每次都无条件进入 Native CreateSession,很快就会出现“UI 只是重新出现,底层却多建了一次会话”的问题。
所以 ReconManager 当前还维护了一个内部阶段:
EMPTY → PREPARING → READY
只有 EMPTY 才允许真正进入 Native 创建;PREPARING 时新的调用复用同一个 Promise;已经是 READY 时直接返回现有 Snapshot。这样同一时刻无论页面触发一次还是三次 prepare,底层都只有一个创建动作。
这个设计后面还会用于防止“用户连续点两次开始”。按钮层做防抖只能解决交互层重复点击,Manager 幂等才能保证业务层不会因为其他入口再次触发。对 Session 这类重资源对象,我更愿意在 Manager 里多做一道保护,而不是相信所有页面永远按预期调用。
4. 第一篇的验收标准,不是“看到 68%”这么简单
我给这一阶段定了五条验收条件:
- 不支持设备上,不允许创建 Session;
- 支持设备上,同一任务只能存在一个 Session;
- 页面退出只停止 UI 订阅,不误杀运行中的任务;
- 业务
taskId和 Native Session 解耦; - 任务结束或不可恢复失败后,能够确定地执行释放。
其中任何一条做不到,后面的暂停恢复都会变得不可信。
所以 68% 只是这轮截图里的一个观察点,它证明“状态确实在往前走”,却不是最终结论。真正有价值的是从 CHECKING 到 READY、再到 RECONSTRUCTING 的每次状态变化都有日志、有唯一任务 ID、有明确所有者,也知道异常后谁来收尾。
把这些边界固定以后,下一步再处理帧质量,问题就会干净很多:如果模型结果异常,可以更确定地把注意力放到输入,而不是继续怀疑 Session 是否被重复创建、页面是否提前释放了资源。
七、第一条链路跑通后,新的问题反而更明显
现在 ReconRoom 已经能从支持检测进入 Session,再把状态推到重建阶段。运行结果如下:

这张图里几组数据和 DevEco 调试保持一致:
- 任务:
recon_20261001_01 - 场景:
living_room - 状态:
RECONSTRUCTING - 进度:68%
- 采集帧:128 / 200
这一步完成以后,我没有急着做结果渲染。实际连续扫两轮后,模型质量差异很明显:有一轮帧数更多,结果却更差。回看日志才发现,采集阶段塞进去了很多位姿变化很小的相似帧。
也就是说,链路已经通了,下一阶段真正该解决的不是“再多采几张”,而是 哪些帧应该进入 Session。
下一轮会继续沿用 ReconRoom,把相机内参、位姿变化和帧队列放到采集链路前面,先过滤尺寸不对、变化太小、重复度高的输入,再看重建质量是否稳定。
参考资料
- HarmonyOS 7 空间计算能力:https://developer.huawei.com/consumer/cn/features/spatialization
- Spatial Recon Kit 3DGS 端侧重建介绍:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/spatial-recon-introduction
- 3DGS 空间重建 Pipeline:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/spatial-recon-c-spatial-recon-pipeline
更多推荐




所有评论(0)