Cocos Creator 新資源管理系統(tǒng)剖析
v2.4 開始,Cocos Creator 使用
AssetBundle完全重構(gòu)了資源底層,提供了更加靈活強(qiáng)大的資源管理方式,也解決了之前版本資源管理的痛點(diǎn)(資源依賴與引用),本文將帶你深入了解 Creator 的新資源底層。
目錄
資源與構(gòu)建 理解與使用 AssetBundle 新資源框架剖析 加載管線 文件下載 文件解析 依賴加載 資源釋放
1.資源與構(gòu)建
1.1 creator 資源文件基礎(chǔ)
在了解引擎如何解析、加載資源之前,我們先來了解一下這些資源文件(圖片、Prefab、動(dòng)畫等)的規(guī)則。
在 creator 項(xiàng)目目錄下有幾個(gè)與資源相關(guān)的目錄:
assets 所有資源的總目錄,對(duì)應(yīng) creator 編輯器的資源管理器 library 本地資源庫,預(yù)覽項(xiàng)目時(shí)使用的目錄 build 構(gòu)建后的項(xiàng)目默認(rèn)目錄

在 assets 目錄下,creator 會(huì)為每個(gè)資源文件和目錄生成一個(gè)同名的.meta文件,meta 文件是一個(gè) json 文件,記錄了資源的版本、uuid 以及各種自定義的信息(在編輯器的屬性檢查器中設(shè)置),比如 prefab 的 meta 文件,就記錄了我們可以在編輯器修改的 optimizationPolicy 和 asyncLoadAssets 等屬性。

{
"ver": "1.2.7",
"uuid": "a8accd2e-6622-4c31-8a1e-4db5f2b568b5",
"optimizationPolicy": "AUTO", // prefab創(chuàng)建優(yōu)化策略
"asyncLoadAssets": false, // 是否延遲加載
"readonly": false,
"subMetas": {}
}
在 library 目錄下的 imports 目錄,資源文件名會(huì)被轉(zhuǎn)換成 uuid,并取 uuid 前2個(gè)字符進(jìn)行目錄分組存放,creator 會(huì)將所有資源的 uuid 到 assets 目錄的映射關(guān)系,以及資源和 meta 的最后更新時(shí)間戳放到一個(gè)名為 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 資源構(gòu)建
在項(xiàng)目構(gòu)建之后,資源會(huì)從 library 目錄下移動(dòng)到構(gòu)建輸出的 build 目錄中,基本只會(huì)導(dǎo)出參與構(gòu)建的場(chǎng)景和 resources 目錄下的資源,及其引用到的資源。腳本資源會(huì)由多個(gè) js 腳本合并為一個(gè) js,各種 json 文件也會(huì)按照特定的規(guī)則進(jìn)行打包。
我們可以在 Bundle 的配置界面和項(xiàng)目的構(gòu)建界面,為 Bundle 和項(xiàng)目設(shè)置:

