剖析CocosCreator新資源管理系統(tǒng)
1.資源與構(gòu)建
1.1 creator資源文件基礎(chǔ)
在了解引擎如何解析、加載資源之前,我們先來(lái)了解一下這些資源文件(圖片、Prefab、動(dòng)畫(huà)等)的規(guī)則,在creator項(xiàng)目目錄下有幾個(gè)與資源相關(guān)的目錄:
- assets 所有資源的總目錄,對(duì)應(yīng)creator編輯器的資源管理器
- library 本地資源庫(kù),預(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目錄并沒(méi)有為目錄生成任何東西。
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è),如果選擇無(wú)壓縮
,則每個(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的信息。而在沒(méi)有勾選內(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)建后的文件,可以看到,無(wú)壓縮的情況下生成的文件數(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è)文件。
資源文件 | 無(wú)壓縮 | 默認(rèn)(不內(nèi)聯(lián)) | 默認(rèn)(內(nèi)聯(lián)) | 合并json |
---|---|---|---|---|
a.png | a.texture.json + a.spriteframe.json | a.spriteframe.json | ||
./dir/b.png | b.texture.json + b.spriteframe.json | b.spriteframe.json | ||
./dir/c.png | c.texture.json + c.spriteframe.json | c.spriteframe.json | c.spriteframe.json | |
./dir/AutoAtlas | autoatlas.json | autoatlas.json | autoatlas.json | |
d.png | d.texture.json + d.spriteframe.json | d.spriteframe.json | d.spriteframe.json | |
d.plist | d.plist.json | d.plist.json | d.plist.json | |
e.prefab | e.prefab.json | e.prefab.json | e.prefab.json(pack a+b) | |
f.prefab | f.prefab.json | f.prefab.json | f.prefab.json(pack b) | |
g.allTexture.json | g.allTexture.json | all.json |
默認(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)單地說(shuō)就是通過(guò)目錄來(lái)對(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)于壓縮的理解,文檔并沒(méi)有詳細(xì)的描述,這里的壓縮指的并不是zip之類的壓縮,而是通過(guò)packAssets的方式,把多個(gè)資源的json文件合并到一個(gè),達(dá)到減少io的目的
在選項(xiàng)上打勾非常簡(jiǎn)單,真正的關(guān)鍵在于如何規(guī)劃Bundle,規(guī)劃的原則在于減少包體、加速啟動(dòng)以及資源復(fù)用。根據(jù)游戲的模塊來(lái)規(guī)劃資源是比較不錯(cuò)的選擇,比如按子游戲、關(guān)卡副本、或者系統(tǒng)功能來(lái)規(guī)劃。
Bundle會(huì)自動(dòng)將文件夾下的資源,以及文件夾中引用到的其它文件夾下的資源打包(如果這些資源不是在其它Bundle中),如果我們按照模塊來(lái)規(guī)劃資源,很容易出現(xiàn)多個(gè)Bundle共用了某個(gè)資源的情況??梢詫⒐操Y源提取到一個(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來(lái)加載
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來(lái)加載Bundle,然后使用加載后的Bundle對(duì)象,來(lái)加載Bundle中的資源。對(duì)于原生平臺(tái),如果Bundle被配置為遠(yuǎn)程包,在構(gòu)建時(shí)需要在構(gòu)建發(fā)布面板中填寫(xiě)資源服務(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的下載和緩存,可以放到本地可寫(xiě)路徑,并傳入路徑來(lái)加載這些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沒(méi)有被加載,加載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資源的封裝,可以通過(guò)
cc.assetManager.builtins
訪問(wèn)
管線部分
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)換真正的url
- preprocess.js 過(guò)濾出需要進(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ù)器上下載下來(lái)的緩存
- pack-manager.js 處理打包資源,包括拆包,加載,緩存等等
3.1 加載管線
creator使用管線(pipeline)來(lái)處理整個(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)加載管線【加載接口】
接下來(lái)我們看一下一個(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)程資源類型,無(wú)需特殊處理
function parse (task) { // 將input轉(zhuǎn)換成數(shù)組 var input = task.input, options = task.options; input = Array.isArray(input) ? input : [ input ]; task.output = []; for (var i = 0; i < input.length; i ++ ) { var item = input[i]; var out = RequestItem.create(); if (typeof item === 'string') { // 先創(chuàng)建object item = Object.create(null); item[options.__requestType__ || RequestType.UUID] = input[i]; } if (typeof item === 'object') { // local options will overlap glabal options // 將options的屬性復(fù)制到item身上,addon會(huì)復(fù)制options上有,而item沒(méi)有的屬性 cc.js.addon(item, options); if (item.preset) { cc.js.addon(item, cc.assetManager.presets[item.preset]); } for (var key in item) { switch (key) { // uuid類型資源,從bundle中取出該資源的詳細(xì)信息 case RequestType.UUID: var uuid = out.uuid = decodeUuid(item.uuid); if (bundles.has(item.bundle)) { var config = bundles.get(item.bundle)._config; var info = config.getAssetInfo(uuid); if (info && info.redirect) { if (!bundles.has(info.redirect)) throw new Error(`Please load bundle ${info.redirect} first`); config = bundles.get(info.redirect)._config; info = config.getAssetInfo(uuid); } out.config = config; out.info = info; } out.ext = item.ext || '.json'; break; case '__requestType__': case 'ext': case 'bundle': case 'preset': case 'type': break; case RequestType.DIR: // 解包后動(dòng)態(tài)添加到input列表尾部,后續(xù)的循環(huán)會(huì)自動(dòng)parse這些資源 if (bundles.has(item.bundle)) { var infos = []; bundles.get(item.bundle)._config.getDirWithPath(item.dir, item.type, infos); for (let i = 0, l = infos.length; i < l; i++) { var info = infos[i]; input.push({uuid: info.uuid, __isNative__: false, ext: '.json', bundle: item.bundle}); } } out.recycle(); out = null; break; case RequestType.PATH: // PATH類型的資源根據(jù)路徑和type取出該資源的詳細(xì)信息 if (bundles.has(item.bundle)) { var config = bundles.get(item.bundle)._config; var info = config.getInfoWithPath(item.path, item.type); if (info && info.redirect) { if (!bundles.has(info.redirect)) throw new Error(`you need to load bundle ${info.redirect} first`); config = bundles.get(info.redirect)._config; info = config.getAssetInfo(info.uuid); } if (!info) { out.recycle(); throw new Error(`Bundle ${item.bundle} doesn't contain ${item.path}`); } out.config = config; out.uuid = info.uuid; out.info = info; } out.ext = item.ext || '.json'; break; case RequestType.SCENE: // 場(chǎng)景類型,從bundle中的config調(diào)用getSceneInfo取出該場(chǎng)景的詳細(xì)信息 if (bundles.has(item.bundle)) { var config = bundles.get(item.bundle)._config; var info = config.getSceneInfo(item.scene); if (info && info.redirect) { if (!bundles.has(info.redirect)) throw new Error(`you need to load bundle ${info.redirect} first`); config = bundles.get(info.redirect)._config; info = config.getAssetInfo(info.uuid); } if (!info) { out.recycle(); throw new Error(`Bundle ${config.name} doesn't contain scene ${item.scene}`); } out.config = config; out.uuid = info.uuid; out.info = info; } break; case '__isNative__': out.isNative = item.__isNative__; break; case RequestType.URL: out.url = item.url; out.uuid = item.uuid || item.url; out.ext = item.ext || cc.path.extname(item.url); out.isNative = item.__isNative__ !== undefined ? item.__isNative__ : true; break; default: out.options[key] = item[key]; } if (!out) break; } } if (!out) continue; task.output.push(out); if (!out.uuid && !out.url) throw new Error('unknown input:' + item.toString()); } return null; }
RequestItem的初始信息,都是從bundle對(duì)象中查詢的,bundle的信息則是從bundle自帶的config.json文件中初始化的,在打包bundle的時(shí)候,會(huì)將bundle中的資源信息寫(xiě)入config.json中。
經(jīng)過(guò)parse方法處理后,我們會(huì)得到一系列RequestItem,并且很多RequestItem都自帶了AssetInfo和uuid等信息,combine方法會(huì)為每個(gè)RequestItem構(gòu)建出真正的加載路徑,這個(gè)加載路徑最終會(huì)轉(zhuǎn)換到item.url中。
function combine (task) { var input = task.output = task.input; for (var i = 0; i < input.length; i++) { var item = input[i]; // 如果item已經(jīng)包含了url,則跳過(guò),直接使用item的url if (item.url) continue; var url = '', base = ''; var config = item.config; // 決定目錄的前綴 if (item.isNative) { base = (config && config.nativeBase) ? (config.base + config.nativeBase) : cc.assetManager.generalNativeBase; } else { base = (config && config.importBase) ? (config.base + config.importBase) : cc.assetManager.generalImportBase; } let uuid = item.uuid; var ver = ''; if (item.info) { if (item.isNative) { ver = item.info.nativeVer ? ('.' + item.info.nativeVer) : ''; } else { ver = item.info.ver ? ('.' + item.info.ver) : ''; } } // 拼接最終的url // ugly hack, WeChat does not support loading font likes 'myfont.dw213.ttf'. So append hash to directory if (item.ext === '.ttf') { url = `${base}/${uuid.slice(0, 2)}/${uuid}${ver}/${item.options.__nativeName__}`; } else { url = `${base}/${uuid.slice(0, 2)}/${uuid}${ver}${item.ext}`; } item.url = url; } return null; }
3.1.3 load管線【加載流程】
load方法做的事情很簡(jiǎn)單,基本只是創(chuàng)建了新的任務(wù),在loadOneAssetPipeline中執(zhí)行每個(gè)子任務(wù)
function load (task, done) { if (!task.progress) { task.progress = {finish: 0, total: task.input.length}; } var options = task.options, progress = task.progress; options.__exclude__ = options.__exclude__ || Object.create(null); task.output = []; forEach(task.input, function (item, cb) { // 對(duì)每個(gè)input項(xiàng)都創(chuàng)建一個(gè)子任務(wù),并交由loadOneAssetPipeline執(zhí)行 let subTask = Task.create({ input: item, onProgress: task.onProgress, options, progress, onComplete: function (err, item) { if (err && !task.isFinish && !cc.assetManager.force) done(err); task.output.push(item); subTask.recycle(); cb(); } }); // 執(zhí)行子任務(wù),loadOneAssetPipeline有fetch和parse組成 loadOneAssetPipeline.async(subTask); }, function () { // 每個(gè)input執(zhí)行完成后,最后執(zhí)行該函數(shù) options.__exclude__ = null; if (task.isFinish) { clear(task, true); return task.dispatch('error'); } gatherAsset(task); clear(task, true); done(); }); }
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ù)雜的條件來(lái)決定是否加載資源的依賴
var loadOneAssetPipeline = new Pipeline('loadOneAsset', [ function fetch (task, done) { var item = task.output = task.input; var { options, isNative, uuid, file } = item; var { reload } = options; // 如果assets里面已經(jīng)加載了這個(gè)資源,則直接完成 if (file || (!reload && !isNative && assets.has(uuid))) return done(); // 下載文件,這是一個(gè)異步的過(guò)程,文件下載完會(huì)被放到item.file中,并執(zhí)行done驅(qū)動(dòng)管線 packManager.load(item, task.options, function (err, data) { if (err) { if (cc.assetManager.force) { err = null; } else { cc.error(err.message, err.stack); } data = null; } item.file = data; done(err); }); }, // 將資源文件轉(zhuǎn)換成資源對(duì)象的過(guò)程 function parse (task, done) { var item = task.output = task.input, progress = task.progress, exclude = task.options.__exclude__; var { id, file, options } = item; if (item.isNative) { // 對(duì)于原生資源,調(diào)用parser.parse進(jìn)行處理,將處理完的資源放到item.content中,并結(jié)束流程 parser.parse(id, file, item.ext, options, function (err, asset) { if (err) { if (!cc.assetManager.force) { cc.error(err.message, err.stack); return done(err); } } item.content = asset; task.dispatch('progress', ++progress.finish, progress.total, item); files.remove(id); parsed.remove(id); done(); }); } else { var { uuid } = item; // 非原生資源,如果在task.options.__exclude__中,直接結(jié)束 if (uuid in exclude) { var { finish, content, err, callbacks } = exclude[uuid]; task.dispatch('progress', ++progress.finish, progress.total, item); if (finish || checkCircleReference(uuid, uuid, exclude) ) { content && content.addRef(); item.content = content; done(err); } else { callbacks.push({ done, item }); } } else { // 如果不是reload,且asset中包含了該uuid if (!options.reload && assets.has(uuid)) { var asset = assets.get(uuid); // 開(kāi)啟了options.__asyncLoadAssets__,或asset.__asyncLoadAssets__為false,直接結(jié)束,不加載依賴 if (options.__asyncLoadAssets__ || !asset.__asyncLoadAssets__) { item.content = asset.addRef(); task.dispatch('progress', ++progress.finish, progress.total, item); done(); } else { loadDepends(task, asset, done, false); } } else { // 如果是reload,或者assets中沒(méi)有,則進(jìn)行解析,并加載依賴 parser.parse(id, file, 'import', options, function (err, asset) { if (err) { if (cc.assetManager.force) { err = null; } else { cc.error(err.message, err.stack); } return done(err); } asset._uuid = uuid; loadDepends(task, asset, done, true); }); } } } } ]);
3.2 文件下載
creator使用packManager.load
來(lái)完成下載的工作,當(dāng)要下載一個(gè)文件時(shí),有2個(gè)問(wèn)題需要考慮:
- 該文件是否被打包了,比如由于勾選了內(nèi)聯(lián)所有SpriteFrame,導(dǎo)致SpriteFrame的json文件被合并到prefab中
- 當(dāng)前平臺(tái)是原生平臺(tái)還是web平臺(tái),對(duì)于一些本地資源,原生平臺(tái)需要從磁盤讀取
// packManager.load的實(shí)現(xiàn) load (item, options, onComplete) { // 如果資源沒(méi)有被打包,則直接調(diào)用downloader.download下載(download內(nèi)部也有已下載和加載中的判斷) if (item.isNative || !item.info || !item.info.packs) return downloader.download(item.id, item.url, item.ext, item.options, onComplete); // 如果文件已經(jīng)下載過(guò)了,則直接返回 if (files.has(item.id)) return onComplete(null, files.get(item.id)); var packs = item.info.packs; // 如果pack已經(jīng)在加載中,則將回調(diào)添加到_loading隊(duì)列,等加載完成后觸發(fā)回調(diào) var pack = packs.find(isLoading); if (pack) return _loading.get(pack.uuid).push({ onComplete, id: item.id }); // 下載一個(gè)新的pack pack = packs[0]; _loading.add(pack.uuid, [{ onComplete, id: item.id }]); let url = cc.assetManager._transform(pack.uuid, {ext: pack.ext, bundle: item.config.name}); // 下載pack并解包, downloader.download(pack.uuid, url, pack.ext, item.options, function (err, data) { files.remove(pack.uuid); if (err) { cc.error(err.message, err.stack); } // unpack package,內(nèi)部實(shí)現(xiàn)包含2種解包,一種針對(duì)prefab、圖集等json數(shù)組的分割解包,另一種針對(duì)Texture2D的content進(jìn)行解包 packManager.unpack(pack.packs, data, pack.ext, item.options, function (err, result) { if (!err) { for (var id in result) { files.add(id, result[id]); } } var callbacks = _loading.remove(pack.uuid); for (var i = 0, l = callbacks.length; i < l; i++) { var cb = callbacks[i]; if (err) { cb.onComplete(err); continue; } var data = result[cb.id]; if (!data) { cb.onComplete(new Error('can not retrieve data from package')); } else { cb.onComplete(null, data); } } }); }); }
3.2.1 Web平臺(tái)的下載
web平臺(tái)的download實(shí)現(xiàn)如下:
- 用一個(gè)downloaders數(shù)組來(lái)管理各種資源類型對(duì)應(yīng)的下載方式
- 使用files緩存來(lái)避免重復(fù)下載
- 使用_downloading隊(duì)列來(lái)處理并發(fā)下載同一個(gè)資源時(shí)的回調(diào),并保證時(shí)序
- 支持了下載的優(yōu)先級(jí)、重試等邏輯
download (id, url, type, options, onComplete) { // 取出downloaders中對(duì)應(yīng)類型的下載回調(diào) let func = downloaders[type] || downloaders['default']; let self = this; // 避免重復(fù)下載 let file, downloadCallbacks; if (file = files.get(id)) { onComplete(null, file); } // 如果在下載中,添加到隊(duì)列 else if (downloadCallbacks = _downloading.get(id)) { downloadCallbacks.push(onComplete); for (let i = 0, l = _queue.length; i < l; i++) { var item = _queue[i]; if (item.id === id) { var priority = options.priority || 0; if (item.priority < priority) { item.priority = priority; _queueDirty = true; } return; } } } else { // 進(jìn)行下載,并設(shè)置好下載失敗的重試 var maxRetryCount = options.maxRetryCount || this.maxRetryCount; var maxConcurrency = options.maxConcurrency || this.maxConcurrency; var maxRequestsPerFrame = options.maxRequestsPerFrame || this.maxRequestsPerFrame; function process (index, callback) { if (index === 0) { _downloading.add(id, [onComplete]); } if (!self.limited) return func(urlAppendTimestamp(url), options, callback); updateTime(); function invoke () { func(urlAppendTimestamp(url), options, function () { // when finish downloading, update _totalNum _totalNum--; if (!_checkNextPeriod && _queue.length > 0) { callInNextTick(handleQueue, maxConcurrency, maxRequestsPerFrame); _checkNextPeriod = true; } callback.apply(this, arguments); }); } if (_totalNum < maxConcurrency && _totalNumThisPeriod < maxRequestsPerFrame) { invoke(); _totalNum++; _totalNumThisPeriod++; } else { // when number of request up to limitation, cache the rest _queue.push({ id, priority: options.priority || 0, invoke }); _queueDirty = true; if (!_checkNextPeriod && _totalNum < maxConcurrency) { callInNextTick(handleQueue, maxConcurrency, maxRequestsPerFrame); _checkNextPeriod = true; } } } // retry完成后,將文件添加到files緩存中,從_downloading隊(duì)列中移除,并執(zhí)行callbacks回調(diào) // when retry finished, invoke callbacks function finale (err, result) { if (!err) files.add(id, result); var callbacks = _downloading.remove(id); for (let i = 0, l = callbacks.length; i < l; i++) { callbacks[i](err, result); } } retry(process, maxRetryCount, this.retryInterval, finale); } }
downloaders是一個(gè)map,映射了各種資源類型對(duì)應(yīng)的下載方法,在web平臺(tái)主要包含以下幾類下載方法:
圖片類 downloadImage
- downloadDomImage 使用Html的Image元素,指定其src屬性來(lái)下載
- downloadBlob 以文件下載的方式下載圖片
文件類,這里可以分為二進(jìn)制文件、json文件和文本文件
- downloadArrayBuffer 指定arraybuffer類型調(diào)用downloadFile,用于skel、bin、pvr等文件下載
- downloadText 指定text類型調(diào)用downloadFile,用于atlas、tmx、xml、vsh等文件下載
- downloadJson 指定json類型調(diào)用downloadFile,并在下載完后解析json,用于plist、json等文件下載
字體類 loadFont 構(gòu)建css樣式,指定url下載
聲音類 downloadAudio
- downloadDomAudio 創(chuàng)建Html的audio元素,指定其src屬性來(lái)下載
- downloadBlob 以文件下載的方式下載音效
視頻類 downloadVideo web端直接返回了
腳本 downloadScript 創(chuàng)建Html的script元素,指定其src屬性來(lái)下載并執(zhí)行
Bundle downloadBundle 同時(shí)下載了Bundle的json和腳本
downloadFile使用了XMLHttpRequest來(lái)下載文件,具體實(shí)現(xiàn)如下:
function downloadFile (url, options, onProgress, onComplete) { var { options, onProgress, onComplete } = parseParameters(options, onProgress, onComplete); var xhr = new XMLHttpRequest(), errInfo = 'download failed: ' + url + ', status: '; xhr.open('GET', url, true); if (options.responseType !== undefined) xhr.responseType = options.responseType; if (options.withCredentials !== undefined) xhr.withCredentials = options.withCredentials; if (options.mimeType !== undefined && xhr.overrideMimeType ) xhr.overrideMimeType(options.mimeType); if (options.timeout !== undefined) xhr.timeout = options.timeout; if (options.header) { for (var header in options.header) { xhr.setRequestHeader(header, options.header[header]); } } xhr.onload = function () { if ( xhr.status === 200 || xhr.status === 0 ) { onComplete && onComplete(null, xhr.response); } else { onComplete && onComplete(new Error(errInfo + xhr.status + '(no response)')); } }; if (onProgress) { xhr.onprogress = function (e) { if (e.lengthComputable) { onProgress(e.loaded, e.total); } }; } xhr.onerror = function(){ onComplete && onComplete(new Error(errInfo + xhr.status + '(error)')); }; xhr.ontimeout = function(){ onComplete && onComplete(new Error(errInfo + xhr.status + '(time out)')); }; xhr.onabort = function(){ onComplete && onComplete(new Error(errInfo + xhr.status + '(abort)')); }; xhr.send(null); return xhr; }
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來(lái)進(jìn)行資源的下載,在資源下載之前會(huì)調(diào)用transformUrl對(duì)url進(jìn)行檢測(cè),主要判斷該資源是網(wǎng)絡(luò)資源還是本地資源,如果是網(wǎng)絡(luò)資源,是否已經(jīng)下載過(guò)了。只有沒(méi)下載過(guò)的網(wǎng)絡(luò)資源,才需要進(jìn)行下載。不需要下載的在文件解析的地方會(huì)直接讀文件。
// func傳入的是下載完成之后的處理,比如腳本下載完成后需要執(zhí)行,此時(shí)會(huì)調(diào)用window.require // 如果說(shuō)要下載的是json資源之類的,傳入的func是doNothing,也就是直接調(diào)用onComplete方法 function download (url, func, options, onFileProgress, onComplete) { var result = transformUrl(url, options); // 如果是本地文件,直接指向func if (result.inLocal) { func(result.url, options, onComplete); } // 如果在緩存中,更新資源的最后使用時(shí)間(lru) else if (result.inCache) { cacheManager.updateLastTime(url) func(result.url, options, function (err, data) { if (err) { cacheManager.removeCache(url); } onComplete(err, data); }); } else { // 未下載的網(wǎng)絡(luò)資源,調(diào)用downloadFile進(jìn)行下載 var time = Date.now(); var storagePath = ''; if (options.__cacheBundleRoot__) { storagePath = `${cacheManager.cacheDir}/${options.__cacheBundleRoot__}/${time}${suffix++}${cc.path.extname(url)}`; } else { storagePath = `${cacheManager.cacheDir}/${time}${suffix++}${cc.path.extname(url)}`; } // 使用downloadFile下載并緩存 downloadFile(url, storagePath, options.header, onFileProgress, function (err, path) { if (err) { onComplete(err, null); return; } func(path, options, function (err, data) { if (!err) { cacheManager.cacheFile(url, storagePath, options.__cacheBundleRoot__); } onComplete(err, data); }); }); } } function transformUrl (url, options) { var inLocal = false; var inCache = false; // 通過(guò)正則匹配是不是URL if (REGEX.test(url)) { if (options.reload) { return { url }; } else { // 檢查是否在緩存中(本地磁盤緩存) var cache = cacheManager.cachedFiles.get(url); if (cache) { inCache = true; url = cache.url; } } } else { inLocal = true; } return { url, inLocal, inCache }; }
downloadFile會(huì)調(diào)用原生平臺(tái)的jsb_downloader來(lái)下載資源,并保存到本地磁盤中
downloadFile (remoteUrl, filePath, header, onProgress, onComplete) { downloading.add(remoteUrl, { onProgress, onComplete }); var storagePath = filePath; if (!storagePath) storagePath = tempDir + '/' + performance.now() + cc.path.extname(remoteUrl); jsb_downloader.createDownloadFileTask(remoteUrl, storagePath, header); },
3.3 文件解析
在loadOneAssetPipeline中,資源會(huì)經(jīng)過(guò)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映射了各種類型文件的解析方法,下面以圖片和普通的asset資源為例:
注意:在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}]
// 映射圖片格式到解析方法 var parsers = { '.png' : parser.parseImage, '.jpg' : parser.parseImage, '.bmp' : parser.parseImage, '.jpeg' : parser.parseImage, '.gif' : parser.parseImage, '.ico' : parser.parseImage, '.tiff' : parser.parseImage, '.webp' : parser.parseImage, '.image' : parser.parseImage, '.pvr' : parser.parsePVRTex, '.pkm' : parser.parsePKMTex, // Audio '.mp3' : parser.parseAudio, '.ogg' : parser.parseAudio, '.wav' : parser.parseAudio, '.m4a' : parser.parseAudio, // plist '.plist' : parser.parsePlist, 'import' : parser.parseImport }; // 圖片并不會(huì)解析成Asset對(duì)象,而是解析成對(duì)應(yīng)的圖片對(duì)象 parseImage (file, options, onComplete) { if (capabilities.imageBitmap && file instanceof Blob) { let imageOptions = {}; imageOptions.imageOrientation = options.__flipY__ ? 'flipY' : 'none'; imageOptions.premultiplyAlpha = options.__premultiplyAlpha__ ? 'premultiply' : 'none'; createImageBitmap(file, imageOptions).then(function (result) { result.flipY = !!options.__flipY__; result.premultiplyAlpha = !!options.__premultiplyAlpha__; onComplete && onComplete(null, result); }, function (err) { onComplete && onComplete(err, null); }); } else { onComplete && onComplete(null, file); } }, // Asset對(duì)象的解析,通過(guò)deserialize實(shí)現(xiàn),大致流程是解析json然后找到對(duì)應(yīng)的class,并調(diào)用對(duì)應(yīng)class的_deserialize方法拷貝數(shù)據(jù)、初始化變量,并將依賴資源放到asset.__depends parseImport (file, options, onComplete) { if (!file) return onComplete && onComplete(new Error('Json is empty')); var result, err = null; try { result = deserialize(file, options); } catch (e) { err = e; } onComplete && onComplete(err, result); },
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來(lái)加載圖片,這種方式加載本地磁盤的圖片也是可以的,但紋理對(duì)象又是如何創(chuàng)建的呢?通過(guò)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ù)的解析在哪里呢?沒(méi)錯(cuò),跟上面的Texture2D一樣,都是放在對(duì)應(yīng)的Asset資源本身,有些在_nativeAsset字段的setter回調(diào)中初始化,而有些會(huì)在真正使用這個(gè)資源時(shí)才惰性地進(jìn)行初始化。
// 在jsb-loader.js文件中 function parseText (url, options, onComplete) { readText(url, onComplete); } function parseArrayBuffer (url, options, onComplete) { readArrayBuffer(url, onComplete); } function parseJson (url, options, onComplete) { readJson(url, onComplete); } // 在jsb-fs-utils.js文件中 readText (filePath, onComplete) { fsUtils.readFile(filePath, 'utf8', onComplete); }, readArrayBuffer (filePath, onComplete) { fsUtils.readFile(filePath, '', onComplete); }, readJson (filePath, onComplete) { fsUtils.readFile(filePath, 'utf8', function (err, text) { var out = null; if (!err) { try { out = JSON.parse(text); } catch (e) { cc.warn('Read json failed: ' + e.message); err = new Error(e.message); } } onComplete && onComplete(err, out); }); },
像圖集、Prefab這些資源又是怎么初始化的呢?Creator還是使用parseImport方法進(jìn)行解析,因?yàn)檫@些資源對(duì)應(yīng)的類型是import
,原生平臺(tái)下并沒(méi)有覆蓋這種類型對(duì)應(yīng)的parse函數(shù),而這些資源會(huì)直接反序列化成可用的Asset對(duì)象。
3.4 依賴加載
creator將資源分為兩大類,普通資源和原生資源,普通資源包括cc.Asset及其子類,如cc.SpriteFrame、cc.Texture2D、cc.Prefab等等。原生資源包括各種格式的紋理、音樂(lè)、字體等文件,在游戲中我們無(wú)法直接使用這些原生資源,而是需要讓creator將他們轉(zhuǎn)換成對(duì)應(yīng)的cc.Asset對(duì)象之后才能使用。
在creator中,一個(gè)Prefab可能會(huì)依賴很多資源,這些依賴也可以分為普通依賴和原生資源依賴,creator的cc.Asset提供了_parseDepsFromJson
和_parseNativeDepFromJson
方法來(lái)檢查資源的依賴。loadDepends通過(guò)getDepends方法搜集了資源的依賴。
loadDepends創(chuàng)建了一個(gè)子任務(wù)來(lái)負(fù)責(zé)依賴資源的加載,并調(diào)用pipeline執(zhí)行加載,實(shí)際上無(wú)論有無(wú)依賴需要加載,都會(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的開(kāi)頭構(gòu)造,默認(rèn)記錄傳入的done方法)
// 加載指定asset的依賴項(xiàng) function loadDepends (task, asset, done, init) { var item = task.input, progress = task.progress; var { uuid, id, options, config } = item; var { __asyncLoadAssets__, cacheAsset } = options; var depends = []; // 增加引用計(jì)數(shù)來(lái)避免加載依賴的過(guò)程中資源被釋放,調(diào)用getDepends獲取依賴資源 asset.addRef && asset.addRef(); getDepends(uuid, asset, Object.create(null), depends, false, __asyncLoadAssets__, config); task.dispatch('progress', ++progress.finish, progress.total += depends.length, item); var repeatItem = task.options.__exclude__[uuid] = { content: asset, finish: false, callbacks: [{ done, item }] }; let subTask = Task.create({ input: depends, options: task.options, onProgress: task.onProgress, onError: Task.prototype.recycle, progress, onComplete: function (err) { // 在所有依賴項(xiàng)加載完成之后回調(diào) asset.decRef && asset.decRef(false); asset.__asyncLoadAssets__ = __asyncLoadAssets__; repeatItem.finish = true; repeatItem.err = err; if (!err) { var assets = Array.isArray(subTask.output) ? subTask.output : [subTask.output]; // 構(gòu)造一個(gè)map,記錄uuid到asset的映射 var map = Object.create(null); for (let i = 0, l = assets.length; i < l; i++) { var dependAsset = assets[i]; dependAsset && (map[dependAsset instanceof cc.Asset ? dependAsset._uuid + '@import' : uuid + '@native'] = dependAsset); } // 調(diào)用setProperties將對(duì)應(yīng)的依賴資源設(shè)置到asset的成員變量中 if (!init) { if (asset.__nativeDepend__ && !asset._nativeAsset) { var missingAsset = setProperties(uuid, asset, map); if (!missingAsset) { try { asset.onLoad && asset.onLoad(); } catch (e) { cc.error(e.message, e.stack); } } } } else { var missingAsset = setProperties(uuid, asset, map); if (!missingAsset) { try { asset.onLoad && asset.onLoad(); } catch (e) { cc.error(e.message, e.stack); } } files.remove(id); parsed.remove(id); cache(uuid, asset, cacheAsset !== undefined ? cacheAsset : cc.assetManager.cacheAsset); } subTask.recycle(); } // 這個(gè)repeatItem可能有很多個(gè)地方都加載了它,要通知所有回調(diào)加載完成 var callbacks = repeatItem.callbacks; for (var i = 0, l = callbacks.length; i < l; i++) { var cb = callbacks[i]; asset.addRef && asset.addRef(); cb.item.content = asset; cb.done(err); } callbacks.length = 0; } }); pipeline.async(subTask); }
3.4.1 依賴解析
getDepends (uuid, data, exclude, depends, preload, asyncLoadAssets, config) { var err = null; try { var info = dependUtil.parse(uuid, data); var includeNative = true; if (data instanceof cc.Asset && (!data.__nativeDepend__ || data._nativeAsset)) includeNative = false; if (!preload) { asyncLoadAssets = !CC_EDITOR && (!!data.asyncLoadAssets || (asyncLoadAssets && !info.preventDeferredLoadDependents)); for (let i = 0, l = info.deps.length; i < l; i++) { let dep = info.deps[i]; if (!(dep in exclude)) { exclude[dep] = true; depends.push({uuid: dep, __asyncLoadAssets__: asyncLoadAssets, bundle: config && config.name}); } } if (includeNative && !asyncLoadAssets && !info.preventPreloadNativeObject && info.nativeDep) { config && (info.nativeDep.bundle = config.name); depends.push(info.nativeDep); } } else { for (let i = 0, l = info.deps.length; i < l; i++) { let dep = info.deps[i]; if (!(dep in exclude)) { exclude[dep] = true; depends.push({uuid: dep, bundle: config && config.name}); } } if (includeNative && info.nativeDep) { config && (info.nativeDep.bundle = config.name); depends.push(info.nativeDep); } } } catch (e) { err = e; } return err; },
dependUtil是一個(gè)控制依賴列表的單例,通過(guò)傳入uuid和asset對(duì)象來(lái)解析該對(duì)象的依賴資源列表,返回的依賴資源列表可能包含以下4個(gè)字段:
- deps 依賴的Asset資源
- nativeDep 依賴的原生資源
- preventPreloadNativeObject 禁止預(yù)加載原生對(duì)象,這個(gè)值默認(rèn)是false
- preventDeferredLoadDependents 禁止延遲加載依賴,默認(rèn)為false,對(duì)于骨骼動(dòng)畫(huà)、TiledMap等資源為true
- parsedFromExistAsset 是否直接從
asset.__depends__
中取出
dependUtil還維護(hù)了_depends緩存來(lái)避免依賴的重復(fù)查詢,這個(gè)緩存會(huì)在首次查詢某資源依賴時(shí)添加,當(dāng)該資源被釋放時(shí)移除
// 根據(jù)json信息獲取其資源依賴列表,實(shí)際上json信息就是asset對(duì)象 parse (uuid, json) { var out = null; // 如果是場(chǎng)景或者Prefab,data會(huì)是一個(gè)數(shù)組,scene or prefab if (Array.isArray(json)) { // 如果已經(jīng)解析過(guò)了,在_depends中有依賴列表,則直接返回 if (this._depends.has(uuid)) return this._depends.get(uuid) out = { // 對(duì)于Prefab或場(chǎng)景,直接使用_parseDepsFromJson方法返回 deps: cc.Asset._parseDepsFromJson(json), asyncLoadAssets: json[0].asyncLoadAssets }; } // 如果包含__type__,獲取其構(gòu)造函數(shù),并從json中查找依賴資源 get deps from json // 實(shí)際測(cè)試,預(yù)加載的資源會(huì)走下面這個(gè)分支,預(yù)加載的資源并沒(méi)有把json反序列化成Asset對(duì)象 else if (json.__type__) { if (this._depends.has(uuid)) return this._depends.get(uuid); var ctor = js._getClassById(json.__type__); // 部分資源重寫(xiě)了_parseDepsFromJson和_parseNativeDepFromJson方法 // 比如cc.Texture2D out = { preventPreloadNativeObject: ctor.preventPreloadNativeObject, preventDeferredLoadDependents: ctor.preventDeferredLoadDependents, deps: ctor._parseDepsFromJson(json), nativeDep: ctor._parseNativeDepFromJson(json) }; out.nativeDep && (out.nativeDep.uuid = uuid); } // get deps from an existing asset // 如果沒(méi)有__type__字段,則無(wú)法找到它對(duì)應(yīng)的ctor,從asset的__depends__字段中取出依賴 else { if (!CC_EDITOR && (out = this._depends.get(uuid)) && out.parsedFromExistAsset) return out; var asset = json; out = { deps: [], parsedFromExistAsset: true, preventPreloadNativeObject: asset.constructor.preventPreloadNativeObject, preventDeferredLoadDependents: asset.constructor.preventDeferredLoadDependents }; let deps = asset.__depends__; for (var i = 0, l = deps.length; i < l; i++) { var dep = deps[i].uuid; out.deps.push(dep); } if (asset.__nativeDepend__) { // asset._nativeDep會(huì)返回類似這樣的對(duì)象 {__isNative__: true, uuid: this._uuid, ext: this._native} out.nativeDep = asset._nativeDep; } } // 第一次找到依賴,直接放到_depends列表中,cache dependency list this._depends.add(uuid, out); return out; }
CCAsset默認(rèn)的_parseDepsFromJson
和_parseNativeDepFromJson
實(shí)現(xiàn)如下,_parseDepsFromJson
通過(guò)調(diào)用parseDependRecursively遞歸json,將json對(duì)象及其子對(duì)象的所有__uuid__
全部找到放到depends數(shù)組中。Texture2D、TTFFont、AudioClip的實(shí)現(xiàn)為直接返回空數(shù)組,而SpriteFrame的實(shí)現(xiàn)為返回cc.assetManager.utils.decodeUuid(json.content.texture)
,這個(gè)字段記錄了SpriteFrame對(duì)應(yīng)紋理的uuid。
而_parseNativeDepFromJson
在改asset的_native
有值的情況下,會(huì)返回{ __isNative__: true, ext: json._native}
。實(shí)際上大部分的native資源走的是_nativeDep
,這個(gè)屬性的get方法會(huì)返回一個(gè)包含類似這樣的對(duì)象{__isNative__: true, uuid: this._uuid, ext: this._native}
。
_parseDepsFromJson (json) { var depends = []; parseDependRecursively(json, depends); return depends; }, _parseNativeDepFromJson (json) { if (json._native) return { __isNative__: true, ext: json._native}; return null; }
3.5 資源釋放
這一小節(jié)重點(diǎn)介紹在Creator中釋放資源的三種方式以及其背后的實(shí)現(xiàn),最后介紹在項(xiàng)目中如何排查資源泄露的情況。
3.5.1 Creator的資源釋放
Creator支持以下3種資源釋放的方式:
釋放方式 | 釋放效果 |
---|---|
勾選:場(chǎng)景->屬性檢查器->自動(dòng)釋放資源 | 在場(chǎng)景切換后,自動(dòng)釋放新場(chǎng)景不使用的資源 |
引用計(jì)數(shù)釋放res.decRef | 使用addRef和decRef維護(hù)引用計(jì)數(shù),在decRef后引用計(jì)數(shù)為0時(shí)自動(dòng)釋放 |
手動(dòng)釋放cc.assetManager.releaseAsset(texture); | 手動(dòng)釋放資源,強(qiáng)制釋放 |
3.5.2 場(chǎng)景自動(dòng)釋放
當(dāng)一個(gè)新場(chǎng)景運(yùn)行的時(shí)候會(huì)執(zhí)行Director.runSceneImmediate方法,這里調(diào)用了_autoRelease來(lái)實(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。
// do auto release _autoRelease (oldScene, newScene, persistNodes) { // 所有持久節(jié)點(diǎn)依賴的資源自動(dòng)addRef、并記錄到sceneDeps.persistDeps中 for (let i = 0, l = persistNodes.length; i < l; i++) { var node = persistNodes[i]; var sceneDeps = dependUtil._depends.get(newScene._id); var deps = _persistNodeDeps.get(node.uuid); for (let i = 0, l = deps.length; i < l; i++) { var dependAsset = assets.get(deps[i]); if (dependAsset) { dependAsset.addRef(); } } if (sceneDeps) { !sceneDeps.persistDeps && (sceneDeps.persistDeps = []); sceneDeps.persistDeps.push.apply(sceneDeps.persistDeps, deps); } } // 釋放老場(chǎng)景的依賴 if (oldScene) { var childs = dependUtil.getDeps(oldScene._id); for (let i = 0, l = childs.length; i < l; i++) { let asset = assets.get(childs[i]); asset && asset.decRef(CC_TEST || oldScene.autoReleaseAssets); } var dependencies = dependUtil._depends.get(oldScene._id); if (dependencies && dependencies.persistDeps) { var persistDeps = dependencies.persistDeps; for (let i = 0, l = persistDeps.length; i < l; i++) { let asset = assets.get(persistDeps[i]); asset && asset.decRef(CC_TEST || oldScene.autoReleaseAssets); } } dependUtil.remove(oldScene._id); } },
3.5.3 引用計(jì)數(shù)和手動(dòng)釋放資源
剩下兩種釋放資源的方式,本質(zhì)上都是調(diào)用releaseManager.tryRelease來(lái)實(shí)現(xiàn)資源釋放,區(qū)別在于decRef是根據(jù)引用計(jì)數(shù)和autoRelease來(lái)決定是否調(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中移除資源的依賴記錄
checkCircularReference返回值如果大于0,表示資源還有被其它地方引用,其它地方指所有我們addRef的地方,該方法會(huì)先記錄asset當(dāng)前的refCount,然后消除掉資源和依賴資源中對(duì)asset的引用,這相當(dāng)于資源A內(nèi)部掛載了組件B和C,它們都引用了資源A,此時(shí)資源A的引用計(jì)數(shù)為2,而組件B和C其實(shí)是要跟著A釋放的,而A被B和C引用著,計(jì)數(shù)就不為0無(wú)法釋放,所以checkCircularReference先排除了內(nèi)部的引用。如果資源的refCount減去了內(nèi)部的引用次數(shù)還大于1,說(shuō)明有其它地方還引用著它,不能釋放。
tryRelease (asset, force) { if (!(asset instanceof cc.Asset)) return; if (force) { releaseManager._free(asset, force); } else { _toDelete.add(asset._uuid, asset); // 在下次Director繪制完成之后,執(zhí)行freeAssets 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) { // 檢查資源內(nèi)部的循環(huán)引用 if (checkCircularReference(asset) > 0) return; } } // 從緩存中移除 assets.remove(asset._uuid); 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); }, // 釋放_(tái)toDelete中的資源并清空 function freeAssets () { eventListener = false; _toDelete.forEach(function (asset) { releaseManager._free(asset); }); _toDelete.clear(); }
asset.destroy做了什么?資源對(duì)象是如何被釋放掉的?像紋理、聲音這樣的資源又是如何被釋放掉的呢?Asset對(duì)象本身并沒(méi)有destroy方法,而是Asset對(duì)象所繼承的CCObject對(duì)象實(shí)現(xiàn)了destroy,這里的實(shí)現(xiàn)只是將對(duì)象放到了一個(gè)待釋放的數(shù)組中,并打上ToDestroy
的標(biāo)記。Director每一幀都會(huì)調(diào)用deferredDestroy來(lái)執(zhí)行_destroyImmediate
進(jìn)行資源釋放,這個(gè)方法會(huì)對(duì)對(duì)象的Destroyed標(biāo)記進(jìn)行判斷和操作、調(diào)用_onPreDestroy
方法執(zhí)行回調(diào)、以及_destruct
方法進(jìn)行析構(gòu)。
prototype.destroy = function () { if (this._objFlags & Destroyed) { cc.warnID(5000); return false; } if (this._objFlags & ToDestroy) { return false; } this._objFlags |= ToDestroy; objectsToDestroy.push(this); if (CC_EDITOR && deferredDestroyTimer === null && cc.engine && ! cc.engine._isUpdating) { // 在編輯器模式下可以立即銷毀 deferredDestroyTimer = setImmediate(deferredDestroy); } return true; }; // Director每一幀都會(huì)調(diào)用這個(gè)方法 function deferredDestroy () { var deleteCount = objectsToDestroy.length; for (var i = 0; i < deleteCount; ++i) { var obj = objectsToDestroy[i]; if (!(obj._objFlags & Destroyed)) { obj._destroyImmediate(); } } // 當(dāng)我們?cè)赼.onDestroy中調(diào)用b.destroy,objectsToDestroy數(shù)組的大小會(huì)變化,我們只銷毀在這次deferredDestroy之前objectsToDestroy中的元素 if (deleteCount === objectsToDestroy.length) { objectsToDestroy.length = 0; } else { objectsToDestroy.splice(0, deleteCount); } if (CC_EDITOR) { deferredDestroyTimer = null; } } // 真正的資源釋放 prototype._destroyImmediate = function () { if (this._objFlags & Destroyed) { cc.errorID(5000); return; } // 執(zhí)行回調(diào) if (this._onPreDestroy) { this._onPreDestroy(); } if ((CC_TEST ? (/* make CC_EDITOR mockable*/ Function('return !CC_EDITOR'))() : !CC_EDITOR) || cc.engine._isPlaying) { this._destruct(); } this._objFlags |= Destroyed; };
在這里_destruct
做的事情就是將對(duì)象的屬性清空,比如將object類型的屬性置為null,將string類型的屬性置為'',compileDestruct方法會(huì)返回一個(gè)該類的析構(gòu)函數(shù),compileDestruct先收集了普通object和cc.Class這兩種類型下的所有屬性,并根據(jù)類型構(gòu)建了一個(gè)propsToReset用來(lái)清空屬性,支持JIT的情況下會(huì)根據(jù)要清空的屬性生成一個(gè)類似這樣的函數(shù)返回function(o) {o.a='';o.b=null;o.['c']=undefined...}
,而非JIT情況下會(huì)返回一個(gè)根據(jù)propsToReset遍歷處理的函數(shù),前者占用更多內(nèi)存,但效率更高。
prototype._destruct = function () { var ctor = this.constructor; var destruct = ctor.__destruct__; if (!destruct) { destruct = compileDestruct(this, ctor); js.value(ctor, '__destruct__', destruct, true); } destruct(this); }; function compileDestruct (obj, ctor) { var shouldSkipId = obj instanceof cc._BaseNode || obj instanceof cc.Component; var idToSkip = shouldSkipId ? '_id' : null; var key, propsToReset = {}; for (key in obj) { if (obj.hasOwnProperty(key)) { if (key === idToSkip) { continue; } switch (typeof obj[key]) { case 'string': propsToReset[key] = ''; break; case 'object': case 'function': propsToReset[key] = null; break; } } } // Overwrite propsToReset according to Class if (cc.Class._isCCClass(ctor)) { var attrs = cc.Class.Attr.getClassAttrs(ctor); var propList = ctor.__props__; for (var i = 0; i < propList.length; i++) { key = propList[i]; var attrKey = key + cc.Class.Attr.DELIMETER + 'default'; if (attrKey in attrs) { if (shouldSkipId && key === '_id') { continue; } switch (typeof attrs[attrKey]) { case 'string': propsToReset[key] = ''; break; case 'object': case 'function': propsToReset[key] = null; break; case 'undefined': propsToReset[key] = undefined; break; } } } } if (CC_SUPPORT_JIT) { // compile code var func = ''; for (key in propsToReset) { var statement; if (CCClass.IDENTIFIER_RE.test(key)) { statement = 'o.' + key + '='; } else { statement = 'o[' + CCClass.escapeForJS(key) + ']='; } var val = propsToReset[key]; if (val === '') { val = '""'; } func += (statement + val + ';\n'); } return Function('o', func); } else { return function (o) { for (var key in propsToReset) { o[key] = propsToReset[key]; } }; } }
那么_onPreDestroy
又做了什么呢?主要是將各種事件、定時(shí)器進(jìn)行注銷,對(duì)子節(jié)點(diǎn)、組件等進(jìn)行刪除,詳情可以看下面這段代碼。
// Node的_onPreDestroy _onPreDestroy () { // 調(diào)用_onPreDestroyBase方法,實(shí)際是調(diào)用BaseNode.prototype._onPreDestroy,這個(gè)方法下面介紹 var destroyByParent = this._onPreDestroyBase(); // 注銷Actions if (ActionManagerExist) { cc.director.getActionManager().removeAllActionsFromTarget(this); } // 移除_currentHovered if (_currentHovered === this) { _currentHovered = null; } this._bubblingListeners && this._bubblingListeners.clear(); this._capturingListeners && this._capturingListeners.clear(); // 移除所有觸摸和鼠標(biāo)事件監(jiān)聽(tīng) if (this._touchListener || this._mouseListener) { eventManager.removeListeners(this); if (this._touchListener) { this._touchListener.owner = null; this._touchListener.mask = null; this._touchListener = null; } if (this._mouseListener) { this._mouseListener.owner = null; this._mouseListener.mask = null; this._mouseListener = null; } } if (CC_JSB && CC_NATIVERENDERER) { this._proxy.destroy(); this._proxy = null; } // 回收到對(duì)象池中 this._backDataIntoPool(); if (this._reorderChildDirty) { cc.director.__fastOff(cc.Director.EVENT_AFTER_UPDATE, this.sortAllChildren, this); } if (!destroyByParent) { if (CC_EDITOR) { // 確保編輯模式下的,節(jié)點(diǎn)的被刪除后可以通過(guò)ctrl+z撤銷(重新添加到原來(lái)的父節(jié)點(diǎn)) this._parent = null; } } }, // BaseNode的_onPreDestroy _onPreDestroy () { var i, len; // 加上Destroying標(biāo)記 this._objFlags |= Destroying; var parent = this._parent; // 根據(jù)檢測(cè)父節(jié)點(diǎn)的標(biāo)記判斷是不是由父節(jié)點(diǎn)的destroy發(fā)起的釋放 var destroyByParent = parent && (parent._objFlags & Destroying); if (!destroyByParent && (CC_EDITOR || CC_TEST)) { // 從編輯器中移除 this._registerIfAttached(false); } // 把所有子節(jié)點(diǎn)進(jìn)行釋放,它們的_onPreDestroy也會(huì)被執(zhí)行 var children = this._children; for (i = 0, len = children.length; i < len; ++i) { children[i]._destroyImmediate(); } // 把所有的組件進(jìn)行釋放,它們的_onPreDestroy也會(huì)被執(zhí)行 for (i = 0, len = this._components.length; i < len; ++i) { var component = this._components[i]; component._destroyImmediate(); } // 注銷事件監(jiān)聽(tīng),比如otherNode.on(type, callback, thisNode) 注冊(cè)了事件 // thisNode被釋放時(shí),需要注銷otherNode身上的監(jiān)聽(tīng),避免事件回調(diào)到已銷毀的對(duì)象上 var eventTargets = this.__eventTargets; for (i = 0, len = eventTargets.length; i < len; ++i) { var target = eventTargets[i]; target && target.targetOff(this); } eventTargets.length = 0; // 如果自己是常駐節(jié)點(diǎn),則從常駐節(jié)點(diǎn)列表中移除 if (this._persistNode) { cc.game.removePersistRootNode(this); } // 如果是自己釋放的自己,而不是從父節(jié)點(diǎn)釋放的,要通知父節(jié)點(diǎn),把這個(gè)失效的子節(jié)點(diǎn)移除掉 if (!destroyByParent) { if (parent) { var childIndex = parent._children.indexOf(this); parent._children.splice(childIndex, 1); parent.emit && parent.emit('child-removed', this); } } return destroyByParent; }, // Component的_onPreDestroy _onPreDestroy () { // 移除ActionManagerExist和schedule if (ActionManagerExist) { cc.director.getActionManager().removeAllActionsFromTarget(this); } this.unscheduleAllCallbacks(); // 移除所有的監(jiān)聽(tīng) var eventTargets = this.__eventTargets; for (var i = eventTargets.length - 1; i >= 0; --i) { var target = eventTargets[i]; target && target.targetOff(this); } eventTargets.length = 0; // 編輯器模式下停止監(jiān)控 if (CC_EDITOR && !CC_TEST) { _Scene.AssetsWatcher.stop(this); } // destroyComp的實(shí)現(xiàn)為調(diào)用組件的onDestroy回調(diào),各個(gè)組件會(huì)在回調(diào)中銷毀自身的資源 // 比如RigidBody3D組件會(huì)調(diào)用body的destroy方法,而Animation組件會(huì)調(diào)用stop方法 cc.director._nodeActivator.destroyComp(this); // 將組件從節(jié)點(diǎn)身上移除 this.node._removeComponent(this); },
3.5.4 資源釋放的問(wèn)題
最后我們來(lái)聊一聊資源釋放的問(wèn)題與定位,在加入引用計(jì)數(shù)后,最常見(jiàn)的問(wèn)題還是沒(méi)有正確增減引用計(jì)數(shù)導(dǎo)致的內(nèi)存泄露(循環(huán)引用、少調(diào)用了decRef或多調(diào)用了addRef),以及正在使用的資源被釋放的問(wèn)題(和內(nèi)存泄露相反,資源被提前釋放了)。
從目前的代碼來(lái)看,如果正確使用了引用計(jì)數(shù),新的資源底層是可以避免內(nèi)存泄露等問(wèn)題的
這種問(wèn)題怎么解決呢?首先是定位出哪些資源出了問(wèn)題,如果是被提前釋放,我們可以直接定位到這個(gè)資源,如果是內(nèi)存泄露,當(dāng)我們發(fā)現(xiàn)問(wèn)題時(shí)程序往往已經(jīng)占用了大量的內(nèi)存,這種情況下可以切換到一個(gè)空?qǐng)鼍?,并清理資源,把資源清理完后,可以檢查assets中殘留的資源是否有未被釋放的資源。
要了解資源為什么會(huì)泄露,可以通過(guò)跟蹤addRef和decRef的調(diào)用得到,下面提供了一個(gè)示例方法,用于跟蹤某資源的addRef和decRef調(diào)用,然后調(diào)用資源的dump方法打印出所有調(diào)用的堆棧:
public static traceObject(obj : cc.Asset) { let addRefFunc = obj.addRef; let decRefFunc = obj.decRef; let traceMap = new Map(); obj.addRef = function() : cc.Asset { let stack = ResUtil.getCallStack(1); let cnt = traceMap.has(stack) ? traceMap.get(stack) + 1 : 1; traceMap.set(stack, cnt); return addRefFunc.apply(obj, arguments); } obj.decRef = function() : cc.Asset { let stack = ResUtil.getCallStack(1); let cnt = traceMap.has(stack) ? traceMap.get(stack) + 1 : 1; traceMap.set(stack, cnt); return decRefFunc.apply(obj, arguments); } obj['dump'] = function() { console.log(traceMap); } }
以上就是剖析CocosCreator新資源管理系統(tǒng)的詳細(xì)內(nèi)容,更多關(guān)于CococCreator的資料,請(qǐng)關(guān)注腳本之家其他相關(guān)文章!
- Unity3D實(shí)現(xiàn)攝像機(jī)鏡頭移動(dòng)并限制角度
- 詳解CocosCreator中幾種計(jì)時(shí)器的使用方法
- CocosCreator學(xué)習(xí)之模塊化腳本
- 怎樣在CocosCreator中使用物理引擎關(guān)節(jié)
- 如何在CocosCreator中使用JSZip壓縮
- CocosCreator入門教程之用TS制作第一個(gè)游戲
- 解讀CocosCreator源碼之引擎啟動(dòng)與主循環(huán)
- CocosCreator通用框架設(shè)計(jì)之資源管理
- 如何在CocosCreator中做一個(gè)List
- 如何在CocosCreator中使用http和WebSocket
- CocosCreator怎樣使用cc.follow進(jìn)行鏡頭跟隨
相關(guān)文章
Javascript查看大圖功能代碼實(shí)現(xiàn)
這篇文章主要介紹了Javascript查看大圖功能代碼實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05JavaScript版經(jīng)典游戲之掃雷游戲完整示例【附demo源碼下載】
這篇文章主要介紹了JavaScript版經(jīng)典游戲之掃雷游戲?qū)崿F(xiàn)方法,結(jié)合完整實(shí)例形式分析了掃雷游戲的原理與具體實(shí)現(xiàn)流程,并附帶demo源碼供讀者下載參考,需要的朋友可以參考下2016-12-12javascript正則表達(dá)式中參數(shù)g(全局)的作用
表達(dá)式加上參數(shù)g之后,表明可以進(jìn)行全局匹配,注意這里可以的含義。2010-11-11省市選擇的簡(jiǎn)單實(shí)現(xiàn)(基于zepto.js)
下面小編就為大家?guī)?lái)一篇省市選擇的簡(jiǎn)單實(shí)現(xiàn)(基于zepto.js)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨想過(guò)來(lái)看看吧2016-06-06原生js仿jquery animate動(dòng)畫(huà)效果
這篇文章主要為大家詳細(xì)介紹了原生js仿jquery animate動(dòng)畫(huà)效果,具有一定的,感興趣的小伙伴們可以參考一下2016-07-07