Creator | 优化三剑客之内存!-程序员宅基地

官方文档:

资源加载:

https://docs.cocos.com/creator/manual/zh/scripting/dynamic-load-resources.html


资源释放:

https://docs.cocos.com/creator/manual/zh/asset-manager/release-manager.html

34fcdc7c0353e38cd2cf2c03f2f4ceb9.gif

设备对每个程序都有最大的内存分配限制,如果超过了这个阈值,会被系统强制关闭,造成 crash。

因此在开发的过程中,我们要在保证程序运行效率的前提下,尽量压缩程序运行时所占用的内存。

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

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


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

01

存储形式

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

2c733445f8773d396627077550a76a38.png

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

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

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

    2d3dd36f4511a9cac15293e31dfb7ca6.png

  2. 假设现在又有一个 B 预制体,其依赖的资源包括 b 材质和 c 材质。则在加载 B 预制体之后,b 材质的引用计数为 2,因为它同时被 A 和 B 预制体所引用。而 c 材质的引用计数为 1,α、β 贴图的引用计数也仍为 1

    8a23f4b232841ac7f32bdf86df9c3ba7.png

  3. 此时释放 A 预制体,则 a,b 材质的引用计数会各减 1

  • a 材质的引用计数变为 0,被释放,所以贴图 α 的引用计数减 1 变为了 0,也被释放

  • b 材质的引用计数变为 1,被保留,所以贴图 β 的引用计数仍为 1,也被保留

  • 因为 B 预制体没有被释放,所以 c 材质的引用计数仍为 1,被保留

    3e241d50290d67a709320c6bdf5cda91.png

0bd0dbbafc07aaaa31d8692f2fa1aa57.gif

我们通过 creator 来了解下 assets

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

e5372efe33e1b0bca2df9128590af473.png

打印 assets

console.log(cc.assetManager.assets)

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

42f3dab30c53c6bd952db4fe23c4f2d7.png

在场景中放入 HelloWorld

6f0bae739e58bc5d76745b56c8f261cf.png

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

c9264db30531544a70dd1f132d64a873.png

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

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

37f3e97c5cba2e4e56525f2449c87464.png

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

6aa0aa6a-ebee-4155-a088-a687a6aadec4 

31bc895a-c003-4566-a9f3-2e54ae1c17dc

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

b0aa925e37c91bd6340e59e2d6ef4eaa.png

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

97e6c21b8615872df94c252333b163e0.png

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

38df30e28746d7469774880f1fc8e841.png

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

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

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

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

94e54dc5ac676fe3d5441d928b5e4cc6.png

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

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

2动态引用

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

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

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

8ab27596274f091a66466731b6bfba27.png

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 目录中的资源

a4876473eb8773403332a711b2aab2b2.png

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 及资源的引用计数

a1bfa1a641b09a27522dfb46c8708d18.png

可以看到,资源加载完成后会将 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

74a73bab6b54f7bbaa392bcf3c650173.png

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

注意:

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

① 通过引用计数:addRef  和 decRef

② 直接释放:releaseAsset

在资源加载完成后,会被临时缓存到 cc.assetManager.assets 中,以便下次复用。但是这也会造成内存和显存的持续增长,所以有些资源如果不需要用到,可以通过 自动释放 或者 手动释放 的方式进行释放。释放资源将会销毁资源的所有内部属性,比如渲染层的相关数据,并移出缓存,从而释放内存和显存(对纹理而言)

3自动释放

① 场景自动释放

在 资源管理器 选中场景后,属性检查器 中会出现 自动释放资源 选项。

1258b2cf25427fd6f6f5ff021694b514.png

勾选后,点击右上方的 应用 按钮,之后在切换该场景时便会自动释放该场景所有 静态引用 的依赖资源。建议场景尽量都勾选自动释放选项,以确保内存占用较低,除了部分高频使用的场景(例如主场景)。

② 资源自动释放

所有 cc.Asset 实例都拥有成员函数 addRef 和 decRef,分别用于增加和减少引用计数。一旦引用计数为零,Creator 会对资源进行自动释放(需要先通过释放检查,具体可参考下部分内容的介绍)。

start () {
    cc.resources.load('images/background', cc.Texture2D, (err, texture) => {
        this.texture = texture;
        // 当需要使用资源时,增加其引用
        texture.addRef();


    });
}


