Cocos Creator 新资源管理系统剖析
v2.4 开始,Cocos Creator 使用
AssetBundle完全重构了资源底层,提供了更加灵活强大的资源管理方式,也解决了之前版本资源管理的痛点(资源依赖与引用),本文将带你深入了解 Creator 的新资源底层。
目录
资源与构建 理解与使用 AssetBundle 新资源框架剖析 加载管线 文件下载 文件解析 依赖加载 资源释放
1.资源与构建
1.1 creator 资源文件基础
在了解引擎如何解析、加载资源之前,我们先来了解一下这些资源文件(图片、Prefab、动画等)的规则。
在 creator 项目目录下有几个与资源相关的目录:
assets 所有资源的总目录,对应 creator 编辑器的资源管理器 library 本地资源库,预览项目时使用的目录 build 构建后的项目默认目录

在 assets 目录下,creator 会为每个资源文件和目录生成一个同名的.meta文件,meta 文件是一个 json 文件,记录了资源的版本、uuid 以及各种自定义的信息(在编辑器的属性检查器中设置),比如 prefab 的 meta 文件,就记录了我们可以在编辑器修改的 optimizationPolicy 和 asyncLoadAssets 等属性。

{
"ver": "1.2.7",
"uuid": "a8accd2e-6622-4c31-8a1e-4db5f2b568b5",
"optimizationPolicy": "AUTO", // prefab创建优化策略
"asyncLoadAssets": false, // 是否延迟加载
"readonly": false,
"subMetas": {}
}
在 library 目录下的 imports 目录,资源文件名会被转换成 uuid,并取 uuid 前2个字符进行目录分组存放,creator 会将所有资源的 uuid 到 assets 目录的映射关系,以及资源和 meta 的最后更新时间戳放到一个名为 uuid-to-mtime.json 的文件中,如下所示:
{
"9836134e-b892-4283-b6b2-78b5acf3ed45": {
"asset": 1594351233259,
"meta": 1594351616611,
"relativePath": "effects"
},
"430eccbf-bf2c-4e6e-8c0c-884bbb487f32": {
"asset": 1594351233254,
"meta": 1594351616643,
"relativePath": "effects\\__builtin-editor-gizmo-line.effect"
},
...
}
与
assets目录下的资源相比,library目录下的资源合并了meta文件的信息。文件目录则只在uuid-to-mtime.json中记录,library目录并没有为目录生成任何东西。
1.2 资源构建
在项目构建之后,资源会从 library 目录下移动到构建输出的 build 目录中,基本只会导出参与构建的场景和 resources 目录下的资源,及其引用到的资源。脚本资源会由多个 js 脚本合并为一个 js,各种 json 文件也会按照特定的规则进行打包。
我们可以在 Bundle 的配置界面和项目的构建界面,为 Bundle 和项目设置:

1.2.1 图片、图集、自动图集
https://docs.cocos.com/creator/manual/zh/asset-workflow/sprite.html https://docs.cocos.com/creator/manual/zh/asset-workflow/atlas.html https://docs.cocos.com/creator/manual/zh/asset-workflow/auto-atlas.html
导入编辑器的每张图片都会对应生成一个 json 文件,用于描述 Texture 的信息。
如下所示,默认情况下项目中所有的 Texture2D 的 json 文件会被压缩成一个,如果选择无压缩,则每个图片都会生成一个 Texture2D 的 json 文件。
{
"__type__": "cc.Texture2D",
"content": "0,9729,9729,33071,33071,0,0,1"
}
如果将纹理的 Type 属性设置为 Sprite,Creator 还会自动生成了 SpriteFrame 类型的 json 文件。
图集资源除了图片外,还对应一个图集 json,这个 json 包含了 cc.SpriteAtlas 信息,以及每个碎图的 SpriteFrame 信息。
自动图集在默认情况下只包含了 cc.SpriteAtlas 信息,在勾选内联所有 SpriteFrame 的情况下,会合并所有 SpriteFrame。
1.2.2 Prefab与场景
https://docs.cocos.com/creator/manual/zh/asset-workflow/prefab.html https://docs.cocos.com/creator/manual/zh/asset-workflow/scene-managing.html
场景资源与 Prefab 资源非常类似,都是一个描述了所有节点、组件等信息的 json文件,在勾选内联所有 SpriteFrame 的情况下,Prefab 引用到的 SpriteFrame 会被合并到 prefab 所在的 json 文件中。
如果一个 SpriteFrame 被多个 prefab 引用,那么每个 prefab 的 json 文件都会包含该 SpriteFrame 的信息。
而在没有勾选内联所有 SpriteFrame 的情况下,SpriteFrame 会是单独的 json文件。
1.2.3 资源文件合并规则
当 Creator 将多个资源合并到一个 json 文件中,我们可以在 config.json 中的 packs 字段找到被打包的资源信息,一个资源有可能被重复打包到多个 json 中。
下面举一个例子,展示在不同的选项下,creator 的构建规则:
a.png 一个单独的Sprite类型图片 dir/b.png、c.png、AutoAtlas dir目录下包含2张图片,以及一个AutoAtlas d.png、d.plist 普通图集 e.prefab 引用了SpriteFrame a和b的prefab f.prefab 引用了SpriteFrame b的prefab
下面是按不同规则构建后的文件,可以看到,无压缩的情况下生成的文件数量是最多的,不内联的文件会比内联多,但内联可能会导致同一个文件被重复包含,比如 e 和 f 这两个 Prefab 都引用了同一个图片,这个图片的 SpriteFrame.json 会被重复包含,合并成一个 json 则只会生成一个文件。

默认选项在绝大多数情况下都是一个不错的选择。
如果是 web 平台,建议勾选内联所有 SpriteFrame 这可以减少网络 io,提高性能。
原生平台则不建议勾选,这可能会增加包体大小以及热更时要下载的内容。
对于一些紧凑的 Bundle(比如加载该 Bundle 就需要用到里面所有的资源),我们可以配置为合并所有的 json。
2. 理解与使用 Asset Bundle
2.1 创建 Bundle
Asset Bundle 是 creator 2.4 之后的资源管理方案,简单地说,就是通过目录来对资源进行规划,按照项目的需求将各种资源放到不同的目录下,并将目录配置成 Asset Bundle。能够起到以下作用:
加快游戏启动时间 减小首包体积 跨项目复用资源 方便实现子游戏 以Bundle为单位的热更新
Asset Bundle 的创建非常简单,只要在目录的属性检查器中勾选配置为 bundle 即可,其中的选项官方文档都有比较详细的介绍。
其中关于压缩的理解,文档并没有详细的描述,这里的压缩指的并不是 zip 之类的压缩,而是通过
packAssets的方式,把多个资源的json文件合并到一个,达到减少io的目的。

