HarmonyOS 7 Spatial Recon Kit × Camera Kit 开发实录 04:系统压力保护、异常中断与任务恢复【鸿蒙心迹】
第三篇把 ReconRoom 的前后台状态收住以后,我连续做了几轮更长时间的室内扫描。任务不再因为切后台乱掉,但设备压力开始变成真正的问题。
最明显的一次是长时间采集以后,Camera Kit 上报的 systemPressureLevelChange 进入 SYSTEM_PRESSURE_SEVERE。页面还能动,帧也还能来,如果这时候仍按照原来的节奏继续采集、向队列压数据,短时间看不出错误,后面却更容易遇到队列堆积、相机状态波动和任务中断。
这次我没有做“温度超过某个值就停”这种硬编码,而是直接围绕 Camera Kit 提供的系统压力级别做策略。官方在 API 20 起提供 SystemPressureLevel,包含 NORMAL、MILD、SEVERE、CRITICAL、SHUTDOWN,并建议长时间相机场景根据压力变化动态调整图像质量参数,以平衡功耗、发热和系统负载。
ReconRoom 当前任务固定为 recon_20261002_04。本轮做了一次完整的压力升级和异常恢复:SEVERE 时把采样间隔拉到 160 ms;继续升到 CRITICAL 时停止新的帧输入、保存 cp_20261002_04_02,随后重新创建 Native Session,Generation 从 1 变成 2,回放已落盘的 91 张关键帧,最终恢复到 CAPTURING。恢复后页面显示系统压力 MILD、覆盖率 78%、已接受 236 帧、已拒绝 39 帧、关键帧 91。