onDestroy () {
    // 当不需要使用资源时,减少引用
    // Creator 会在调用 decRef 后尝试对其进行自动释放
    this.texture.decRef();
}

自动释放的优势在于不用显式地调用释放接口,开发者只需要维护好资源的引用计数,Creator 会根据引用计数自动进行释放。这大大降低了错误释放资源的可能性,并且开发者不需要了解资源之间复杂的引用关系。对于没有特殊需求的项目,建议尽量使用自动释放的方式来释放资源。

4手动释放

当项目中使用了更复杂的资源释放机制时,可以调用 Asset Manager 的相关接口来手动释放资源。

cc.assetManager.releaseAsset(texture);

说明

  1. cc.assetManager.releaseAsset 接口仅能释放单个资源,且为了统一,接口只能通过资源本身来释放资源,不能通过资源 uuid、资源 url 等属性进行释放

  2. 在释放资源时,开发者只需要关注资源本身,引擎会 自动释放 其依赖资源(getDeps)

注意:

release 系列接口(例如 release、releaseAsset、releaseAll)会直接释放资源,而不会进行释放检查,只有其依赖资源会进行释放检查。所以当显式调用 release 系列接口时,可以确保资源本身一定会被释放。

5释放检查

为了避免错误释放正在使用的资源造成渲染或其他问题,Creator 会在自动释放资源之前进行一系列的检查,只有检查通过了,才会进行自动释放。

  1. 如果资源的引用计数为 0,即没有其他地方引用到该资源,则无需做后续检查,直接摧毁该资源,移除缓存

  2. 资源一旦被移除,会同步触发其依赖资源的释放检查,将移除缓存后的资源的 直接 依赖资源(不包含后代)的引用都减 1,并同步触发释放检查

  3. 如果资源的引用计数不为 0,即存在其他地方引用到该资源,此时需要进行循环引用检查,避免出现自己的后代引用自己的情况。如果循环引用检查完成之后引用计数仍不为 0,则终止释放,否则直接摧毁该资源,移除缓存,并触发其依赖资源的释放检查(同步骤 2)

a53582f60292e1d82b1799ff153e6d24.gif

我们通过 creator 来了解下资源释放的过程

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

bbc22c66c27f9c57e757b60c0ff6de35.png

打印 assets

console.log(cc.assetManager.assets)

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

012acf380ce6029a948725111958bd68.png

在场景中放入 HelloWorld

2227e14a1027573844dd97139fb06566.png

为了避免过多的资源干扰视线,我们在触摸结束时 手动释放 该节点的图片资源

onTouchEnd(event: cc.Event.EventTouch) {
    cc.assetManager.releaseAsset(this.sprite.spriteFrame);
    console.log(cc.assetManager.assets);
}

再次打印 assets,可以看到释放该资源后,assets 又回到了初始状态

6fbcd5fc5b154ec96b169d4a581d29d9.png

那么 releaseAsset 究竟做了什么?

查阅 assets 相关的源码

52c368f7254cfa9e2fbc84764034a52e.png

断点+单步调试,可以快速的理清脉络

8ad1a5a9d314629abb01b6947c901939.png

db4c771104a9399453a79e0a0115c92a.png

整理后的大致流程:

9109d9cdf9eeafc19791bd00c0dcf5f6.png

下面是对源码的一些注释,配合流程图服用,效果更佳

手动释放

releaseAsset (asset) {
    //强制释放
    releaseManager.tryRelease(asset, true);
}

减少资源的引用并尝试进行自动释放

decRef (autoRelease) {
    this._ref--;
    autoRelease !== false && cc.assetManager._releaseManager.tryRelease(this);
    return this;
}

尝试进行释放

tryRelease (asset, force) {
    if (!(asset instanceof cc.Asset)) return;
    if (force) {
        //强制释放
        releaseManager._free(asset, force);
    }
    else {
        //非强制释放则添加到待删除队列
        _toDelete.add(asset._uuid, asset);
        if (!eventListener) {
            //已监听渲染过程之后所触发的事件
            eventListener = true;
            //渲染过程之后执行释放
            cc.director.once(cc.Director.EVENT_AFTER_DRAW, freeAssets);
        }
    }
}

尝试自动去释放依赖资源并释放该资源