1.2.1 圖片、圖集、自動(dòng)圖集
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
導(dǎo)入編輯器的每張圖片都會(huì)對(duì)應(yīng)生成一個(gè) json 文件,用于描述 Texture 的信息。
如下所示,默認(rèn)情況下項(xiàng)目中所有的 Texture2D 的 json 文件會(huì)被壓縮成一個(gè),如果選擇無壓縮,則每個(gè)圖片都會(huì)生成一個(gè) Texture2D 的 json 文件。
{
"__type__": "cc.Texture2D",
"content": "0,9729,9729,33071,33071,0,0,1"
}
如果將紋理的 Type 屬性設(shè)置為 Sprite,Creator 還會(huì)自動(dòng)生成了 SpriteFrame 類型的 json 文件。
圖集資源除了圖片外,還對(duì)應(yīng)一個(gè)圖集 json,這個(gè) json 包含了 cc.SpriteAtlas 信息,以及每個(gè)碎圖的 SpriteFrame 信息。
自動(dòng)圖集在默認(rèn)情況下只包含了 cc.SpriteAtlas 信息,在勾選內(nèi)聯(lián)所有 SpriteFrame 的情況下,會(huì)合并所有 SpriteFrame。
1.2.2 Prefab與場(chǎng)景
https://docs.cocos.com/creator/manual/zh/asset-workflow/prefab.html https://docs.cocos.com/creator/manual/zh/asset-workflow/scene-managing.html
場(chǎng)景資源與 Prefab 資源非常類似,都是一個(gè)描述了所有節(jié)點(diǎn)、組件等信息的 json文件,在勾選內(nèi)聯(lián)所有 SpriteFrame 的情況下,Prefab 引用到的 SpriteFrame 會(huì)被合并到 prefab 所在的 json 文件中。
如果一個(gè) SpriteFrame 被多個(gè) prefab 引用,那么每個(gè) prefab 的 json 文件都會(huì)包含該 SpriteFrame 的信息。
而在沒有勾選內(nèi)聯(lián)所有 SpriteFrame 的情況下,SpriteFrame 會(huì)是單獨(dú)的 json文件。
1.2.3 資源文件合并規(guī)則
當(dāng) Creator 將多個(gè)資源合并到一個(gè) json 文件中,我們可以在 config.json 中的 packs 字段找到被打包的資源信息,一個(gè)資源有可能被重復(fù)打包到多個(gè) json 中。
下面舉一個(gè)例子,展示在不同的選項(xiàng)下,creator 的構(gòu)建規(guī)則:
a.png 一個(gè)單獨(dú)的Sprite類型圖片 dir/b.png、c.png、AutoAtlas dir目錄下包含2張圖片,以及一個(gè)AutoAtlas d.png、d.plist 普通圖集 e.prefab 引用了SpriteFrame a和b的prefab f.prefab 引用了SpriteFrame b的prefab
下面是按不同規(guī)則構(gòu)建后的文件,可以看到,無壓縮的情況下生成的文件數(shù)量是最多的,不內(nèi)聯(lián)的文件會(huì)比內(nèi)聯(lián)多,但內(nèi)聯(lián)可能會(huì)導(dǎo)致同一個(gè)文件被重復(fù)包含,比如 e 和 f 這兩個(gè) Prefab 都引用了同一個(gè)圖片,這個(gè)圖片的 SpriteFrame.json 會(huì)被重復(fù)包含,合并成一個(gè) json 則只會(huì)生成一個(gè)文件。

默認(rèn)選項(xiàng)在絕大多數(shù)情況下都是一個(gè)不錯(cuò)的選擇。
如果是 web 平臺(tái),建議勾選內(nèi)聯(lián)所有 SpriteFrame 這可以減少網(wǎng)絡(luò) io,提高性能。
原生平臺(tái)則不建議勾選,這可能會(huì)增加包體大小以及熱更時(shí)要下載的內(nèi)容。
對(duì)于一些緊湊的 Bundle(比如加載該 Bundle 就需要用到里面所有的資源),我們可以配置為合并所有的 json。
2. 理解與使用 Asset Bundle
2.1 創(chuàng)建 Bundle
Asset Bundle 是 creator 2.4 之后的資源管理方案,簡(jiǎn)單地說,就是通過目錄來對(duì)資源進(jìn)行規(guī)劃,按照項(xiàng)目的需求將各種資源放到不同的目錄下,并將目錄配置成 Asset Bundle。能夠起到以下作用:
加快游戲啟動(dòng)時(shí)間 減小首包體積 跨項(xiàng)目復(fù)用資源 方便實(shí)現(xiàn)子游戲 以Bundle為單位的熱更新
Asset Bundle 的創(chuàng)建非常簡(jiǎn)單,只要在目錄的屬性檢查器中勾選配置為 bundle 即可,其中的選項(xiàng)官方文檔都有比較詳細(xì)的介紹。
其中關(guān)于壓縮的理解,文檔并沒有詳細(xì)的描述,這里的壓縮指的并不是 zip 之類的壓縮,而是通過
packAssets的方式,把多個(gè)資源的json文件合并到一個(gè),達(dá)到減少io的目的。

