《HarmonyOS 7 ArkGraphics 3D 空间设计开发实战》03:glTF模型加载、实例复用与资源生命周期【鸿蒙心迹】
同一把椅子放三次,为什么加载了三次模型?

前言
前两篇搭好了工程骨架,也搞清楚了坐标体系。这一篇开始往房间里放真正的家具模型:沙发、桌子、椅子、台灯、绿植、柜子。
然后马上就遇到一个问题:我在房间里放了三把一样的椅子,放在不同位置。跑起来一看,内存涨了不少,加载时间也变长了。
我当时第一反应是:三把椅子而已,至于这么慢吗?
然后打印日志才发现:每放一把椅子,模型都重新加载了一遍。三个椅子节点,对应了三份完全一样的模型资源。
这就是 3D 开发里另一个常见误区:把 Resource(模型资源)和 Instance(场景实例)搞混了。一个椅子模型文件,只需要加载一次到内存里,然后可以创建出任意多个实例,每个实例有自己的位置、旋转和缩放,但共享同一份模型数据。
这一篇就解决这个问题:怎么建立模型资源缓存,怎么区分 Resource 和 Instance,以及模型异步加载回来的时候,页面已经退出了怎么办。
一、先搞清楚:Resource 和 Instance 不是一回事
很多初学者刚开始做 3D 的时候,以为"创建一个模型"就是加载一次文件。放一把椅子,加载一次;放两把椅子,再加载一次。这在家具少的时候看不出来,家具一多,内存和加载时间就都上去了。
先把概念拆开:
| 概念 | 是什么 | 有几个 | 举例 |
|---|---|---|---|
| Resource(资源) | 模型文件本身,包含Mesh、Material、纹理 | 同一个模型文件只加载一次 | chair.gltf |
| Instance(实例) | 场景里的一个节点,引用Resource | 同一个Resource可以有多个Instance | 三把椅子就是三个Instance |
| Node(节点) | 场景树里的对象,有Position、Rotation、Scale | 每个Instance对应一个Node | 每个椅子有自己的位置 |
打个比方:Resource 就像是椅子的设计图纸,Instance 就像是按照图纸做出来的真实椅子。图纸只需要一份,但可以做出很多把椅子,每把椅子可以放在不同的位置。
如果每次放一把椅子都重新读一遍图纸,那就太浪费了。
二、第一版:直接加载,放一个加载一个
先写第一版代码,看看问题出在哪:
import { scene } from '@kit.ArkGraphics3DKit';
import { SceneManager } from './SceneManager';
export class ModelManager {
constructor(private sceneMgr: SceneManager) {}
// 加载椅子模型并放到指定位置
addChair(position: {x: number, y: number, z: number}): void {
const scene = this.sceneMgr.getScene();
if (!scene) return;
// 每次都重新加载模型文件
const chairNode = scene.createNode('Chair');
// 加载 chair.gltf
chairNode.loadModel('resources/models/chair.gltf');
chairNode.position = position;
scene.getRoot().addChild(chairNode);
console.info('ModelManager: chair added at', position);
}
}
这个代码看起来没问题,但如果我调用三次 addChair(),放三把椅子,就会加载三次 chair.gltf。
这在小模型的时候可能感觉不出来,但如果是沙发、床这种大模型,每个都几MB甚至几十MB,放几个就把内存占满了。
更麻烦的是:模型加载是异步的。如果用户连续点了五次"添加椅子",就会触发五次模型加载请求,就算后面取消了,加载还是会继续。
三、第二版:加资源缓存,Resource 只加载一次
改进一下:加一个资源缓存表,记录已经加载过的模型。同一个模型文件,只加载一次,后面直接复用。
import { scene } from '@kit.ArkGraphics3DKit';
import { SceneManager } from './SceneManager';
// 模型资源缓存
interface ModelResource {
resource: scene.ModelResource | null; // 模型资源
refCount: number; // 引用计数
loading: boolean; // 是否正在加载
}
export class ModelResourceManager {
private resourceCache: Map<string, ModelResource> = new Map();
constructor(private sceneMgr: SceneManager) {}
/**
* 加载模型资源(带缓存)
*/
async loadModelResource(modelPath: string): Promise<scene.ModelResource | null> {
// 1. 先查缓存
const cached = this.resourceCache.get(modelPath);
if (cached && cached.resource) {
cached.refCount++;
console.info('ModelResourceManager: cache hit', modelPath);
return cached.resource;
}
// 2. 正在加载中,等待加载完成
if (cached && cached.loading) {
// 简单处理:轮询等待,实际工程里用Promise队列
await new Promise(resolve => setTimeout(resolve, 100));
return this.loadModelResource(modelPath);
}
// 3. 开始加载
this.resourceCache.set(modelPath, {
resource: null,
refCount: 0,
loading: true
});
try {
const scene = this.sceneMgr.getScene();
if (!scene) return null;
// 异步加载模型资源
const resource = await scene.loadModelResource(modelPath);
// 更新缓存
this.resourceCache.set(modelPath, {
resource: resource,
refCount: 1,
loading: false
});
console.info('ModelResourceManager: model loaded', modelPath);
return resource;
} catch (e) {
console.error('ModelResourceManager: load failed', modelPath, e);
this.resourceCache.delete(modelPath);
return null;
}
}
/**
* 创建模型实例
*/
createInstance(modelPath: string, position: {x: number, y: number, z: number}): scene.Node | null {
const scene = this.sceneMgr.getScene();
if (!scene) return null;
const node = scene.createNode(modelPath + '_' + Date.now());
// 实例使用已经加载好的资源
node.setModelResource(this.resourceCache.get(modelPath)?.resource ?? null);
node.position = position;
scene.getRoot().addChild(node);
return node;
}
}
现在放三把椅子,只会加载一次 chair.gltf。后面两把椅子直接复用缓存的资源。
这里有个关键概念:引用计数。每创建一个实例,refCount 加1;删除一个实例,refCount 减1。当 refCount 降到 0 的时候,说明这个资源没人用了,可以释放掉。