_free (asset, force) {
    //从待删除队列中移除
    _toDelete.remove(asset._uuid);


    if (!cc.isValid(asset, true)) return;


    if (!force) {
        //非强制释放则判断引用计数
        if (asset.refCount > 0) {
            //检查该资源的循环引用,返回其引用计数
            if (checkCircularReference(asset) > 0) return; 
        }
    }


    // remove from cache
    assets.remove(asset._uuid);
    //获取资源直接引用的非原生依赖列表,例如,材质的非原生依赖是 Texture
    var depends = dependUtil.getDeps(asset._uuid);
    for (let i = 0, l = depends.length; i < l; i++) {
        var dependAsset = assets.get(depends[i]);
        if (dependAsset) {
            //减少资源的引用计数
            dependAsset.decRef(false);
            releaseManager._free(dependAsset, false);
        }
    }
    asset.destroy();
    dependUtil.remove(asset._uuid);
}

释放待删除队列中的资源

function freeAssets () {
    eventListener = false;
    _toDelete.forEach(function (asset) {
        releaseManager._free(asset);
    });
    _toDelete.clear();
}

ada8ba56fd7efa682cfff0549d89cf06.gif

最后一个值得关注的要点:JavaScript 的垃圾回收是延迟的

在 C 与 C++ 等语言中,开发人员可以直接控制内存的申请和回收,而 JavaScript 所有对象的内存都由垃圾回收机制来管理,会周期性对那些我们不再使用的变量、对象所占用的内存进行释放,这就导致 JS 层逻辑永远不知道一个对象会在什么时候被释放。

想象一种情况,当你释放了 AssetManager 对某个资源的引用之后,由于考虑不周的原因,游戏逻辑再次请求了这个资源,这时垃圾回收还没有开始(垃圾回收的时机不可控)。

当出现这个情况时,意味着这个资源还存在内存中,但是 AssetManager 已经访问不到了,所以会重新加载它,就会造成这个资源在内存中有两份同样的拷贝,一份为刚刚请求的,另一份为已经释放但未被回收的,形成资源在内存中 暂时性 的 冗余。

之所以说暂时性,是因为在下个 GC 周期时,该资源依然会被回收,释放对应的内存。

如果只是一个资源还好,但是如果类似的资源很多,甚至不止一次被重复加载,就会造成当前时间内存飙升,而且频繁GC也会影响游戏的流畅性

因此我们释放资源时,应该 避免频繁释放,同时 避免释放近期内将要复用的资源。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_44053279/article/details/129573170

智能推荐

前端设置条件限制form表单提交到后端解决方案_jsp前端页面将表单是否提交成功作为限制条件-程序员宅基地