在選項(xiàng)上打勾非常簡(jiǎn)單,真正的關(guān)鍵在于如何規(guī)劃 Bundle。規(guī)劃的原則在于減少包體、加速啟動(dòng)以及資源復(fù)用。根據(jù)游戲的模塊來規(guī)劃資源是比較不錯(cuò)的選擇,比如按子游戲、關(guān)卡副本、或者系統(tǒng)功能來規(guī)劃。
Bundle 會(huì)自動(dòng)將文件夾下的資源,以及文件夾中引用到的其它文件夾下的資源打包(如果這些資源不是在其它 Bundle 中),如果我們按照模塊來規(guī)劃資源,很容易出現(xiàn)多個(gè) Bundle 共用了某個(gè)資源的情況。
可以將公共資源提取到一個(gè) Bundle 中,或者設(shè)置某個(gè) Bundle 有較高的優(yōu)先級(jí),構(gòu)建 Bundle 的依賴關(guān)系,否則這些資源會(huì)同時(shí)放到多個(gè) Bundle 中(如果是本地 Bundle,這會(huì)導(dǎo)致包體變大)。
2.2 使用 Bundle
關(guān)于加載資源 https://docs.cocos.com/creator/manual/zh/scripting/load-assets.html 關(guān)于釋放資源 https://docs.cocos.com/creator/manual/zh/asset-manager/release-manager.html
Bundle 的使用也非常簡(jiǎn)單,如果是 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 或遠(yuǎn)程 Bundle 都可以用 Bundle 名加載),可以使用 cc.assetManager.loadBundle 來加載 Bundle,然后使用加載后的 Bundle 對(duì)象,來加載 Bundle 中的資源。
對(duì)于原生平臺(tái),如果 Bundle 被配置為遠(yuǎn)程包,在構(gòu)建時(shí)需要在構(gòu)建發(fā)布面板中填寫資源服務(wù)器地址。
cc.assetManager.loadBundle('01_graphics', (err, bundle) => {
bundle.load('xxx');
});
原生或小游戲平臺(tái)下,我們還可以這樣使用 Bundle:
如果要加載其它項(xiàng)目的遠(yuǎn)程 Bundle,則需要使用 url 的方式加載(其它項(xiàng)目指另一個(gè) cocos 工程)
如果希望自己管理 Bundle 的下載和緩存,可以放到本地可寫路徑,并傳入路徑來加載這些 Bundle
// 當(dāng)復(fù)用其他項(xiàng)目的 Asset Bundle 時(shí)
cc.assetManager.loadBundle('https://othergame.com/remote/01_graphics', (err, bundle) => {
bundle.load('xxx');
});
// 原生平臺(tái)
cc.assetManager.loadBundle(jsb.fileUtils.getWritablePath() + '/pathToBundle/bundleName', (err, bundle) => {
// ...
});
// 微信小游戲平臺(tái)
cc.assetManager.loadBundle(wx.env.USER_DATA_PATH + '/pathToBundle/bundleName', (err, bundle) => {
// ...
});
其它注意項(xiàng):
加載 Bundle僅僅只是加載了Bundle的配置和腳本而已,Bundle中的其它資源還需要另外加載目前原生的 Bundle并不支持 zip 打包,遠(yuǎn)程包下載方式為逐文件下載,好處是操作簡(jiǎn)單,更新方便,壞處是 io 多,流量消耗大不同 Bundle下的腳本文件不要重名一個(gè) Bundle A依賴另一個(gè)Bundle B,如果B沒有被加載,加載A時(shí)并不會(huì)自動(dòng)加載B,而是在加載A中依賴B的那個(gè)資源時(shí)報(bào)錯(cuò)
3. 新資源框架剖析
v2.4重構(gòu)后的新框架代碼更加簡(jiǎn)潔清晰,我們可以先從宏觀角度了解一下整個(gè)資源框架,資源管線是整個(gè)框架最核心的部分,它規(guī)范了整個(gè)資源加載的流程,并支持對(duì)管線進(jìn)行自定義。
公共文件:
helper.js定義了一堆公共函數(shù),如decodeUuid、getUuidFromURL、getUrlWithUuid等等utilities.js定義了一堆公共函數(shù),如getDepends、forEach、parseLoadResArgs等等deserialize.js定義了deserialize方法,將json對(duì)象反序列化為Asset對(duì)象,并設(shè)置其__depends__屬性depend-util.js控制資源的依賴列表,每個(gè)資源的所有依賴都放在_depends成員變量中cache.js通用緩存類,封裝了一個(gè)簡(jiǎn)易的鍵值對(duì)容器shared.js定義了一些全局對(duì)象,主要是Cache和Pipeline對(duì)象,如加載好的assets、下載完的files以及bundles等
Bundle 部分:
config.js bundle的配置對(duì)象,負(fù)責(zé)解析bundle的config文件bundle.js bundle類,封裝了config以及加載卸載bundle內(nèi)資源的相關(guān)接口builtins.js內(nèi)建bundle資源的封裝,可以通過cc.assetManager.builtins訪問
管線部分:
CCAssetManager.js管理管線,提供統(tǒng)一的加載卸載接口管線框架
pipeline.js實(shí)現(xiàn)了管線的管道組合以及流轉(zhuǎn)等基本功能task.js定義了一個(gè)任務(wù)的基本屬性,并提供了簡(jiǎn)單的任務(wù)池功能request-item.js定義了一個(gè)資源下載項(xiàng)的基本屬性,一個(gè)任務(wù)可能會(huì)生成多個(gè)下載項(xiàng)預(yù)處理管線
urlTransformer.js parse將請(qǐng)求參數(shù)轉(zhuǎn)換成RequestItem對(duì)象(并查詢相關(guān)的資源配置),combine負(fù)責(zé)轉(zhuǎn)換真正的urlpreprocess.js過濾出需要進(jìn)行url轉(zhuǎn)換的資源,并調(diào)用transformPipeline下載管線
download-dom-audio.js提供下載音效的方法,使用audio標(biāo)簽進(jìn)行下載download-dom-image.js提供下載圖片的方法,使用Image標(biāo)簽進(jìn)行下載download-file.js提供下載文件的方法,使用XMLHttpRequest進(jìn)行下載download-script.js提供下載腳本的方法,使用script標(biāo)簽進(jìn)行下載downloader.js支持下載所有格式的下載器,支持并發(fā)控制、失敗重試、解析管線
factory.js創(chuàng)建Bundle、Asset、Texture2D等對(duì)象的工廠fetch.js調(diào)用packManager下載資源,并解析依賴parser.js對(duì)下載完成的文件進(jìn)行解析其它
releaseManager.js提供資源釋放接口、負(fù)責(zé)釋放依賴資源以及場(chǎng)景切換時(shí)的資源釋放cache-manager.d.ts在非 WEB 平臺(tái)上,用于管理所有從服務(wù)器上下載下來的緩存pack-manager.js處理打包資源,包括拆包,加載,緩存等等
3.1 加載管線
creator 使用管線(pipeline)來處理整個(gè)資源加載的流程,這樣的好處是解耦了資源處理的流程,將每一個(gè)步驟獨(dú)立成一個(gè)單獨(dú)的管道,管道可以很方便地進(jìn)行復(fù)用和組合,并且方便了我們自定義整個(gè)加載流程,我們可以創(chuàng)建一些自己的管道,加入到管線中,比如資源加密。
AssetManager 內(nèi)置了3條管線,普通的加載管線、預(yù)加載、以及資源路徑轉(zhuǎn)換管線,最后這條管線是為前面兩條管線服務(wù)的。
// 正常加載
this.pipeline = pipeline.append(preprocess).append(load);
// 預(yù)加載
this.fetchPipeline = fetchPipeline.append(preprocess).append(fetch);
// 轉(zhuǎn)換資源路徑
this.transformPipeline = transformPipeline.append(parse).append(combine);
3.1.1 啟動(dòng)加載管線【加載接口】
接下來,我們看一下一個(gè)普通的資源是如何加載的,比如最簡(jiǎn)單的 cc.resource.load,在 bundle.load 方法中,調(diào)用了 cc.assetManager.loadAny,在 loadAny 方法中,創(chuàng)建了一個(gè)新的任務(wù),并調(diào)用正常加載管線 pipeline 的 async 方法執(zhí)行任務(wù)。

