【共创稿事节】HarmonyOS 7重建 session 生命周期:单例约束与并发冲突
重建 session 生命周期:单例约束与并发冲突
同一时刻只允许一个重建 session。这条约束写在文档里很不起眼,但商品列表快速切换两个 3DGS 场景时,第二个 session 创建直接失败,错误码 SESSION_ALREADY_EXISTS。我们一开始没当回事,结果多场景切换页上线第一天就收到崩溃堆栈。session 生命周期的坑比设备支持更隐蔽——查询都过了,创建却失败。
能力面:session API 与单例约束
Spatial Recon Kit 的重建 session 通过 spatialRecon.createReconSession(config) 创建,返回 ReconSession 对象,挂载采集、重建、导出三组方法。销毁走 session.destroy(),返回 Promise,异步操作。
单例约束的体现:当前 session 未销毁完成时,再次调用 createReconSession 会立即抛错,错误码集中在 SESSION_ALREADY_EXISTS(上一个还在跑)、SESSION_DESTROYING(destroy 的 Promise 还没 resolve)、INVALID_SESSION(destroy 完成后还用旧引用)、RESOURCE_BUSY(其他进程占 NPU/GPU,极少见)。
文档对单例的描述只有一句"同一时刻只允许一个重建 session"。"同一时刻"的边界是什么——是 destroy 的 Promise resolve 之后?还是底层资源真的释放完之后?文档没说。实测下来,destroy 的 Promise resolve 之后,再创建新 session 仍然有约 80-200ms 的窗口期会失败。底层 NPU 上下文还在清理,但 JS 侧已经认为销毁完成了。
状态流转画成状态机就一眼看清:任何时刻只有一个合法状态,跨状态调用直接拒绝。
最容易被忽略的是 ERROR 回到 IDLE 那条边,漏了它 manager 会死锁。
约束面:销毁的异步延迟
session 销毁不是一步到位。从调用 destroy() 到底层资源真正可再用,中间有 JS 释放引用、通知 native 停任务、NPU 上下文清理、GPU 资源归还、后台 GC 几段。Promise resolve 在"GPU 资源归还"之后,但"后台 GC"之前。
也就是说 resolve 之后立刻创建新 session,理论上资源已经够用,但实测仍有失败 case。我们怀疑是底层 NPU driver 的内部队列还没清空,但没拿到源码确认。销毁后立即创建新 session 报错,这是我们踩的第一个坑。修复方式是给保护性延迟,太短没用,太长用户感知卡顿。
场景落地:商品列表快速切换
商品展示页:列表里 6 个商品,每个对应一个 3DGS 场景。用户点 A 进入预览,看两眼切到 B,再切到 C。理想情况下切换是即时的,实际每次切换都要销毁旧 session、创建新 session。
我们在 Mate 90(9030)上测了快速切换连续 10 次,10 次里 3 次冲突,冲突率 30%。冲突后走重试逻辑,重试间隔 50ms,最多重试 5 次。重试成功后总延迟拉到 300ms 左右,用户能感知到"切的时候偶尔顿一下"。不冲突的切换总延迟在 170-215ms,冲突的拉到 295-348ms。
踩坑:状态机没设计好导致状态混乱
第一版我们写了简单封装:销毁旧的,立刻创建新的,失败就重试。结果快速切换场景下出现更恶性的问题——用户连点 A→B→C 三次,A 的销毁还没完,B 的创建失败重试中,C 又来了。最后 session 引用乱掉,UI 显示的是 C 但实际跑的是 B 的重建结果。
修复方案是引入一个严格的状态机,所有 session 操作都走状态机,不允许跨状态调用。状态机如下:
IDLE → CREATING → ACTIVE → DESTROYING → IDLE
↘ ERROR → IDLE
新请求来了,如果当前状态不是 IDLE,要么排队要么取消上一个操作。我们选了取消——快速切换场景下,用户要看的是最后一个,中间的请求没意义。
// entry/src/main/ets/recon/SessionManager.ets
import { spatialRecon, ReconSession, ReconConfig } from '@kit.SpatialReconKit';
enum SessionState { IDLE, CREATING, ACTIVE, DESTROYING }
export class SessionManager {
private state: SessionState = SessionState.IDLE;
private current: ReconSession | null = null;
private pendingConfig: ReconConfig | null = null;
// 外部调用:切换到新场景
async switchTo(config: ReconConfig): Promise<ReconSession> {
if (this.state !== SessionState.IDLE) {
this.pendingConfig = config; // 只保留最后一个
await this.drainPending();
return this.switchTo(config);
}
return this.doCreate(config);
}
private async doCreate(config: ReconConfig): Promise<ReconSession> {
this.state = SessionState.CREATING;
try {
const session = await spatialRecon.createReconSession(config);
this.current = session;
this.state = SessionState.ACTIVE;
return session;
} catch (e) {
this.state = SessionState.IDLE; // 失败必须回 IDLE,否则死锁
throw e;
}
}
private async destroyCurrent(): Promise<void> {
if (this.current === null || this.state === SessionState.DESTROYING) return;
this.state = SessionState.DESTROYING;
const session = this.current;
this.current = null;
try {
await session.destroy();
} finally {
await this.delay(120); // 保护延迟,避开 NPU 清理窗口
this.state = SessionState.IDLE;
}
}
private async drainPending(): Promise<void> {
const pending = this.pendingConfig;
if (pending === null) return;
this.pendingConfig = null;
await this.destroyCurrent();
await this.doCreate(pending);
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
这段代码的 120ms 保护延迟是实测出来的。试过 50/80/120/200ms 四档,50ms 仍有约 5% 冲突率,80ms 降到 1.2%,120ms 在 200 次切换里 0 次冲突,200ms 也是 0 但用户感知卡顿。120ms 是我们这台机器上的甜点值,换设备可能要调。
被放弃的方案:我们试过纯 Promise 链不加状态机,结果快速切换下仍出现状态混乱,因为 Promise 链没法取消中间的请求。状态机是必须的。
session 状态机
IDLE ──switchTo──> CREATING ──ok──> ACTIVE ──switchTo──> DESTROYING ──destroy.resolve+120ms──> IDLE
│ ▲
└──fail──> ERROR ───────────────────────────────────────────────────┘
状态机里最容易被忽略的是 ERROR 分支。createReconSession 失败时,session 引用无效,但状态必须回 IDLE,否则后续所有 switchTo 都被卡在非 IDLE 状态。我们第一版漏了这个分支,结果一次创建失败后整个 manager 死锁。
真机数据:销毁与创建耗时分布
Mate 90(9030)上跑 100 次"创建-销毁-创建"循环,统计耗时分布:
| 操作 | p50 | p90 | p99 | max |
|---|---|---|---|---|
| createReconSession | 38ms | 52ms | 78ms | 124ms |
| session.destroy() | 142ms | 188ms | 245ms | 312ms |
| destroy resolve 后到可再创建 | 78ms | 165ms | 230ms | 295ms |
第三行是关键——destroy 的 Promise resolve 之后,到下一次 createReconSession 不报错,中间还要等 78-230ms。这个时间没在文档里写明,是实测出来的。
不同设备上保护延迟的甜点值也不同:9030 Pro 上 80ms 够用,9030 要 120ms,9020 要 200ms,中端机 7 系列要 350ms。中端机保护延迟比销毁本身还长,底层资源清理更慢,UI 要给"切换中…"提示,否则用户以为卡死。
几个边界 case
- 重建进行中调用 destroy:先中断重建再清理,耗时比空闲 destroy 多 200-400ms。中断中又触发 switchTo,pending 被覆盖,旧中断结果丢弃——这是我们想要的行为。
- 应用退后台再回前台:session 不自动销毁,但 NPU 资源可能被系统回收,回前台后第一次操作可能报
INVALID_SESSION,强制走 destroyCurrent + 重新创建。 - 重建完成的 session 一直不 destroy:内存不释放,长期占用 1.5-2GB,加了 5 分钟空闲自动销毁兜底。
小总结
- session 是单例,destroy 是异步,resolve 之后仍有资源清理窗口
- 销毁后创建需要保护延迟,时长按设备实测,120ms 在 9030 上够用
- 快速切换场景必须用状态机,纯 Promise 链无法处理取消
- createReconSession 失败时状态必须回 IDLE,否则 manager 死锁
- 应用切后台回前台要处理 INVALID_SESSION,强制重建 session
- 长期不用的 session 要自动销毁,否则内存常驻
session 创建销毁跑通后,下一步压一下快速切换的并发场景:写个脚本每 200ms 触发一次 switchTo,跑 100 次看冲突率和状态是否一致。我们这套状态机在 200 次/200ms 间隔下稳定,但间隔拉到 100ms 仍会偶发状态错乱,那个边界我们还没完全解决。
更多推荐

所有评论(0)