官方文档:

资源加载:

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

资源释放:

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

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

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

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

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

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

01

存储形式

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

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

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

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,被保留

我们通过 creator 来了解下 assets

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

打印 assets

console.log(cc.assetManager.assets)

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

在场景中放入 HelloWorld

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

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

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

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

6aa0aa6a-ebee-4155-a088-a687a6aadec4

31bc895a-c003-4566-a9f3-2e54ae1c17dc

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

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

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

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

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

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

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

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

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

2动态引用

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

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

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

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

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

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

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

注意:

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

① 通过引用计数:addRef  和 decRef

② 直接释放:releaseAsset

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

3自动释放

① 场景自动释放

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

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

② 资源自动释放

所有 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)

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

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

打印 assets

console.log(cc.assetManager.assets)

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

在场景中放入 HelloWorld

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

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

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

那么 releaseAsset 究竟做了什么?

查阅 assets 相关的源码

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

整理后的大致流程:

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

手动释放

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();}

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

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

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

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

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

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

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

cocos creator 方法数组_Creator | 优化三剑客之内存!相关推荐

  1. cocos creator 方法数组_基于 Cocos 游戏引擎的音视频研发探索

    本文转载自公众号:流利说技术团队(lls_tech) 版权归原作者所有 本文主要介绍了流利说团队基于 Cocos 游戏引擎进行音视频相关需求开发过程中所遇到的问题和解决方案.文章中将依次阐述 Coco ...

  2. Cocos Creator 性能调优优化集锦

    01 为什么要做性能优化? 性能:是一种优秀的能力.唤醒快.运行持久.稳定. 这种能力在游戏上能让你的用户感觉很爽,表征表现为加载快.手机不发热.运行流畅.不卡顿. 所以,性能优化的终极目标是让你的用 ...

  3. cocos creator 判断双击_Creator | 编辑器中可操作顶点的多边形遮罩

    感谢群内大佬 honmono 的分享,也欢迎同学们入群交流 QQ群:521643513 Mac 下 cocos 引擎源码位于 CocosCreator.app/Contents/Resources/e ...

  4. Cocos Creator 2.3.3 更新说明,效率即是一切!

    效率即是一切,Cocos Creator 2.3.3 正式版来啦!本次更新带来了更多新的特性,优化了性能以及提升了稳定性,希望能为广大开发者们保驾护航.建议所有开发者升级哦! 以下是 Cocos Cr ...

  5. Cocos Creator 性能优化:DrawCall

    Cocos Creator 性能优化:DrawCall(全面!) title: Cocos Creator 性能优化:DrawCall 前言 在游戏开发中,DrawCall 作为一个非常重要的性能指标 ...

  6. Cocos Creator性能优化---DrawCall

    前言 在游戏开发中,DrawCall 作为一个非常重要的性能指标,直接影响游戏的整体性能表现. 无论是 Cocos Creator.Unity.Unreal 还是其他游戏引擎,只要说到游戏性能优化,D ...

  7. Cocos Creator ScrollView 性能优化

    9月份 Cocos 技术开发分享会第 2 期在深圳圆满举行,近期我们将对活动干货进行整理,陆续在公众号上发布,没能去到深圳现场的开发者可以关注一下!对于分享的议题有哪些疑惑也欢迎在文末给我们留言! 本 ...

  8. Cocos Creator中按钮组件数组的使用

    Cocos Creator游戏开发中经常使用到按钮,特别是大量按钮的情况,此时使用数组来管理这些按钮就显得更具通用性.我大致走了一下官方的示例,好像没有发现有这个小内容(或者有,但我却是没有找到),于 ...

  9. Cocos Creator 基于 Spine 动画的 AVATAR 换装系统优化

    很多游戏开发团队都正在使用 Spine 动画软件来制作人物 AVATAR 动画.今天,玩吧技术专家组的红孩儿将以玩吧 APP 中的游戏<噜噜喵>为例,同大家分享基于 Spine 动画的 A ...

最新文章

  1. 函数或变量 rtenslearn_c 无法识别_Stata:过度识别检验一文读懂
  2. WIN7+wampserver2.4+zend stadio10.6.1配置Xdebug
  3. 基于busybox构建rootfs
  4. 第七十六期:糟糕!服务器被植入挖矿木马,CPU飙升200%
  5. c语言基础编程题讲解,C语言入门例题讲解
  6. 初次安装Mysql5.7以上版本后初始root密码找不到的问题
  7. TabLayout+ViewPager+Fragment(内部:TabLayout+ViewPager+ Fragment)需要注意!!
  8. java opencv 人脸相似度_java实现OpenCV 4.1.0人脸相似度对比
  9. Python 识别图片文字( Tesseract 安装使用 )
  10. 蓝星实物微商城H5源码 附搭建教程
  11. 新手怎么在GitHub上传代码?----最新教程
  12. 华盛顿道格拉斯县计划建立区块链创新园区
  13. GCD栅栏函数和信号量
  14. java实现qq邮箱发送附件和图片
  15. LeetCode 299猜数字游戏
  16. oracle varchar,date互转,number,varchar互转
  17. 零基础学Flink:Window Watermark
  18. 用Axure RP 9制作简易网易云首页
  19. 重载测试打印 - GoogleTest()
  20. Oracle数据库的启停

热门文章

  1. updatedb命令
  2. 服务器页是指包含什么脚本程序的网页,XSS攻击的本质就是被攻击者访问的页面返回页面中,包含了未经编码的脚本代码,如等信息。而浏览 - 众答网问答...
  3. go语言编程项目_一个项目需要多少种编程语言?
  4. 猎聘 大街_大街开放时
  5. jq怎么回到顶部和回到尾部_回到学校系列为孩子们提供开放资源
  6. 开源图书馆系统Evergreen奖励社区
  7. JavaScript类型转换的有趣应用
  8. 推荐算法实现java_利用Java写开源库 覆盖70多种推荐算法
  9. 程序设计基础(C语言)课程设计报告,C语言程序设计基础课程设计报告.doc
  10. oracle查效能,Oracle 11g物理Active Data Guard实时查询(Real-time query)特性