文章浏览阅读375次。<script src="js/jquery-1.8.3.min.js" type="text/javascript"></script> <script type="text/javascript"> function checkName() { var name = document.getElementB..._jsp前端页面将表单是否提交成功作为限制条件

计算机网络sequence number,TCP协议中SequenceNumber和Ack Numbe-程序员宅基地

文章浏览阅读1k次。Sequence Numberlzyws7393074532892018-04-25Number Sequenceqq_391789932452017-09-21理解TCP序列号(Sequence Number)和确认号(Acknowledgment Number)hebbely9822017-01-14Number Sequence(规律)l25336363712902017-07-18Numb..._ack num

计算机系统启动项设置密码,电脑开机第一道密码怎么设置 - 卡饭网-程序员宅基地

文章浏览阅读5.9k次。笔记本电脑怎么进CMOS密码巧设置笔记本电脑怎么进CMOS密码巧设置 笔记本电脑为了保护用户的数据安全,往往采用加密的方式,最常见的还是CMOS密码加密技术。为了让你的重要数据更加安全,你可能需要设置不同的密码,这也就要求你记住许多密码。对于笔记本电脑用户来说,真的需要设置一道道密码关卡吗?非也非也! 一、认识与设置笔记本电脑的CMOS密码 笔记本电脑的CMOS密码大致分为超级密码(Supervi..._电脑第一道密码修改

VulnHub靶机-Jangow: 1.0.1_jangow01-程序员宅基地

文章浏览阅读2.5k次,点赞2次,收藏5次。迟到的文章,就当库存发出来吧~_jangow01

spark实战之RDD的cache或persist操作不会触发transformation计算_spark cache和persist不生效-程序员宅基地

文章浏览阅读1.7w次,点赞2次,收藏5次。默认情况下RDD的transformation是lazy形式,实际计算只有在ation时才会进行,而且rdd的计算结果默认都是临时的,用过即丢弃,每个action都会触发整个DAG的从头开始计算,因此在迭代计算时都会想到用cache或persist进结果进行缓存。敝人看到很多资料或书籍有的说是persist或cache会触发transformation真正执行计算,有的说是不会!敝人亲自实验了一把..._spark cache和persist不生效

html文字滚动_html滚动-程序员宅基地

文章浏览阅读2.4k次。HTML之marquee(文字滚动)详解语法:以下是一个最简单的例子:代码如下:Hello, World下面这两个事件经常用到:onMouseOut=“this.start()” :用来设置鼠标移出该区域时继续滚动onMouseOver=“this.stop()”:用来设置鼠标移入该区域时停止滚动代码如下:onMouseOut=“this.start()” :用来设置鼠标移出该区域时继续滚动 onMouseOver=“this.stop()”:用来设置鼠标移入该区域时停止滚动这是一个完_html滚动

随便推点

信号发生器设计VHDL代码Quartus仿真_vhdl正弦波信号发生器-程序员宅基地

文章浏览阅读1k次,点赞20次,收藏22次。代码功能:信号发生器设计信号发生器由波形选择开关控制波形的输出,分别能输出正弦波、方汉和三角波三种波形,波形的周期为2秒(由40M有源晶振分频控制)。考虑程序的容量,每种波形在一个周期内均取16个取样点,每个样点数据是8位(数值范围:00000000~1111111)要求将D/A变换前的8位二进数据(以十进制方式)输出到数码管动态演示出来。_vhdl正弦波信号发生器

笔记-Java线程概述_java 线程概述-程序员宅基地

文章浏览阅读629次。Java Concurrency in Practice中对线程安全的定义:当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替运行,并且不需要额外的同步及在调用方代码不必做其他的协调,这个类的行为仍然是正确的,那么这个类就是线程安全的。显然只有资源竞争时才会导致线程不安全,因此无状态对象永远是线程安全的 。过多的同步会产生死锁的问题,死锁属于程序运行的时_java 线程概述

MATLAB从文件读取数据_matlab读取数据-程序员宅基地

文章浏览阅读1.2w次,点赞10次,收藏61次。读取表单Sheet2中部分信息。_matlab读取数据

【实践】基于spark的CF实现及优化_spark cf-程序员宅基地

文章浏览阅读1.4w次。最近项目中用到ItemBased Collaborative Filtering,实践过spark mllib中的ALS,但是因为其中涉及到降维操作,大数据量的计算实在不能恭维。所以自己实践实现基于spark的分布式cf,已经做了部分优化。目测运行效率还不错。以下代码package modelimport org.apache.spark.broadcast.Broadcastimp_spark cf

ijkplayer直播播放器使用经验之谈——卡顿优化和秒开实现_libijkplayer 播放直播流卡顿-程序员宅基地

文章浏览阅读1.8w次。 在我的博客移动平台播放器ijkplayer开源框架分析(以IOS源码为例),大致介绍了一下ijkplayer的基本函数调用顺序和主要线程作用,本博客想介绍一下在直播应用中,针对卡顿和秒开做的一些优化,本优化经验主要是用在Android系统上,ios上也可以借鉴,按本博客修改代码,网络带宽足够的情况下,音视频播放基本流畅不卡顿,首屏时间在500ms以内。 首先来看直播应用中的卡顿。直..._libijkplayer 播放直播流卡顿

数据挖掘实践(金融风控-贷款违约预测)(三):特征工程_金融风控(大数据)特征工程-程序员宅基地

文章浏览阅读2.3k次,点赞3次,收藏28次。数据挖掘实践(金融风控-贷款违约预测)(三):特征工程目录数据挖掘实践(金融风控-贷款违约预测)(三):特征工程1.引言2.特征预处理2.1缺失值填充2.2时间格式处理2.3类别特征处理3.异常值处理3.1 检测异常的方法一:正态分布法3.2 检测异常的方法二:箱型图3.3异常值的处理方法4.数据分桶5.特征交互6.特征编码6.1 labelEncode 直接放入树模型中6.2 逻辑回归等模型要单独增加的特征工程7.特征选择7.1 Filter7.2 Wrapper (Recursive feature _金融风控(大数据)特征工程