一、系统压力不是“手机温度”一个数字
我一开始也想过读一个温度值,然后自己定义 38°C、42°C 之类的阈值。真正做工程以后放弃了。
原因是应用真正关心的不是某个传感器温度,而是“当前相机和系统资源压力到了什么程度”。Camera Kit 的 systemPressureLevelChange 已经把这一层抽象成离散级别。对于长时间相机业务,这比在应用里猜一个温度阈值更合适。
当前五个级别是:
SYSTEM_PRESSURE_NORMAL
SYSTEM_PRESSURE_MILD
SYSTEM_PRESSURE_SEVERE
SYSTEM_PRESSURE_CRITICAL
SYSTEM_PRESSURE_SHUTDOWN
ReconRoom 不把它们直接映射成 UI 文案,而是先进入 CameraPressureGuard,再决定采集策略。
我的处理原则是:
NORMAL 保持默认节奏
MILD 轻度降频
SEVERE 明显降频,限制队列增长
CRITICAL 保存检查点,暂停帧输入
SHUTDOWN 停止采集并释放相机侧资源
这套策略是 Demo 的工程决策,不是系统固定要求。真正产品里还要结合业务对画质、时延和续航的取舍。本轮还额外记录了温度 42°C、CPU 68%、内存 72% 作为调试观察量;它们只用于复盘,不参与压力级别判断,真正触发策略的仍然是 Camera Kit 的 SystemPressureLevel。
二、CameraPressureGuard 只负责监听,不直接操作 UI
第二篇已经有 FrameQualityGate,第三篇又加了 ReconRuntime。这一轮我没有继续把压力监听塞进页面,而是在 common/camera/CameraPressureGuard.ets 单独管理。
这一段代码解决的是“压力回调到了以后,页面和采集模块各自做一套判断”的问题:
import { camera } from '@kit.CameraKit'
import { BusinessError } from '@kit.BasicServicesKit'
export class CameraPressureGuard {
private session: camera.PhotoSession | null = null
private listener?: (
err: BusinessError,
level: camera.SystemPressureLevel
) => void
bind(
session: camera.PhotoSession,
onChanged: (level: camera.SystemPressureLevel) => void
): void {
this.session = session
this.listener = (
err: BusinessError,
level: camera.SystemPressureLevel
) => {
if (err !== undefined && err.code !== 0) {
return
}
onChanged(level)
}
this.session.on(
'systemPressureLevelChange',
this.listener
)
}
unbind(): void {
if (this.session === null) {
return
}
this.session.off(
'systemPressureLevelChange',
this.listener
)
this.listener = undefined
this.session = null
}
}
这里最容易漏的是 off()。
如果每次重新创建相机会话都 on() 一次,却没有在旧 Session 释放前取消监听,恢复两三轮以后同一个压力事件可能被处理多次。到那时看到的现象会很奇怪:一次 SEVERE,采样间隔被连续调整,甚至重复保存检查点。
所以压力监听和 Camera Session 必须成对绑定、成对释放。
三、SEVERE 不要立刻停,先把“输入速率”降下来
上一轮采样间隔是 200 ms,这次测试为了更容易制造压力,把 NORMAL 下的基准间隔放到 80 ms。
进入 MILD 时我把它调到 100 ms;进入 SEVERE 时调到 160 ms,并禁止队列超过高水位。这里没有改输入图片的 1080 × 1440 RGB,因为第二篇已经把当前重建链路的尺寸和内参绑定起来,运行中随意切分辨率会让几何参数同步变得更复杂。
这一段解决的是“系统压力升高时仍然用原速率持续灌帧”的问题:
import { camera } from '@kit.CameraKit'
export class CapturePressurePolicy {
async apply(
level: camera.SystemPressureLevel,
manager: ReconManager
): Promise<void> {
switch (level) {
case camera.SystemPressureLevel.SYSTEM_PRESSURE_NORMAL:
manager.setSampleInterval(80)
manager.setQueueHighWatermark(10)
break
case camera.SystemPressureLevel.SYSTEM_PRESSURE_MILD:
manager.setSampleInterval(100)
manager.setQueueHighWatermark(8)
break
case camera.SystemPressureLevel.SYSTEM_PRESSURE_SEVERE:
manager.setSampleInterval(160)
manager.setQueueHighWatermark(6)
break
case camera.SystemPressureLevel.SYSTEM_PRESSURE_CRITICAL:
await manager.checkpointAndPause('SYSTEM_PRESSURE_CRITICAL')
break
case camera.SystemPressureLevel.SYSTEM_PRESSURE_SHUTDOWN:
await manager.stopCaptureAndReleaseCamera()
break
}
}
}
为什么 SEVERE 不是直接停止?
因为压力变化本身有波动。刚进入 SEVERE 就完全终止任务,会让长时间扫描非常脆弱。当前做法先降低帧进入速率,让系统有机会恢复;真正到 CRITICAL 再建立稳定边界。
这里的 queueHighWatermark 也不是队列容量本身。第二篇队列上限仍是 12,高水位表示达到这个数量以后,新的相似帧优先丢弃,避免在压力状态继续把队列塞满。
四、CRITICAL 时最重要的不是“暂停”,而是先得到一致检查点
压力继续上升时,我不再尝试保住当前瞬间的所有数据。
checkpointAndPause() 的顺序固定为:
禁止新帧
→ 停止 FrameCollector
→ 等待 FrameQueue 清空
→ 把关键帧索引落盘
→ 写 task.json
→ 标记 RECOVERY_REQUIRED
这一段解决的是“发生中断时检查点写了一半”的问题:
export interface ReconCheckpoint {
id: string
taskId: string
acceptedFrames: number
rejectedFrames: number
keyFrameFiles: string[]
coverage: number
phase: string
nativeGeneration: number
}
async checkpointAndPause(reason: string): Promise<void> {
this.acceptingFrames = false
await this.frameCollector.stop()
await this.frameQueue.waitUntilIdle()
const checkpoint: ReconCheckpoint = {
id: 'cp_20261002_04_02',
taskId: 'recon_20261002_04',
acceptedFrames: 236,
rejectedFrames: 39,
keyFrameFiles: this.keyFrameStore.listStableFiles(),
coverage: 78,
phase: 'RECOVERY_REQUIRED',
nativeGeneration: this.nativeGeneration
}
await this.checkpointStore.write(checkpoint)
this.phase = 'RECOVERY_REQUIRED'
this.hilog.warn(
`checkpoint saved, id=${checkpoint.id}, reason=${reason}`
)
}
这里依然不保存 Native Session 指针。
指针只对当前进程和当前实例有意义。真正可以跨中断保存的是文件、参数、任务 ID、关键帧列表和业务状态。
本轮检查点固定为 cp_20261002_04_02,里面记录 236 张接受帧和 91 张关键帧。恢复时真正需要回放的是这 91 张已确认稳定的关键帧,而不是把所有相似帧重新塞一遍。
五、异常恢复不是“把 state 改回 CAPTURING”
这次为了验证恢复链路,我主动模拟了一次 Native Session 不可继续使用的情况。
最容易犯的错误是:读取 task.json,看到上次是 RECOVERY_REQUIRED,然后页面直接显示“恢复成功”。实际上 Native Session 已经失效,只改 UI 状态没有任何意义。
真正恢复要重新建立底层环境:
读取 checkpoint
→ 检查关键帧文件完整性
→ 创建新的 Native Session
→ nativeGeneration + 1
→ 按原内参与位姿回放关键帧
→ 恢复 FrameQueue / Camera
→ 状态切回 CAPTURING
这一段解决的是“业务 taskId 不变,但 Native 会话已经换代”的问题:
async recoverTask(
checkpoint: ReconCheckpoint
): Promise<void> {
if (checkpoint.taskId !== 'recon_20261002_04') {
throw new Error('CHECKPOINT_TASK_MISMATCH')
}
const validFrames =
await this.keyFrameStore.verify(checkpoint.keyFrameFiles)
await this.reconNative.destroySessionSafely()
await this.reconNative.createSession(
this.workDirOf(checkpoint.taskId)
)
this.nativeGeneration += 1
for (const frame of validFrames) {
await this.reconNative.pushPersistedFrame(
frame.path,
frame.intrinsics,
frame.pose
)
}
this.acceptedFrames = checkpoint.acceptedFrames
this.rejectedFrames = checkpoint.rejectedFrames
this.coverage = checkpoint.coverage
await this.frameCollector.start(
this.currentSampleInterval
)
this.acceptingFrames = true
this.phase = 'CAPTURING'
}
这里的 pushPersistedFrame() 是 ReconRoom 自己的 Native Adapter,不是把它冒充成一个系统 API。它负责把之前落盘的 RGB 帧、内参和位姿重新组装成 Spatial Recon Kit 当前版本需要的输入结构,再走已有 PushFrame 适配链路。
这也是整个系列一直坚持业务 taskId 和 Native Session 分离的原因。恢复以后业务任务仍然是 recon_20261002_04,只是 nativeGeneration 从 1 变成 2。
六、关键帧回放前必须做文件完整性校验
恢复链路还有一个容易忽略的情况:task.json 在,但某张关键帧文件没了。
如果直接按列表循环回放,恢复会进行到一半才失败,而且很难知道模型是否已经处于部分初始化状态。
所以 verify() 会先检查:
文件存在
尺寸符合 1080 × 1440
内参记录存在
Pose 记录存在
校验值一致
任意一项失败,当前检查点就不能作为自动恢复入口。
产品里可以继续做更细的策略,比如允许少量非关键边缘帧缺失;ReconRoom 当前更保守,只要关键帧集合不完整,就要求用户重新开始本次任务。宁可失败得明确,也不接受“看起来恢复了,最后模型质量却莫名异常”。
七、系统压力和内存压力不要混成一件事
Camera Kit 的 SystemPressureLevel 用来反映相机场景下的系统压力。应用本身还可能收到内存级别变化。
HarmonyOS 的 AbilityStage 提供 onMemoryLevel(),适合在系统内存压力变化时释放非必要资源。ReconRoom 里我把这两条线分开:
系统压力升高,优先处理:
采样频率
队列高水位
相机采集
检查点
内存压力升高,优先释放:
点云预览缓存
缩略图
临时 UI 数据
非必要历史 Snapshot
它们最后都可能表现成“设备很忙”,但工程处理完全不同。把它们写成一个 isLowResource 布尔值,后面很难知道到底该降采样还是清缓存。
八、DevEco 里这次最关键的是一条完整恢复日志
工程目录在第四篇又多了:
common/camera/CameraPressureGuard.ets
manager/RecoveryCoordinator.ets
manager/TaskCheckpointStore.ets
调试时我关心的不是压力级别有没有显示出来,而是事件之间有没有形成闭环:
pressure=SEVERE
action=THROTTLE
interval=160ms
pressure=CRITICAL
action=CHECKPOINT_PAUSE
checkpoint=cp_20261002_04_02
frames=236
keyFrames=91
recreate native session
generation=2
replay keyFrames=91
restore pose graph success
recover success
state=CAPTURING