注意要加載的資源路徑,被放到了 task.input中、options是一個(gè)對(duì)象,對(duì)象包含了 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 實(shí)際上只創(chuàng)建了一個(gè)子任務(wù),然后交由 transformPipeline 執(zhí)行。對(duì)于加載一個(gè)普通的資源,子任務(wù)的 input 和 options 與父任務(wù)相同。
let subTask = Task.create({input: task.input, options: subOptions});
task.output = task.source = transformPipeline.sync(subTask);
3.1.2 transformPipeline 管線【準(zhǔn)備階段】
transformPipeline 由 parse 和 combine 兩個(gè)管線組成,parse 的職責(zé)是為每個(gè)要加載的資源生成 RequestItem對(duì)象并初始化其資源信息(AssetInfo、uuid、config等):
先將 input轉(zhuǎn)換成數(shù)組進(jìn)行遍歷,如果是批量加載資源,每個(gè)加載項(xiàng)都會(huì)生成RequestItem;
如果輸入的 item 是 object,則先將 options 拷貝到 item 身上(實(shí)際上每個(gè) item 都會(huì)是 object,如果是 string 的話,第一步就先轉(zhuǎn)換成 object了)
對(duì)于 UUID類型的item,先檢查bundle,并從bundle中提取AssetInfo,對(duì)于redirect類型的資源,則從其依賴的bundle中獲取AssetInfo,找不到bundle就報(bào)錯(cuò)PATH類型和SCENE類型與UUID類型的處理基本類似,都是要拿到資源的詳細(xì)信息DIR類型會(huì)從bundle中取出指定路徑的信息,然后批量追加到input尾部(額外生成加載項(xiàng))URL類型是遠(yuǎn)程資源類型,無需特殊處理
RequestItem的初始信息,都是從bundle對(duì)象中查詢的,bundle的信息則是從bundle自帶的config.json文件中初始化的,在打包bundle的時(shí)候,會(huì)將bundle中的資源信息寫入config.json中。
經(jīng)過 parse 方法處理后,我們會(huì)得到一系列 RequestItem,并且很多 RequestItem 都自帶了 AssetInfo 和 uuid 等信息,combine 方法會(huì)為每個(gè) RequestItem 構(gòu)建出真正的加載路徑,這個(gè)加載路徑最終會(huì)轉(zhuǎn)換到 item.url 中。
3.1.3 load管線【加載流程】

