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

智能推荐

java调用视图如何传参_java – 在使用具有不同片段/布局的视图寻呼机时如何传递片段参数?...-程序员宅基地

文章浏览阅读67次。目标:使用片段参数将字符串值从TextView传递到新片段TextView,但在FragmentPagerAdapter中使用具有不同布局/片段的ViewPager时.问题:新片段永远不会从前一个片段接收片段参数.我的设置:我的Activity托管了ViewPager和FragmentPagerAdapter.我重写了FragmentPagerAdapters getItem(int positi..._java访问视图传参

Android file类使用详解-SDcard_sdcard_print_file-程序员宅基地

文章浏览阅读1w次,点赞4次,收藏12次。Android file类使用详解-SDcard_sdcard_print_file

CMake中添加Qt模块的合理方法_cmake测试代码中能定义qt类吗-程序员宅基地

文章浏览阅读1.2w次,点赞7次,收藏63次。用CMake来组织的工程中要用Qt首先要设置、找到Qt相关模块。主要是通过find_package这个CMake命令。但网上很多教程都过时了,或者不够清晰灵活。因为这部分很常用,所以特别用一篇文章把我们目前在生产环境中使用的方法给大家介绍下。设置Qt库路径Qt版本很多,我们的开发机上一般也装有多个不同版本的Qt。个人尝试性的项目一般用最新版的Qt,而真正发布的产品一般用的是LTS版本Qt(LTS:Long Term Support,长期支持版本,目前最新的LTS是5.9)。我们的方法是在系统中添_cmake测试代码中能定义qt类吗

在Linux系统编译DCMTK的源码得到其动态库文件和可执行程序_dicom动态库-程序员宅基地

文章浏览阅读2.2k次,点赞4次,收藏5次。Linux环境编译DCMTK源码,生成动态库文件和可执行文件_dicom动态库

详述 MySQL 导出数据遇到 secure-file-priv 的问题_mysql 导出select 数据 --secure-file-priv-程序员宅基地

文章浏览阅读416次。ERROR 1290 (HY000): The MySQL server is running with the –secure-file-priv option so it cannot execute this statement.对于上述错误,相信对于第一次执行 MySQL 数据导出操作的同学大都会遇见。至于为什么会遇到这个错误,原因很简单,那就是:我们不知道 MySQL 默认的_mysql 导出select 数据 --secure-file-priv

P1433 吃奶酪 —(状压DP)_p1433状态dp-程序员宅基地

文章浏览阅读854次,点赞20次,收藏17次。状态压缩动态规划,就是我们俗称的状压DP,是利用计算机二进制的性质来描述状态的一种DP方式。一只小老鼠要把它们都吃掉,问至少要跑多少距离?输出一行一个实数,表示要跑的最少距离,保留。第一行有一个整数,表示奶酪的数量。行,每行两个实数,第。对于全部的测试点,保证。,两点之间的距离公式为。_p1433状态dp

随便推点

php导出筛选后的数据_问卷星数据导出、筛选与分析-程序员宅基地

文章浏览阅读1.6k次。作者/排版/审核:陈明导读 问卷星是当前被广泛使用的线上问卷调查平台,特别是疫情期间,其快捷、易用、低成本的明显优势再度突出。一般而言,问卷调查的基本步骤包括:设计问卷→发放问卷→收集问卷→下载数据→分析数据。前期我们已经对问卷星的操作与使用进行了简单介绍,那么当数据收集完成后,如何将其导出、筛选以及初步分析值得我们进一步探索。解析 1、数据导出:登录问卷星网页→分析&下载→查看..._问卷星导出数据表格筛选怎么弄?

初次联系导师短信模板_20考研注意了:选导师应该关注哪些方面呢?-程序员宅基地

文章浏览阅读1.4k次。我们都知道,考研是为了能让自己在学术领域上得到更深造诣,那么要如何才能够在读研期间真正学习到知识、真正体现读研价值,导师无疑是至关重要的一环!1.科研能力研究生的科研能力是在导师的指导下形成的,如果导师的科研能力不行,仅靠自己要想在某方面有所建树就会比较困难。另外,报考前应该尽量对导师的科研项目和科研经费有一些了解。看看导师的科研经费是否充足,科研课题是否比较多,是否比较前沿。2.科研方..._选导师发短信的格式怎么比较礼貌

Android零基础入门第52节:自定义酷炫进度条_android mprogress_horizontal-程序员宅基地

文章浏览阅读1.2k次。Android系统默认的ProgressBar往往都不能满足实际开发需要,一般都会开发者自定义ProgressBar,一般有三种思路来完成。_android mprogress_horizontal

Zookeeper设置访问权限-程序员宅基地

文章浏览阅读1.2w次。[b][size=medium]zookeeper的身份认证有4种方式[/size][/b](1)world: 它下面只有一个id, 叫anyone, world:anyone代表任何人,zookeeper中对所有人有权限的结点就是属于world:anyone的 (2)auth: 它不需要id, 只要是通过authentication的user都有权限(zookeeper支持通过ker..._zookeeper 权限信息放哪里

SpringMVC——核心技术:异常处理(@ExceptionHandler、@ControllerAdvice)_spring @exceptionhandler message-程序员宅基地

文章浏览阅读8.7k次,点赞2次,收藏8次。SpringMVC——核心技术:异常处理(@ExceptionHandler、@ControllerAdvice)_spring @exceptionhandler message

操作系统文件系统实验报告16281027_i/o磁盘实验报告-程序员宅基地

文章浏览阅读3.6k次。实验五 文件系统1 实验简介本实验要求在模拟的I/O系统之上开发一个简单的文件系统。用户通过create, open, read等命令与文件系统交互。文件系统把磁盘视为顺序编号的逻辑块序列,逻辑块的编号为0至L − 1。2 I/O系统实际物理磁盘的结构是多维的:有柱面、磁头、扇区等概念。I/O系统的任务是隐藏磁盘的结构细节,把磁盘以逻辑块的面目呈现给文件系统。逻辑块顺序编号,编号取值范围为..._i/o磁盘实验报告

推荐文章

热门文章

相关标签