这串日志如果缺任何一段,我都不会把页面上的“已恢复”当成成功。
特别是 generation=2。它明确告诉我底层已经不是原来的 Native Session,而业务 taskId 仍然保持不变。后面出现问题时,日志可以直接区分“同一业务任务里的第几代 Native 会话”。
九、最终结果:压力已经回落,但恢复痕迹要保留
恢复完成后,手机页面当前状态是:

本轮统一数据:
taskId=recon_20261002_04
phase=CAPTURING
systemPressure=MILD
coverage=78%
accepted=236
rejected=39
keyFrames=91
checkpoint=cp_20261002_04_02
nativeGeneration=2
severeSampleInterval=160ms
我没有在恢复成功以后把 checkpoint 信息清掉。
因为“当前已经正常”不代表刚才的中断没有价值。检查点 ID、Native Generation、最近一次压力动作,都应该保留到任务结束,方便后续复盘。
这一轮做完以后,ReconRoom 的长时任务才开始像一个真正能跑的工程:它不再假设相机永远稳定、应用永远在前台、Native Session 永远不会中断。
接下来剩下两篇会把链路收尾。第五篇处理重建结果写出、目录管理、文件完整性和版本元数据;第六篇再把 3DGS 结果加载到 ArkGraphics 3D / Spatial Render,处理 GSNode 生命周期、页面退出释放和长时间预览的性能问题。到那时,ReconRoom 才真正从“采集与恢复”走到“结果可用”。
十、压力回落以后不要立刻恢复满速,避免在边界来回抖动
第一版压力策略还有一个问题:SEVERE 一回到 MILD,我马上把采样间隔从 160 ms 改回 100 ms;如果设备正处在临界状态,几十秒里可能反复出现 MILD → SEVERE → MILD → SEVERE。
这种来回切换会让采集节奏抖得很厉害,队列也跟着忽高忽低。
后来我加了一个很简单的迟滞策略。压力升级立即响应,压力回落则要求连续稳定一段时间再逐级恢复。比如从 SEVERE 回到 MILD 后先保持 160 ms,持续 10 秒没有再次升压,才切到 100 ms;从 MILD 回到 NORMAL 同样延迟恢复。
这样做没有改变 Camera Kit 的级别,只是让 ReconRoom 的业务动作不要过度敏感。
我还把最近 20 次压力事件留在环形日志里。调试时如果只看到最终 MILD,很容易误以为设备一直很稳定;保留短历史以后,可以看出刚才是不是经历过连续抖动。这个信息对解释“为什么采样突然变慢”很有用。
十一、检查点写入要有临时文件,避免进程中断留下半份 JSON
做异常恢复以后,task.json 本身也成了关键数据。最开始我直接覆盖原文件,理论上写到一半进程就可能被终止,下一次启动时读到一份截断 JSON。
现在的策略是:
task.json.tmp
→ 写完整内容
→ flush / close
→ 校验
→ rename 为 task.json
只有最后一步成功,新的检查点才算提交。
同时在 metadata 里记录:
schemaVersion
checkpointId
taskId
createdAt
nativeGeneration
frameManifestVersion
schemaVersion 是为了后续项目升级。如果第五篇增加结果文件字段,第六篇又增加渲染参数,老版本检查点不能在没有迁移规则的情况下直接按新结构读取。
这种版本字段在 Demo 阶段看起来有点“工程过度”,但一旦任务可能跨进程、跨版本恢复,它很快就会变成必要条件。
十二、回放 91 张关键帧时,顺序也属于检查点的一部分
恢复时并不是“文件都在就行”。
3D 重建输入和相机轨迹有关,因此 keyFrameFiles 不是普通 Set,而是有顺序的 manifest。每一项至少保存:
sequence
filePath
timestamp
intrinsics
pose
checksum
verify() 通过以后,RecoveryCoordinator 按 sequence 回放。任何序号缺口都会写入恢复日志。
这次 recon_20261002_04 回放 91 张关键帧后,日志出现:
replay keyFrames=91
restore pose graph success
我才允许状态从 RECOVERING 切到 CAPTURING。
如果中途第 47 张失败,新的 Native Session 会立即销毁,业务状态保持 RECOVERY_FAILED,而不是拿一个只回放了一半的 Session 继续接新帧。宁可让用户重新开始,也不让一个不确定的底层状态继续往后污染结果。
十三、恢复成功以后还要验证“输入端”是不是同一套配置
重新创建 Session 只是恢复的一半。相机重新打开以后,分辨率、内参来源、方向信息都必须重新确认。
ReconRoom 会比较恢复前后的采集配置摘要:
1080×1440 RGB
cameraId
intrinsicsProfile
orientation
gateProfile=room_v1
只要关键字段发生变化,就不自动继续把新帧接在旧任务后面。
原因是第二篇已经确定:图像和内参必须属于同一套几何配置。如果恢复以后相机配置换了,业务 taskId 虽然一样,空间数据却已经不是同一条链路。
这一条在“应用进程重启后系统重新选择相机配置”的场景尤其重要。恢复不是为了尽量继续,而是在条件仍然一致时继续。
十四、我最后故意把压力推到 CRITICAL 做了一次破坏性验证
正常开发时很容易只测 NORMAL 和 MILD,因为设备大多数时间就停在那里。但 04 的核心恰恰是异常路径,所以最后一轮我要求日志必须完整走过:
NORMAL
→ MILD
→ SEVERE
→ CRITICAL
→ CHECKPOINT_PAUSE
→ recreate session
→ replay
→ CAPTURING
如果没有真正经过 CRITICAL,所谓“恢复代码”很可能只是从没执行过的备用分支。
这次结果里,SEVERE 阶段采样间隔稳定到了 160 ms;CRITICAL 到来后不再接受新帧;检查点记录 236 个 accepted、39 个 rejected 和 91 个 keyFrames;Native Session 重建后 generation=2;压力回落到 MILD 时页面恢复采集,覆盖率保持 78%。
更重要的是,恢复后的 taskId 没有变。业务层知道这是同一个房间扫描任务,底层则知道 Session 已经换过一次。两个身份同时保留,后续第五篇保存模型时才能把历史信息一起写进结果元数据。
参考资料
- Spatial Recon Kit 3DGS 端侧重建 Pipeline:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/spatial-recon-c-spatial-recon-pipeline
- Spatial Recon Kit API 参考:https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/api/spatial-recon-api
- Camera Kit 系统压力管理:https://developer.huawei.com/consumer/en/doc/harmonyos-guides/camera-system-pressure
- Camera Kit API 20 系统压力级别变更:https://developer.huawei.com/consumer/en/doc/harmonyos-releases/js-apidiff-camerakit-6003
- AbilityStage 内存级别回调:https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V13/abilitystage-V13
更多推荐



所有评论(0)