要讨论内存优化,首先要知道项目中最消耗内存的是什么?

就像 creator 工程中占用空间最多的一样,是资源,资源包括纹理,声音,数据等等

 

这里我们先了解下 creator 的资源在内存中的管理方式,之后再介绍其他的优化内容

 

01

存储形式

 

资源在加载完成后,会以 { uuid : cc.Asset } 的形式被缓存到 cc.assetManager.assets 中,以避免重复加载

arc ios mrc内存管理机制 creator内存管理机制_贴图

 

但是这也会造成内存和显存的持续增长,所以有些资源如果不再需要用到,可以通过 自动释放 或者 手动释放 的方式进行释放

释放资源将会销毁资源的所有内部属性,比如渲染层的相关数据,并移出缓存,从而释放内存和显存(对纹理而言)

 

cc.assetManager:管理资源的行为和信息,包括加载,释放等

cc.assetManager.assets :已加载资源的集合

 

02

引用计数

 

引用计数 是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程

 

资源在加载完成后,会返回 cc.Asset 实例, 所有 cc.Asset 实例都拥有成员函数 addRef 和 decRef,分别用于增加和减少引用计数

 

初始化引用计数



this._ref = 0;

 

资源的引用计数 +1



addRef () {
    this._ref++;
    return this;
}

 

资源的引用计数 -1,并尝试进行自动释放



decRef (autoRelease) {
    this._ref--;
    //接下来会对代码进行详细的解读
    autoRelease !== false && cc.assetManager._releaseManager.tryRelease(this);
    return this;
}

 

Asset Manager 只会 自动统计 资源之间的 静态引用,并不能真实地反应资源在游戏中被动态引用的情况,动态引用 还需要 开发者进行控制 以保证资源能够被正确释放

 

1静态引用

当开发者在编辑器中编辑资源时(例如场景、预制体、材质等),需要在这些资源的属性中配置一些其他的资源,例如在材质中设置贴图,在场景的 Sprite 组件上设置 SpriteFrame。那么这些引用关系会被记录在资源的序列化数据中,引擎可以通过这些数据分析出依赖资源列表,像这样的引用关系就是 静态引用

引擎对资源的 静态引用 的统计方式为:

  1. 在 动态加载 某个资源时,引擎会在底层加载管线中记录该资源所有 直接依赖资源 的信息,并将所有 直接依赖资源 的引用计数加 1,然后将该资源的引用计数初始化为 0
  2. 在释放资源时,取得该资源之前记录的所有 直接依赖资源 信息,并将所有依赖资源的引用计数减 1

因为在释放检查时,如果资源的引用计数为 0,才可以被自动释放。所以上述步骤可以保证资源的依赖资源无法先于资源本身被释放,因为依赖资源的引用计数肯定不为 0。也就是说,只要一个资源本身不被释放,其依赖资源就不会被释放,从而保证在复用资源时不会错误地进行释放

下面我们来看一个例子:

  1. 假设现在有一个 A 预制体,其依赖的资源包括 a 材质和 b 材质。a 材质引用了 α 贴图,b 材质引用了 β 贴图。那么在加载 A 预制体之后,a、b 材质的引用计数都为 1,α、β 贴图的引用计数也都为 1
  2. 假设现在又有一个 B 预制体,其依赖的资源包括 b 材质和 c 材质。则在加载 B 预制体之后,b 材质的引用计数为 2,因为它同时被 A 和 B 预制体所引用。而 c 材质的引用计数为 1,α、β 贴图的引用计数也仍为 1
  3. 此时释放 A 预制体,则 a,b 材质的引用计数会各减 1
  • a 材质的引用计数变为 0,被释放,所以贴图 α 的引用计数减 1 变为了 0,也被释放
  • b 材质的引用计数变为 1,被保留,所以贴图 β 的引用计数仍为 1,也被保留
  • 因为 B 预制体没有被释放,所以 c 材质的引用计数仍为 1,被保留

arc ios mrc内存管理机制 creator内存管理机制_引用计数_02

 

我们通过 creator 来了解下 assets

 

新建一个场景,不放入任何资源

arc ios mrc内存管理机制 creator内存管理机制_贴图_03

 

打印 assets



console.log(cc.assetManager.assets)

 

可以看到内存中的资源均为 cocos 的内置资源

arc ios mrc内存管理机制 creator内存管理机制_引用计数_04

 

在场景中放入 HelloWorld

arc ios mrc内存管理机制 creator内存管理机制_引用计数_05

 

启动游戏后,引擎在底层加载管线中调用 assets 的成员方法 addRef

arc ios mrc内存管理机制 creator内存管理机制_加载_06

 

再次打印 assets 及资源的引用计数



console.log(cc.assetManager.assets);
console.log("spriteFrame.refCount : " + this.sprite.spriteFrame.refCount);

 

arc ios mrc内存管理机制 creator内存管理机制_引用计数_05

 

会发现 assets 多了两项,uuid 分别是

6aa0aa6a-ebee-4155-a088-a687a6aadec4 

31bc895a-c003-4566-a9f3-2e54ae1c17dc

 