四、模型异步加载回来,页面已经退出了怎么办
模型加载是异步的,这个过程中可能发生很多事:
- 用户点了添加椅子,然后马上退出了页面
- 模型还在加载中,Scene 已经被释放了
- 加载完成的回调里,sceneMgr 已经是空的了
如果不处理这种情况,就会出现:页面都退出了,日志还在打印"模型加载完成",甚至还会有内存泄漏。
处理办法:加载的时候记录一个"是否有效"的标记,加载完成以后先检查一下:
async loadModelResource(modelPath: string): Promise<scene.ModelResource | null> {
// ...前面的加载逻辑
// 开始加载前,记录当前是否有效
const isValid = true;
try {
const resource = await scene.loadModelResource(modelPath);
// 加载完成后,检查一下页面是不是还在
if (!isValid || !this.sceneMgr.getScene()) {
console.warn('ModelResourceManager: page already closed, discard loaded resource');
// 直接释放刚加载的资源
resource?.release();
return null;
}
// ...正常更新缓存
}
}
这个问题在真实工程里特别常见。我第一次做的时候,用户快速进出页面好几次,内存涨得特别快,就是因为每次加载的模型都没释放。
五、删除实例的时候,记得释放引用
创建实例的时候 refCount 加了1,删除实例的时候就得减1,不然资源永远释放不掉。
/**
* 删除模型实例
*/
releaseInstance(node: scene.Node, modelPath: string): void {
const scene = this.sceneMgr.getScene();
if (!scene) return;
// 从场景树移除节点
node.removeFromParent();
// 引用计数减1
const cached = this.resourceCache.get(modelPath);
if (cached) {
cached.refCount--;
console.info('ModelResourceManager: refCount', modelPath, cached.refCount);
// 引用计数为0,释放资源
if (cached.refCount <= 0) {
cached.resource?.release();
this.resourceCache.delete(modelPath);
console.info('ModelResourceManager: resource released', modelPath);
}
}
}
这个逻辑很简单,但特别容易忘。很多人只记得创建实例,不记得删除的时候要减引用计数,结果就是退出页面以后,所有模型资源都还留在内存里。
六、几个最容易踩的资源坑
把这一篇遇到的坑总结一下:
| 坑 | 现象 | 解决办法 |
|---|---|---|
| 每个实例都重新加载模型 | 放N个家具就加载N次文件 | 加资源缓存,Resource只加载一次 |
| 忘了引用计数 | 退出页面后内存不释放 | 创建实例加1,删除实例减1,为0时释放 |
| 异步加载回来页面已退出 | 页面都没了还在加载模型 | 加载完成后检查页面状态,无效就丢弃 |
| 用户连续点击创建 | 触发重复加载请求 | 加载中的模型不再发新请求,等待即可 |
| Node删了但资源没释放 | 节点从场景树移除了,资源还在 | 删除节点时同步减引用计数 |
这些问题在家具少的时候都看不出来,一旦房间里有几十上百个模型,内存和性能问题就全暴露出来了。这也是为什么 Resource 和 Instance 要分开管理——不是为了写代码好看,是为了真的能在手机上跑起来。


总结
第三篇的核心就一句话:Resource 和 Instance 是两回事,别把它们混在一起。
- 同一个模型文件只需要加载一次,多个实例共享同一份资源
- 用引用计数管理资源生命周期:实例创建加1,实例删除减1,为0时释放
- 模型是异步加载的,加载完成后要检查页面是不是还在
- 节点从场景树移除的时候,记得同步释放资源引用
- 资源缓存不是什么高级优化,是 3D 应用能跑起来的基本要求
SpaceRoom 现在能加载多个家具了,而且不会重复加载。下一篇解决视觉效果:为什么东西都有了,但看起来像塑料——材质、纹理和灯光到底是怎么决定 3D 场景质感的。
更多推荐




所有评论(0)