在选项上打勾非常简单,真正的关键在于如何规划 Bundle。规划的原则在于减少包体、加速启动以及资源复用。根据游戏的模块来规划资源是比较不错的选择,比如按子游戏、关卡副本、或者系统功能来规划。
Bundle 会自动将文件夹下的资源,以及文件夹中引用到的其它文件夹下的资源打包(如果这些资源不是在其它 Bundle 中),如果我们按照模块来规划资源,很容易出现多个 Bundle 共用了某个资源的情况。
可以将公共资源提取到一个 Bundle 中,或者设置某个 Bundle 有较高的优先级,构建 Bundle 的依赖关系,否则这些资源会同时放到多个 Bundle 中(如果是本地 Bundle,这会导致包体变大)。
2.2 使用 Bundle
关于加载资源 https://docs.cocos.com/creator/manual/zh/scripting/load-assets.html 关于释放资源 https://docs.cocos.com/creator/manual/zh/asset-manager/release-manager.html
Bundle 的使用也非常简单,如果是 resources 目录下的资源,可以直接使用 cc.resources.load 来加载。
cc.resources.load("test assets/prefab", function (err, prefab) {
var newNode = cc.instantiate(prefab);
cc.director.getScene().addChild(newNode);
});
如果是其他自定义 Bundle(本地 Bundle 或远程 Bundle 都可以用 Bundle 名加载),可以使用 cc.assetManager.loadBundle 来加载 Bundle,然后使用加载后的 Bundle 对象,来加载 Bundle 中的资源。
对于原生平台,如果 Bundle 被配置为远程包,在构建时需要在构建发布面板中填写资源服务器地址。
cc.assetManager.loadBundle('01_graphics', (err, bundle) => {
bundle.load('xxx');
});
原生或小游戏平台下,我们还可以这样使用 Bundle:
如果要加载其它项目的远程 Bundle,则需要使用 url 的方式加载(其它项目指另一个 cocos 工程)
如果希望自己管理 Bundle 的下载和缓存,可以放到本地可写路径,并传入路径来加载这些 Bundle
// 当复用其他项目的 Asset Bundle 时
cc.assetManager.loadBundle('https://othergame.com/remote/01_graphics', (err, bundle) => {
bundle.load('xxx');
});
// 原生平台
cc.assetManager.loadBundle(jsb.fileUtils.getWritablePath() + '/pathToBundle/bundleName', (err, bundle) => {
// ...
});
// 微信小游戏平台
cc.assetManager.loadBundle(wx.env.USER_DATA_PATH + '/pathToBundle/bundleName', (err, bundle) => {
// ...
});
其它注意项:
加载 Bundle仅仅只是加载了Bundle的配置和脚本而已,Bundle中的其它资源还需要另外加载目前原生的 Bundle并不支持 zip 打包,远程包下载方式为逐文件下载,好处是操作简单,更新方便,坏处是 io 多,流量消耗大不同 Bundle下的脚本文件不要重名一个 Bundle A依赖另一个Bundle B,如果B没有被加载,加载A时并不会自动加载B,而是在加载A中依赖B的那个资源时报错
3. 新资源框架剖析
v2.4重构后的新框架代码更加简洁清晰,我们可以先从宏观角度了解一下整个资源框架,资源管线是整个框架最核心的部分,它规范了整个资源加载的流程,并支持对管线进行自定义。
公共文件:
helper.js定义了一堆公共函数,如decodeUuid、getUuidFromURL、getUrlWithUuid等等utilities.js定义了一堆公共函数,如getDepends、forEach、parseLoadResArgs等等deserialize.js定义了deserialize方法,将json对象反序列化为Asset对象,并设置其__depends__属性depend-util.js控制资源的依赖列表,每个资源的所有依赖都放在_depends成员变量中cache.js通用缓存类,封装了一个简易的键值对容器shared.js定义了一些全局对象,主要是Cache和Pipeline对象,如加载好的assets、下载完的files以及bundles等
Bundle 部分:
config.js bundle的配置对象,负责解析bundle的config文件bundle.js bundle类,封装了config以及加载卸载bundle内资源的相关接口builtins.js内建bundle资源的封装,可以通过cc.assetManager.builtins访问
管线部分:
CCAssetManager.js管理管线,提供统一的加载卸载接口管线框架
pipeline.js实现了管线的管道组合以及流转等基本功能task.js定义了一个任务的基本属性,并提供了简单的任务池功能request-item.js定义了一个资源下载项的基本属性,一个任务可能会生成多个下载项预处理管线
urlTransformer.js parse将请求参数转换成RequestItem对象(并查询相关的资源配置),combine负责转换真正的urlpreprocess.js过滤出需要进行url转换的资源,并调用transformPipeline下载管线
download-dom-audio.js提供下载音效的方法,使用audio标签进行下载download-dom-image.js提供下载图片的方法,使用Image标签进行下载download-file.js提供下载文件的方法,使用XMLHttpRequest进行下载download-script.js提供下载脚本的方法,使用script标签进行下载downloader.js支持下载所有格式的下载器,支持并发控制、失败重试、解析管线
factory.js创建Bundle、Asset、Texture2D等对象的工厂fetch.js调用packManager下载资源,并解析依赖parser.js对下载完成的文件进行解析其它
releaseManager.js提供资源释放接口、负责释放依赖资源以及场景切换时的资源释放cache-manager.d.ts在非 WEB 平台上,用于管理所有从服务器上下载下来的缓存pack-manager.js处理打包资源,包括拆包,加载,缓存等等
3.1 加载管线
creator 使用管线(pipeline)来处理整个资源加载的流程,这样的好处是解耦了资源处理的流程,将每一个步骤独立成一个单独的管道,管道可以很方便地进行复用和组合,并且方便了我们自定义整个加载流程,我们可以创建一些自己的管道,加入到管线中,比如资源加密。
AssetManager 内置了3条管线,普通的加载管线、预加载、以及资源路径转换管线,最后这条管线是为前面两条管线服务的。
// 正常加载
this.pipeline = pipeline.append(preprocess).append(load);
// 预加载
this.fetchPipeline = fetchPipeline.append(preprocess).append(fetch);
// 转换资源路径
this.transformPipeline = transformPipeline.append(parse).append(combine);
3.1.1 启动加载管线【加载接口】
接下来,我们看一下一个普通的资源是如何加载的,比如最简单的 cc.resource.load,在 bundle.load 方法中,调用了 cc.assetManager.loadAny,在 loadAny 方法中,创建了一个新的任务,并调用正常加载管线 pipeline 的 async 方法执行任务。