在编辑器中显示 HelloWorld 的 Texture2D 和 SpriteFrame 的 uuid,和上述的两个 uuid 完全匹配

arc ios mrc内存管理机制 creator内存管理机制_引用计数_05

 

图片的引用计数也增加为 1

arc ios mrc内存管理机制 creator内存管理机制_引用计数_04

 

如果存在两份 HelloWorld,但他们的 spriteFrame 是同一份

arc ios mrc内存管理机制 creator内存管理机制_引用计数_10

 

那么 cc.assetManager.assets 依然保持原样,但 spriteFrame 的 refCount 会变成 2

 

对于更复杂的资源引用情况,可以自己测试下 assets 及引用计数

 

补充知识点:Texture 和 SpriteFrame 资源类型

 

在 资源管理器 中,图像资源的左边会显示一个和文件夹类似的三角图标,点击就可以展开看到它的子资源(sub asset),每个图像资源导入后编辑器会自动在它下面创建同名的 SpriteFrame 资源。

arc ios mrc内存管理机制 creator内存管理机制_引用计数_04

 

SpriteFrame 是核心渲染组件 Sprite 所使用的资源,设置或替换 Sprite 组件中的 spriteFrame 属性,就可以切换显示的图像

 

为什么会有 SpriteFrame 这种资源?Texture 是保存在 GPU 缓冲中的一张纹理,是原始的图像资源。而 SpriteFrame 包含两部分内容:记录了 Texture 及其相关属性的 Texture2D 对象和纹理的矩形区域,对于相同的 Texture 可以进行不同的纹理矩形区域设置,然后根据 Sprite 的填充类型,如 SIMPLE、SLICED、TILED 等进行不同的顶点数据填充,从而满足 Texture 填充图像精灵的多样化需求。而 SpriteFrame 记录的纹理矩形区域数据又可以在资源的属性检查器中根据需求自由定义,这样的设置让资源的开发更为高效和便利。除了每个文件会产生一个 SpriteFrame 的图像资源(Texture)之外,我们还有包含多个 SpriteFrame 的图集资源(Atlas)类型

 

2动态引用

当开发者在编辑器中没有对资源做任何设置,而是通过代码动态加载资源并设置到场景的组件上,则资源的引用关系不会记录在序列化数据中,引擎无法统计到这部分的引用关系,这些引用关系就是 动态引用

 

使用 动态加载 资源来进行动态引用

 

  • 动态加载 resources 目录中的资源

arc ios mrc内存管理机制 creator内存管理机制_贴图_12



cc.resources.load("HelloWorld", cc.SpriteFrame, (err, assets: cc.SpriteFrame) => {
    this.sprite.spriteFrame = assets;
    console.log(cc.assetManager.assets);
    console.log("spriteFrame.refCount : " + this.sprite.spriteFrame.refCount);
});

 

  • 动态加载 bundle 目录中的资源

arc ios mrc内存管理机制 creator内存管理机制_引用计数_13



cc.assetManager.loadBundle("bundle", (err: Error, bundle: cc.AssetManager.Bundle) => {
    bundle.load("HelloWorld", cc.SpriteFrame, (err, assets: cc.SpriteFrame) => {
        this.sprite.spriteFrame = assets;
        console.log(cc.assetManager.assets);
        console.log("spriteFrame.refCount : " + this.sprite.spriteFrame.refCount);
    });
});

 

在资源加载完成后打印下 assets 及资源的引用计数

arc ios mrc内存管理机制 creator内存管理机制_加载_14

 

可以看到,资源加载完成后会将 SpriteFrame 资源设置到 Sprite 组件上,但引擎不会做特殊处理,SpriteFrame 的引用计数仍保持 0,此时需要我们手动来管理引用计数

 

增加引用计数



cc.resources.load("HelloWorld", cc.SpriteFrame, (err, assets: cc.SpriteFrame) => {
    this.sprite.spriteFrame = assets;
    this.sprite.spriteFrame.addRef();
    console.log(cc.assetManager.assets);
    console.log("spriteFrame.refCount : " + this.sprite.spriteFrame.refCount);
});

 

减少引用计数(为了避免过多的资源干扰视线,我们在触摸结束时减少引用计数)



onTouchEnd(event: cc.Event.EventTouch) {
    console.log("###");
    this.sprite.node.destroy();
    this.sprite.spriteFrame.decRef();
    console.log("spriteFrame.refCount : " + this.sprite.spriteFrame.refCount);
    this.sprite.spriteFrame = null;
    //在下一帧打印 assets
    this.scheduleOnce(()=>{
        console.log(cc.assetManager.assets);
    });
}

 

运行后的 log

arc ios mrc内存管理机制 creator内存管理机制_贴图_15

 

从 log 中可以看到,addRef 后,资源的引用计数变为 1,decRef 之后资源的引用计数在当前帧为 0,在下一帧,资源也从 assets 中被清除了

 

注意:

动态加载 的资源必须手动卸载,卸载方式

① 通过引用计数:addRef  和 decRef

② 直接释放:releaseAsset