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
负责转换真正的url
preprocess.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
禁止预加载原生对象,这个值默认是 false
preventDeferredLoadDependents
禁止延迟加载依赖,默认为 false
,对于骨骼动画、TiledMap
等资源为 true
parsedFromExistAsset
是否直接从 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
方法打印出所有调用的堆栈。
结语
本教程包含详细的代码解读,为了保障手机端的阅读体验,没有全部放进来,欢迎大家点击文末阅读原文
按钮前往社区查看!
非常感谢宝爷
的无私分享,快来给宝爷点赞吧!