注意要加载的资源路径,被放到了 task.input中、options是一个对象,对象包含了 type、bundle 和 __requestType__ 等字段。
// bundle类的load方法
load (paths, type, onProgress, onComplete) {
var { type, onProgress, onComplete } = parseLoadResArgs(type, onProgress, onComplete);
cc.assetManager.loadAny(paths, { __requestType__: RequestType.PATH, type: type, bundle: this.name }, onProgress, onComplete);
},
// assetManager的loadAny方法
loadAny (requests, options, onProgress, onComplete) {
var { options, onProgress, onComplete } = parseParameters(options, onProgress, onComplete);
options.preset = options.preset || 'default';
let task = new Task({input: requests, onProgress, onComplete: asyncify(onComplete), options});
pipeline.async(task);
},
pipeline 由两部分组成 preprocess 和 load。preprocess 由以下管线组成 preprocess、transformPipeline { parse、combine },preprocess 实际上只创建了一个子任务,然后交由 transformPipeline 执行。对于加载一个普通的资源,子任务的 input 和 options 与父任务相同。
let subTask = Task.create({input: task.input, options: subOptions});
task.output = task.source = transformPipeline.sync(subTask);
3.1.2 transformPipeline 管线【准备阶段】
transformPipeline 由 parse 和 combine 两个管线组成,parse 的职责是为每个要加载的资源生成 RequestItem对象并初始化其资源信息(AssetInfo、uuid、config等):
先将 input转换成数组进行遍历,如果是批量加载资源,每个加载项都会生成RequestItem;
如果输入的 item 是 object,则先将 options 拷贝到 item 身上(实际上每个 item 都会是 object,如果是 string 的话,第一步就先转换成 object了)
对于 UUID类型的item,先检查bundle,并从bundle中提取AssetInfo,对于redirect类型的资源,则从其依赖的bundle中获取AssetInfo,找不到bundle就报错PATH类型和SCENE类型与UUID类型的处理基本类似,都是要拿到资源的详细信息DIR类型会从bundle中取出指定路径的信息,然后批量追加到input尾部(额外生成加载项)URL类型是远程资源类型,无需特殊处理
RequestItem的初始信息,都是从bundle对象中查询的,bundle的信息则是从bundle自带的config.json文件中初始化的,在打包bundle的时候,会将bundle中的资源信息写入config.json中。
经过 parse 方法处理后,我们会得到一系列 RequestItem,并且很多 RequestItem 都自带了 AssetInfo 和 uuid 等信息,combine 方法会为每个 RequestItem 构建出真正的加载路径,这个加载路径最终会转换到 item.url 中。
3.1.3 load管线【加载流程】