load 方法做的事情很簡(jiǎn)單,基本只是創(chuàng)建了新的任務(wù),在 loadOneAssetPipeline 中執(zhí)行每個(gè)子任務(wù)。
loadOneAssetPipeline 如其函數(shù)名所示,就是加載一個(gè)資源的管線,它分為2步,fetch 和 parse:
fetch方法:
用于下載資源文件,由 packManager 負(fù)責(zé)下載的實(shí)現(xiàn),fetch 會(huì)將下載完的文件數(shù)據(jù)放到 item.file 中。
parse方法:
用于將加載完的資源文件轉(zhuǎn)換成我們可用的資源對(duì)象:
對(duì)于原生資源,調(diào)用 parser.parse 進(jìn)行解析,該方法會(huì)根據(jù)資源類型調(diào)用不同的解析方法
import資源調(diào)用parseImport方法,根據(jù)json數(shù)據(jù)反序列化出Asset對(duì)象,并放到assets中圖片資源會(huì)調(diào)用 parseImage、parsePVRTex或parsePKMTex方法解析圖像格式(但不會(huì)創(chuàng)建Texture對(duì)象)音效資源調(diào)用 parseAudio方法進(jìn)行解析plist資源調(diào)用parsePlist方法進(jìn)行解析
對(duì)于其它資源,如果 uuid在 task.options.__exclude__ 中,則標(biāo)記為完成,并添加引用計(jì)數(shù);否則,根據(jù)一些復(fù)雜的條件來決定是否加載資源的依賴。
3.2 文件下載
creator 使用 packManager.load 來完成下載的工作,當(dāng)要下載一個(gè)文件時(shí),有2個(gè)問題需要考慮:
該文件是否被打包了?比如由于勾選了內(nèi)聯(lián)所有 SpriteFrame,導(dǎo)致 SpriteFrame 的 json 文件被合并到 prefab 中
當(dāng)前平臺(tái)是原生平臺(tái)還是 web 平臺(tái)?對(duì)于一些本地資源,原生平臺(tái)需要從磁盤讀取。
3.2.1 Web 平臺(tái)的下載
web 平臺(tái)的 download 實(shí)現(xiàn)如下:
用一個(gè) downloaders數(shù)組來管理各種資源類型對(duì)應(yīng)的下載方式使用 files緩存來避免重復(fù)下載使用 _downloading隊(duì)列來處理并發(fā)下載同一個(gè)資源時(shí)的回調(diào),并保證時(shí)序支持了下載的優(yōu)先級(jí)、重試等邏輯
downloaders 是一個(gè) map,映射了各種資源類型對(duì)應(yīng)的下載方法,在 web 平臺(tái)主要包含以下幾類下載方法:圖片類、文件類、字體類、聲音類、視頻類等等,具體的實(shí)現(xiàn)方式,感興趣的可以點(diǎn)擊「閱讀原文」查看詳情介紹和代碼。
3.2.2 原生平臺(tái)下載
原生平臺(tái)的引擎相關(guān)文件可以在引擎目錄的 resources/builtin/jsb-adapter/engine 目錄下,資源加載相關(guān)的實(shí)現(xiàn)在 jsb-loader.js 文件中,這里的 downloader 重新注冊(cè)了回調(diào)函數(shù)。
downloader.register({
// JS
'.js' : downloadScript,
'.jsc' : downloadScript,
// Images
'.png' : downloadAsset,
'.jpg' : downloadAsset,
...
});
在原生平臺(tái)下,downloadAsset 等方法都會(huì)調(diào)用 download 來進(jìn)行資源的下載,在資源下載之前會(huì)調(diào)用 transformUrl 對(duì) url 進(jìn)行檢測(cè),主要判斷該資源是網(wǎng)絡(luò)資源還是本地資源,如果是網(wǎng)絡(luò)資源,是否已經(jīng)下載過了。只有沒下載過的網(wǎng)絡(luò)資源,才需要進(jìn)行下載。不需要下載的在文件解析的地方會(huì)直接讀文件。
3.3 文件解析
在 loadOneAssetPipeline 中,資源會(huì)經(jīng)過 fetch 和 parse 兩個(gè)管線進(jìn)行處理,fetch 負(fù)責(zé)下載而 parse 負(fù)責(zé)解析資源,并實(shí)例化資源對(duì)象。在 parse 方法中調(diào)用了 parser.parse 將文件內(nèi)容傳入,解析成對(duì)應(yīng)的 Asset 對(duì)象,并返回。
3.3.1 Web 平臺(tái)解析
Web 平臺(tái)下的 parser.parse 主要做的是對(duì)解析中的文件的管理,為解析中、解析完的文件維護(hù)一個(gè)列表,避免重復(fù)解析。同時(shí)維護(hù)了解析完成后的回調(diào)列表,而真正的解析方法在 parsers 數(shù)組中。
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方法中,反序列化方法會(huì)將資源的依賴放到asset.__depends__中,結(jié)構(gòu)為數(shù)組,數(shù)組中每個(gè)對(duì)象包含3個(gè)字段,資源id uuid、owner 對(duì)象、prop 屬性。比如一個(gè)Prefab資源,下面有2個(gè)節(jié)點(diǎn),都引用了同一個(gè)資源,depends列表需要為這兩個(gè)節(jié)點(diǎn)對(duì)象分別記錄一條依賴信息 [{uuid:xxx, owner:1, prop:tex}, {uuid:xxx, owner:2, prop:tex}]
3.3.2 原生平臺(tái)解析
在原生平臺(tái)下,jsb-loader.js 中重新注冊(cè)了各種資源的解析方法:
parser.register({
'.png' : downloader.downloadDomImage,
'.binary' : parseArrayBuffer,
'.txt' : parseText,
'.plist' : parsePlist,
'.font' : loadFont,
'.ExportJson' : parseJson,
...
});
圖片的解析方法竟然是 downloader.downloadDomImage?跟蹤原生平臺(tái)調(diào)試了一下,確實(shí)是調(diào)用的這個(gè)方法,創(chuàng)建了 Image 對(duì)象并指定 src 來加載圖片,這種方式加載本地磁盤的圖片也是可以的,但紋理對(duì)象又是如何創(chuàng)建的呢?
通過 Texture2D 對(duì)應(yīng)的 json 文件,creator 在加載真正的原生紋理之前,就已經(jīng)創(chuàng)建好了 Texture2D 這個(gè) Asset 對(duì)象,而在加載完原生圖片資源后,會(huì)將 Image 對(duì)象設(shè)置為 Texture2D 對(duì)象的 _nativeAsset,在這個(gè)屬性的 set 方法中,會(huì)調(diào)用 initWithData 或 initWithElement,這里才真正使用紋理數(shù)據(jù)創(chuàng)建了用于渲染的紋理對(duì)象。
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
},
而對(duì)于 parseJson、parseText、parseArrayBuffer 等實(shí)現(xiàn),這里只是簡(jiǎn)單地調(diào)用了文件系統(tǒng)讀取文件而已。像一些拿到文件內(nèi)容之后,需要進(jìn)一步解析才能使用的資源呢?比如模型、骨骼等資源依賴二進(jìn)制的模型數(shù)據(jù),這些數(shù)據(jù)的解析在哪里呢?
沒錯(cuò),跟上面的 Texture2D 一樣,都是放在對(duì)應(yīng)的 Asset 資源本身,有些在_nativeAsset 字段的 setter 回調(diào)中初始化,而有些會(huì)在真正使用這個(gè)資源時(shí)才惰性地進(jìn)行初始化。
像圖集、Prefab 這些資源又是怎么初始化的呢?
Creator 還是使用 parseImport 方法進(jìn)行解析,因?yàn)檫@些資源對(duì)應(yīng)的類型是 import,原生平臺(tái)下并沒有覆蓋這種類型對(duì)應(yīng)的 parse 函數(shù),而這些資源會(huì)直接反序列化成可用的 Asset 對(duì)象。
3.4 依賴加載
creator 將資源分為兩大類,普通資源和原生資源,普通資源包括 cc.Asset 及其子類,如 cc.SpriteFrame、cc.Texture2D、cc.Prefab 等等。
原生資源包括各種格式的紋理、音樂、字體等文件,在游戲中我們無法直接使用這些原生資源,而是需要讓 creator 將他們轉(zhuǎn)換成對(duì)應(yīng)的 cc.Asset 對(duì)象之后才能使用。
在 creator 中,一個(gè) Prefab 可能會(huì)依賴很多資源,這些依賴也可以分為普通依賴和原生資源依賴,creator 的 cc.Asset 提供了 _parseDepsFromJson 和 _parseNativeDepFromJson 方法來檢查資源的依賴。loadDepends 通過 getDepends 方法搜集了資源的依賴。
loadDepends 創(chuàng)建了一個(gè)子任務(wù)來負(fù)責(zé)依賴資源的加載,并調(diào)用 pipeline 執(zhí)行加載,實(shí)際上無論有無依賴需要加載,都會(huì)執(zhí)行這段邏輯,加載完成后執(zhí)行以下重要邏輯:
初始化 assset:在依賴加載完成后,將依賴的資源賦值到asset對(duì)應(yīng)的屬性后調(diào)用asset.onLoad將資源對(duì)應(yīng)的 files和parsed緩存移除,并緩存資源到assets中(如果是場(chǎng)景的話,不會(huì)緩存)執(zhí)行 repeatItem.callbacks列表中的回調(diào)(在loadDepends的開頭構(gòu)造,默認(rèn)記錄傳入的done方法)
3.4.1 依賴解析
dependUtil 是一個(gè)控制依賴列表的單例,通過傳入 uuid 和 asset 對(duì)象來解析該對(duì)象的依賴資源列表,返回的依賴資源列表可能包含以下4個(gè)字段:
deps 依賴的 Asset資源nativeDep 依賴的原生資源preventPreloadNativeObject 禁止預(yù)加載原生對(duì)象,這個(gè)值默認(rèn)是 falsepreventDeferredLoadDependents 禁止延遲加載依賴,默認(rèn)為 false,對(duì)于骨骼動(dòng)畫、TiledMap 等資源為 trueparsedFromExistAsset 是否直接從 asset.__depends__ 中取出。
dependUtil還維護(hù)了_depends緩存來避免依賴的重復(fù)查詢,這個(gè)緩存會(huì)在首次查詢某資源依賴時(shí)添加,當(dāng)該資源被釋放時(shí)移除。
3.5 資源釋放
這一小節(jié)重點(diǎn)介紹在 Creator 中釋放資源的三種方式以及其背后的實(shí)現(xiàn),最后介紹在項(xiàng)目中如何排查資源泄露的情況。
3.5.1 Creator 的資源釋放
Creator 支持以下3種資源釋放的方式:

3.5.2 場(chǎng)景自動(dòng)釋放
當(dāng)一個(gè)新場(chǎng)景運(yùn)行的時(shí)候會(huì)執(zhí)行 Director.runSceneImmediate 方法,這里調(diào)用了 _autoRelease 來實(shí)現(xiàn)老場(chǎng)景資源的自動(dòng)釋放(如果老場(chǎng)景勾選了自動(dòng)釋放資源)。
runSceneImmediate: function (scene, onBeforeLoadScene, onLaunched) {
// 省略代碼...
var oldScene = this._scene;
if (!CC_EDITOR) {
// 自動(dòng)釋放資源
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 的實(shí)現(xiàn)非常簡(jiǎn)潔干脆,將持久節(jié)點(diǎn)的引用從老場(chǎng)景遷移到新場(chǎng)景,然后直接調(diào)用資源的 decRef 減少引用計(jì)數(shù),而是否釋放老場(chǎng)景引用的資源,則取決于老場(chǎng)景是否設(shè)置了 autoReleaseAssets。
具體實(shí)現(xiàn)方式可戳「閱讀原文」查看閱讀相關(guān)代碼。
3.5.3 引用計(jì)數(shù)和手動(dòng)釋放資源
剩下兩種釋放資源的方式,本質(zhì)上都是調(diào)用 releaseManager.tryRelease 來實(shí)現(xiàn)資源釋放,區(qū)別在于 decRef 是根據(jù)引用計(jì)數(shù)和 autoRelease 來決定是否調(diào)用 tryRelease,而 releaseAsset 是強(qiáng)制釋放。
資源釋放的完整流程大致如下圖所示:

// CCAsset.js 減少引用
decRef (autoRelease) {
this._ref--;
autoRelease !== false && cc.assetManager._releaseManager.tryRelease(this);
return this;
}
// CCAssetManager.js 手動(dòng)釋放資源
releaseAsset (asset) {
releaseManager.tryRelease(asset, true);
},
tryRelease 支持延遲釋放和強(qiáng)制釋放2種模式,當(dāng)傳入 force 參數(shù)為 true 時(shí)直接進(jìn)入釋放流程,否則 creator 會(huì)將資源放入待釋放的列表中,并在 EVENT_AFTER_DRAW 事件中執(zhí)行 freeAssets 方法真正清理資源。不論何種方式,資源會(huì)傳入到 _free 方法處理,這個(gè)方法做了以下幾件事情。
從 _toDelete中移除在非 force釋放時(shí),需要檢查是否還有其它引用,如果是則返回從 assets緩存中移除自動(dòng)釋放依賴資源 調(diào)用資源的 destroy方法銷毀資源從 dependUtil中移除資源的依賴記錄
3.5.4 資源釋放的問題
最后我們來聊一聊資源釋放的問題與定位,在加入引用計(jì)數(shù)后,最常見的問題還是沒有正確增減引用計(jì)數(shù)導(dǎo)致的內(nèi)存泄露(循環(huán)引用、少調(diào)用了 decRef 或多調(diào)用了 addRef),以及正在使用的資源被釋放的問題(和內(nèi)存泄露相反,資源被提前釋放了)。
從目前的代碼來看,如果正確使用了引用計(jì)數(shù),新的資源底層是可以避免內(nèi)存泄露等問題的。
這種問題怎么解決呢?
首先是定位出哪些資源出了問題,如果是被提前釋放,我們可以直接定位到這個(gè)資源,如果是內(nèi)存泄露,當(dāng)我們發(fā)現(xiàn)問題時(shí)程序往往已經(jīng)占用了大量的內(nèi)存,這種情況下可以切換到一個(gè)空?qǐng)鼍埃⑶謇碣Y源,把資源清理完后,可以檢查 assets 中殘留的資源是否有未被釋放的資源。
要了解資源為什么會(huì)泄露,可以通過跟蹤 addRef 和 decRef 的調(diào)用得到,下面提供了一個(gè)示例方法,用于跟蹤某資源的 addRef 和 decRef調(diào)用,然后調(diào)用資源的 dump方法打印出所有調(diào)用的堆棧。
結(jié)語
本教程包含詳細(xì)的代碼解讀,為了保障手機(jī)端的閱讀體驗(yàn),沒有全部放進(jìn)來,歡迎大家點(diǎn)擊文末閱讀原文按鈕前往社區(qū)查看!
非常感謝寶爺的無私分享,快來給寶爺點(diǎn)贊吧!