load 方法做的事情很简单,基本只是创建了新的任务,在 loadOneAssetPipeline 中执行每个子任务。
loadOneAssetPipeline 如其函数名所示,就是加载一个资源的管线,它分为2步,fetch 和 parse:
fetch方法:
用于下载资源文件,由 packManager 负责下载的实现,fetch 会将下载完的文件数据放到 item.file 中。
parse方法:
用于将加载完的资源文件转换成我们可用的资源对象:
对于原生资源,调用 parser.parse 进行解析,该方法会根据资源类型调用不同的解析方法
import资源调用parseImport方法,根据json数据反序列化出Asset对象,并放到assets中图片资源会调用 parseImage、parsePVRTex或parsePKMTex方法解析图像格式(但不会创建Texture对象)音效资源调用 parseAudio方法进行解析plist资源调用parsePlist方法进行解析
对于其它资源,如果 uuid在 task.options.__exclude__ 中,则标记为完成,并添加引用计数;否则,根据一些复杂的条件来决定是否加载资源的依赖。
3.2 文件下载
creator 使用 packManager.load 来完成下载的工作,当要下载一个文件时,有2个问题需要考虑:
该文件是否被打包了?比如由于勾选了内联所有 SpriteFrame,导致 SpriteFrame 的 json 文件被合并到 prefab 中
当前平台是原生平台还是 web 平台?对于一些本地资源,原生平台需要从磁盘读取。
3.2.1 Web 平台的下载
web 平台的 download 实现如下:
用一个 downloaders数组来管理各种资源类型对应的下载方式使用 files缓存来避免重复下载使用 _downloading队列来处理并发下载同一个资源时的回调,并保证时序支持了下载的优先级、重试等逻辑
downloaders 是一个 map,映射了各种资源类型对应的下载方法,在 web 平台主要包含以下几类下载方法:图片类、文件类、字体类、声音类、视频类等等,具体的实现方式,感兴趣的可以点击「阅读原文」查看详情介绍和代码。
3.2.2 原生平台下载
原生平台的引擎相关文件可以在引擎目录的 resources/builtin/jsb-adapter/engine 目录下,资源加载相关的实现在 jsb-loader.js 文件中,这里的 downloader 重新注册了回调函数。
downloader.register({
// JS
'.js' : downloadScript,
'.jsc' : downloadScript,
// Images
'.png' : downloadAsset,
'.jpg' : downloadAsset,
...
});
在原生平台下,downloadAsset 等方法都会调用 download 来进行资源的下载,在资源下载之前会调用 transformUrl 对 url 进行检测,主要判断该资源是网络资源还是本地资源,如果是网络资源,是否已经下载过了。只有没下载过的网络资源,才需要进行下载。不需要下载的在文件解析的地方会直接读文件。
3.3 文件解析
在 loadOneAssetPipeline 中,资源会经过 fetch 和 parse 两个管线进行处理,fetch 负责下载而 parse 负责解析资源,并实例化资源对象。在 parse 方法中调用了 parser.parse 将文件内容传入,解析成对应的 Asset 对象,并返回。
3.3.1 Web 平台解析
Web 平台下的 parser.parse 主要做的是对解析中的文件的管理,为解析中、解析完的文件维护一个列表,避免重复解析。同时维护了解析完成后的回调列表,而真正的解析方法在 parsers 数组中。
parse (id, file, type, options, onComplete) {
let parsedAsset, parsing, parseHandler;
if (parsedAsset = parsed.get(id)) {
onComplete(null, parsedAsset);
}
else if (parsing = _parsing.get(id)){
parsing.push(onComplete);
}
else if (parseHandler = parsers[type]){
_parsing.add(id, [onComplete]);
parseHandler(file, options, function (err, data) {
if (err) {
files.remove(id);
}
else if (!isScene(data)){
parsed.add(id, data);
}
let callbacks = _parsing.remove(id);
for (let i = 0, l = callbacks.length; i < l; i++) {
callbacks[i](err, data);
}
});
}
else {
onComplete(null, file);
}
}
parsers 映射了各种类型文件的解析方法。
注意:在
parseImport方法中,反序列化方法会将资源的依赖放到asset.__depends__中,结构为数组,数组中每个对象包含3个字段,资源id uuid、owner 对象、prop 属性。比如一个Prefab资源,下面有2个节点,都引用了同一个资源,depends列表需要为这两个节点对象分别记录一条依赖信息 [{uuid:xxx, owner:1, prop:tex}, {uuid:xxx, owner:2, prop:tex}]
3.3.2 原生平台解析
在原生平台下,jsb-loader.js 中重新注册了各种资源的解析方法:
parser.register({
'.png' : downloader.downloadDomImage,
'.binary' : parseArrayBuffer,
'.txt' : parseText,
'.plist' : parsePlist,
'.font' : loadFont,
'.ExportJson' : parseJson,
...
});
图片的解析方法竟然是 downloader.downloadDomImage?跟踪原生平台调试了一下,确实是调用的这个方法,创建了 Image 对象并指定 src 来加载图片,这种方式加载本地磁盘的图片也是可以的,但纹理对象又是如何创建的呢?
通过 Texture2D 对应的 json 文件,creator 在加载真正的原生纹理之前,就已经创建好了 Texture2D 这个 Asset 对象,而在加载完原生图片资源后,会将 Image 对象设置为 Texture2D 对象的 _nativeAsset,在这个属性的 set 方法中,会调用 initWithData 或 initWithElement,这里才真正使用纹理数据创建了用于渲染的纹理对象。
var Texture2D = cc.Class({
name: 'cc.Texture2D',
extends: require('../assets/CCAsset'),
mixins: [EventTarget],
properties: {
_nativeAsset: {
get () {
// maybe returned to pool in webgl
return this._image;
},
set (data) {
if (data._data) {
this.initWithData(data._data, this._format, data.width, data.height);
}
else {
this.initWithElement(data);
}
},
override: true
},
而对于 parseJson、parseText、parseArrayBuffer 等实现,这里只是简单地调用了文件系统读取文件而已。像一些拿到文件内容之后,需要进一步解析才能使用的资源呢?比如模型、骨骼等资源依赖二进制的模型数据,这些数据的解析在哪里呢?
没错,跟上面的 Texture2D 一样,都是放在对应的 Asset 资源本身,有些在_nativeAsset 字段的 setter 回调中初始化,而有些会在真正使用这个资源时才惰性地进行初始化。
像图集、Prefab 这些资源又是怎么初始化的呢?
Creator 还是使用 parseImport 方法进行解析,因为这些资源对应的类型是 import,原生平台下并没有覆盖这种类型对应的 parse 函数,而这些资源会直接反序列化成可用的 Asset 对象。
3.4 依赖加载
creator 将资源分为两大类,普通资源和原生资源,普通资源包括 cc.Asset 及其子类,如 cc.SpriteFrame、cc.Texture2D、cc.Prefab 等等。
原生资源包括各种格式的纹理、音乐、字体等文件,在游戏中我们无法直接使用这些原生资源,而是需要让 creator 将他们转换成对应的 cc.Asset 对象之后才能使用。
在 creator 中,一个 Prefab 可能会依赖很多资源,这些依赖也可以分为普通依赖和原生资源依赖,creator 的 cc.Asset 提供了 _parseDepsFromJson 和 _parseNativeDepFromJson 方法来检查资源的依赖。loadDepends 通过 getDepends 方法搜集了资源的依赖。
loadDepends 创建了一个子任务来负责依赖资源的加载,并调用 pipeline 执行加载,实际上无论有无依赖需要加载,都会执行这段逻辑,加载完成后执行以下重要逻辑:
初始化 assset:在依赖加载完成后,将依赖的资源赋值到asset对应的属性后调用asset.onLoad将资源对应的 files和parsed缓存移除,并缓存资源到assets中(如果是场景的话,不会缓存)执行 repeatItem.callbacks列表中的回调(在loadDepends的开头构造,默认记录传入的done方法)
3.4.1 依赖解析
dependUtil 是一个控制依赖列表的单例,通过传入 uuid 和 asset 对象来解析该对象的依赖资源列表,返回的依赖资源列表可能包含以下4个字段:
deps 依赖的 Asset资源nativeDep 依赖的原生资源preventPreloadNativeObject 禁止预加载原生对象,这个值默认是 falsepreventDeferredLoadDependents 禁止延迟加载依赖,默认为 false,对于骨骼动画、TiledMap 等资源为 trueparsedFromExistAsset 是否直接从 asset.__depends__ 中取出。
dependUtil还维护了_depends缓存来避免依赖的重复查询,这个缓存会在首次查询某资源依赖时添加,当该资源被释放时移除。
3.5 资源释放
这一小节重点介绍在 Creator 中释放资源的三种方式以及其背后的实现,最后介绍在项目中如何排查资源泄露的情况。
3.5.1 Creator 的资源释放
Creator 支持以下3种资源释放的方式:

3.5.2 场景自动释放
当一个新场景运行的时候会执行 Director.runSceneImmediate 方法,这里调用了 _autoRelease 来实现老场景资源的自动释放(如果老场景勾选了自动释放资源)。
runSceneImmediate: function (scene, onBeforeLoadScene, onLaunched) {
// 省略代码...
var oldScene = this._scene;
if (!CC_EDITOR) {
// 自动释放资源
CC_BUILD && CC_DEBUG && console.time('AutoRelease');
cc.assetManager._releaseManager._autoRelease(oldScene, scene, persistNodeList);
CC_BUILD && CC_DEBUG && console.timeEnd('AutoRelease');
}
// unload scene
CC_BUILD && CC_DEBUG && console.time('Destroy');
if (cc.isValid(oldScene)) {
oldScene.destroy();
}
// 省略代码...
},
最新版本的 _autoRelease 的实现非常简洁干脆,将持久节点的引用从老场景迁移到新场景,然后直接调用资源的 decRef 减少引用计数,而是否释放老场景引用的资源,则取决于老场景是否设置了 autoReleaseAssets。
具体实现方式可戳「阅读原文」查看阅读相关代码。
3.5.3 引用计数和手动释放资源
剩下两种释放资源的方式,本质上都是调用 releaseManager.tryRelease 来实现资源释放,区别在于 decRef 是根据引用计数和 autoRelease 来决定是否调用 tryRelease,而 releaseAsset 是强制释放。
资源释放的完整流程大致如下图所示:

// CCAsset.js 减少引用
decRef (autoRelease) {
this._ref--;
autoRelease !== false && cc.assetManager._releaseManager.tryRelease(this);
return this;
}
// CCAssetManager.js 手动释放资源
releaseAsset (asset) {
releaseManager.tryRelease(asset, true);
},
tryRelease 支持延迟释放和强制释放2种模式,当传入 force 参数为 true 时直接进入释放流程,否则 creator 会将资源放入待释放的列表中,并在 EVENT_AFTER_DRAW 事件中执行 freeAssets 方法真正清理资源。不论何种方式,资源会传入到 _free 方法处理,这个方法做了以下几件事情。
从 _toDelete中移除在非 force释放时,需要检查是否还有其它引用,如果是则返回从 assets缓存中移除自动释放依赖资源 调用资源的 destroy方法销毁资源从 dependUtil中移除资源的依赖记录
3.5.4 资源释放的问题
最后我们来聊一聊资源释放的问题与定位,在加入引用计数后,最常见的问题还是没有正确增减引用计数导致的内存泄露(循环引用、少调用了 decRef 或多调用了 addRef),以及正在使用的资源被释放的问题(和内存泄露相反,资源被提前释放了)。
从目前的代码来看,如果正确使用了引用计数,新的资源底层是可以避免内存泄露等问题的。
这种问题怎么解决呢?
首先是定位出哪些资源出了问题,如果是被提前释放,我们可以直接定位到这个资源,如果是内存泄露,当我们发现问题时程序往往已经占用了大量的内存,这种情况下可以切换到一个空场景,并清理资源,把资源清理完后,可以检查 assets 中残留的资源是否有未被释放的资源。
要了解资源为什么会泄露,可以通过跟踪 addRef 和 decRef 的调用得到,下面提供了一个示例方法,用于跟踪某资源的 addRef 和 decRef调用,然后调用资源的 dump方法打印出所有调用的堆栈。
结语
本教程包含详细的代码解读,为了保障手机端的阅读体验,没有全部放进来,欢迎大家点击文末阅读原文按钮前往社区查看!
非常感谢宝爷的无私分享,快来给宝爷点赞吧!
