這系列文章會對Cocos Creator的資源加載和管理進行深刻的剖析。主要包含如下內容:javascript
前面4章節介紹了完整的資源加載流程以及資源管理,以及如何自定義這個加載流程(有時候咱們須要加載一些特殊類型的資源)。「從編輯器到運行時」介紹了咱們在編輯器中編輯的場景、Prefab等資源是如何序列化到磁盤,打包發佈以後又是如何被加載到遊戲中。html
在開始以前咱們須要解決這幾個問題:java
引擎的代碼大致分爲js和原生c++ 兩種類型,在web平臺上不使用任何 c++ 代碼,而是一個基於webgl編寫的渲染底層。而在移動平臺上仍然使用 c++ 的底層,經過jsb將原生的接口暴露給上層的js。在引擎安裝目錄下的resources/engine下放着引擎的全部js代碼。而原生c++ 代碼放在引擎安裝目錄下的resources/cocos2d-x目錄下。咱們能夠在這兩個目錄下查看代碼。這系列文章中咱們要查看的代碼位於引擎安裝目錄下的resources/engine/cocos2d/core/load-pipeline目錄下。c++
JS的調試很是簡單,咱們能夠在Chrome瀏覽器運行程序,按F12進入調試模式,經過ctrl + p快捷鍵能夠根據文件名搜索源碼,進行斷點調試。具體的各類調試技巧可參考如下幾個教程。web
原平生臺的調試也能夠用Chrome,官方的文檔介紹瞭如何調試原生普通的JS代碼。至於原平生臺的C++ 代碼調試,能夠在Windows上使用Visual Studio調試,也能夠在Mac上使用XCode調試。chrome
首先咱們從總體上觀察CCLoader大體的類結構,這個密密麻麻的圖估計沒有人會仔細看,因此這裏簡單介紹一下:json
CocosCreator2.x和1.x版本對比,整個加載的流程沒有太大的變化,主要的變化是引入了FontLoader,將Font初始化的邏輯從Downloader轉移到了Loader這個Pipe中。將JSB的部分分開,在編譯時完全根據不一樣的平臺編譯不一樣的js,而不是在一個js中使用條件判斷當前是什麼平臺來執行對應的代碼。其餘優化了一些寫法,好比cc.Class.inInstanceOf調整爲instanceof,JS.getClassName、cc.isChildClassOf等方法移動到js這個模塊中。數組
CCLoader提供了多種加載資源的接口,要加載的資源必須放到resources目錄下,咱們在加載資源的時候,除了要加載的資源url和完成回調,最好將type參數傳入,這是一個良好的習慣。CCLoader提供瞭如下加載資源的接口:瀏覽器
loadRes是咱們最經常使用的一個接口,該函數主要作了3個事情:緩存
proto.loadRes = function (url, type, progressCallback, completeCallback) { var args = this._parseLoadResArgs(type, progressCallback, completeCallback); type = args.type; progressCallback = args.onProgress; completeCallback = args.onComplete; var self = this; var uuid = self._getResUuid(url, type); if (uuid) { this.load( { type: 'uuid', uuid: uuid }, progressCallback, function (err, asset) { if (asset) { // 禁止自動釋放資源 self.setAutoReleaseRecursively(uuid, false); } if (completeCallback) { completeCallback(err, asset); } } ); } else { self._urlNotFound(url, type, completeCallback); } };
不管調用哪一個接口,最後都會走到load函數,load函數作了幾個事情,首先是對輸入的參數進行處理,以知足其餘資源加載接口的調用,全部要加載的資源最後會被添加到_sharedResources中(不論該資源是否已加載,若是已加載會push它的item,未加載會push它的res對象,res對象是經過getResWithUrl方法從AssetLibrary中查詢出來的,AssetLibrary在後面的章節中會詳細介紹)。
load和其它接口的最大區別在於,load能夠用於加載絕對路徑的資源(好比一個sd卡的絕對路徑、或者網絡上的一個url),而loadRes等只能加載resources目錄下的資源。
proto.load = function(resources, progressCallback, completeCallback) { // 下面這幾段代碼對輸入的參數進行了處理,保證了load函數的各類重載寫法能被正確識別 // progressCallback是可選的,能夠只傳入resources和completeCallback if (completeCallback === undefined) { completeCallback = progressCallback; progressCallback = this.onProgress || null; } // 檢測是否爲單個資源的加載 var self = this; var singleRes = false; if (!(resources instanceof Array)) { singleRes = true; resources = resources ? [resources] : []; } // 將待加載的資源放到_sharedResources數組中 _sharedResources.length = 0; for (var i = 0; i < resources.length; ++i) { var resource = resources[i]; // 前向兼容 {id: 'http://example.com/getImageREST?file=a.png', type: 'png'} 這種寫法 if (resource && resource.id) { cc.warnID(4920, resource.id); if (!resource.uuid && !resource.url) { resource.url = resource.id; } } // 支持如下格式的寫法 // 1. {url: 'http://example.com/getImageREST?file=a.png', type: 'png'} // 2. 'http://example.com/a.png' // 3. 'a.png' var res = getResWithUrl(resource); if (!res.url && !res.uuid) continue; // 若是是已加載過的資源這裏會把它取出 var item = this._cache[res.url]; _sharedResources.push(item || res); } // 建立一個LoadingItems加載隊列,在全部資源加載完成後的下一幀執行完成回調 var queue = LoadingItems.create(this, progressCallback, function (errors, items) { callInNextTick(function () { if (completeCallback) { if (singleRes) { let id = res.url; completeCallback.call(self, items.getError(id), items.getContent(id)); } else { completeCallback.call(self, errors, items); } completeCallback = null; } if (CC_EDITOR) { for (let id in self._cache) { if (self._cache[id].complete) { self.removeItem(id); } } } items.destroy(); }); }); // 初始化隊列 LoadingItems.initQueueDeps(queue); // 真正的啓動加載管線 queue.append(_sharedResources); _sharedResources.length = 0; };
初始化_sharedResources以後,開始建立一個LoadingItems,將調用queue.append將_sharedResources追加到LoadingItems中。特別須要注意的地方是,咱們的加載完成回調,至少會在下一幀才執行,由於這裏用了一個callInNextTick包裹了傳入的completeCallback。
LoadingItems.create方法主要的職責包含LoadingItems的建立(使用對象池進行復用),綁定onProgress和onComplete回調到queue對象中(建立出來的LoadingItems類實例)。
queue.append完成了資源加載的準備和啓動,首先遍歷要加載的全部資源(urlList),檢查已在隊列中的資源對象,若是已經加載完成或者爲循環引用對象則當作加載完成處理,不然在該資源的加載隊列中添加監聽,在資源加載完成後執行self.itemComplete(item.id)。
若是是一個全新的資源,則調用createItem建立這個資源的item,把item放到this.map和accepted數組中。綜上,若是咱們使用CCLoader去加載一個已加載完成的資源,也會在下一幀才獲得回調。
proto.append = function (urlList, owner) { if (!this.active) { return []; } if (owner && !owner.deps) { owner.deps = []; } this._appending = true; var accepted = [], i, url, item; for (i = 0; i < urlList.length; ++i) { url = urlList[i]; // 已經在另外一個LoadingItems隊列中了,url對象就是實際的item對象 // 在load方法中,若是已加載或正在加載,會取出_cache[res.url]添加到urlList if (url.queueId && !this.map[url.id]) { this.map[url.id] = url; // 將url添加到owner的deps數組中,以便於檢測循環引用 owner && owner.deps.push(url); // 已加載完成或循環引用(在遞歸該資源的依賴時,發現了該資源本身的id,owner.id) if (url.complete || checkCircleReference(owner, url)) { this.totalCount++; this.itemComplete(url.id); continue; } // 還未加載完成,須要等待其加載完成 else { var self = this; var queue = _queues[url.queueId]; if (queue) { this.totalCount++; LoadingItems.registerQueueDep(owner || this._id, url.id); // 已經在其它隊列中加載了,監聽那個隊列該資源加載完成的事件便可 // 若是加載失敗,錯誤會記錄在item.error中 queue.addListener(url.id, function (item) { self.itemComplete(item.id); }); } continue; } } // 隊列中的新item,從未加載過 if (isIdValid(url)) { item = createItem(url, this._id); var key = item.id; // 不存在重複的url if (!this.map[key]) { this.map[key] = item; this.totalCount++; // 將item添加到owner的deps數組中,以便於檢測循環引用 owner && owner.deps.push(item); LoadingItems.registerQueueDep(owner || this._id, key); accepted.push(item); } } } this._appending = false; // 所有完成則手動結束 if (this.completedCount === this.totalCount) { this.allComplete(); } else { // 開始加載本次須要加載的資源(accepted數組) this._pipeline.flowIn(accepted); } return accepted; };
若是所有資源已經加載完成,則執行this.allComplete,不然調用this._pipeline.flowIn(accepted),啓動由本隊列進行加載的部分資源。
基本上全部的資源都會有一個uuid,Creator會爲它生成一個json文件,通常都是先加載其json文件,再進一步加載其依賴資源。CCLoader和LoadingItems自己並不處理這些依賴資源的加載,依賴加載是由UuidLoader這個加載器進行加載的。這個設計看上去會致使的一個問題就是加載大部分的資源都會有2個io操做,一個是json文件的加載,一個是raw資源的加載。Creator是如何處理資源的,具體可參考《從編輯器到運行時》一章。
在LoadingItems的append方法中,調用了flowIn啓動了Pipeline,傳入的accepted數組爲新加載的資源——即未加載完成,也不處於加載中的資源。
Pipeline的flowIn方法中獲取this._pipes的第一個pipe,遍歷全部的item,調用flow傳入該pipe來處理每個item。若是獲取不到第一個pipe,則調用flowOut來處理全部的item,直接將item從Pipeline中流出。
默認狀況下,CCLoader初始化有3個Pipe,分別是AssetLoader(獲取資源的詳細信息以便於決定後續使用何種方式處理)、Downloader(處理了iOS、Android、Web等平臺以及各類類型資源的下載——即讀取文件)、Loader(對已下載的資源進行加載解析處理,使遊戲內能夠直接使用)。
proto.flowIn = function (items) { var i, pipe = this._pipes[0], item; if (pipe) { // 第一步先Cache全部的item,以防止重複加載相同的item!!! for (i = 0; i < items.length; i++) { item = items[i]; this._cache[item.id] = item; } for (i = 0; i < items.length; i++) { item = items[i]; flow(pipe, item); } } else { for (i = 0; i < items.length; i++) { this.flowOut(items[i]); } } };
flow方法主要的職責包含檢查item處理的狀態,若是有異常進行異常處理,調用pipe的handle方法對item進行處理,銜接下一個pipe,若是沒有下一個pipe則調用Pipeline.flowOut對item進行流出。
function flow (pipe, item) { var pipeId = pipe.id; var itemState = item.states[pipeId]; var next = pipe.next; var pipeline = pipe.pipeline; // 出錯或已在處理中則不須要進行處理 if (item.error || itemState === ItemState.WORKING || itemState === ItemState.ERROR) { return; // 已完成則驅動下一步 } else if (itemState === ItemState.COMPLETE) { if (next) { flow(next, item); } else { pipeline.flowOut(item); } } else { // 開始處理 item.states[pipeId] = ItemState.WORKING; // pipe.handle【可能】是異步的,傳入匿名函數在pipe執行完時調用 var result = pipe.handle(item, function (err, result) { if (err) { item.error = err; item.states[pipeId] = ItemState.ERROR; pipeline.flowOut(item); } else { // result能夠爲null,這意味着該pipe沒有result if (result) { item.content = result; } item.states[pipeId] = ItemState.COMPLETE; if (next) { flow(next, item); } else { pipeline.flowOut(item); } } }); // 若是返回了一個Error類型的result,則要進行記錄,修改item狀態,並調用flowOut流出item if (result instanceof Error) { item.error = result; item.states[pipeId] = ItemState.ERROR; pipeline.flowOut(item); } // 若是返回了非undefined的結果 else if (result !== undefined) { // 意爲着這個pipe沒有result if (result !== null) { item.content = result; } item.states[pipeId] = ItemState.COMPLETE; if (next) { flow(next, item); } else { pipeline.flowOut(item); } } // 其它狀況爲返回了undefined,這意味着這個pipe是一個異步的pipe,且啓動handle的時候沒有出現錯誤,咱們傳入的回調會被執行,在回調中驅動下一個pipe或結束Pipeline。 } }
flowOut方法流出資源,若是item在Pipeline處理中出現了錯誤,會被刪除。不然會保存該item到this._cache中,this._cache中是緩存全部已加載資源的容器。最後調用LoadingItems.itemComplete(item),這個方法會驅動onProgress、onCompleted等方法的執行。
proto.flowOut = function (item) { if (item.error) { delete this._cache[item.id]; } else if (!this._cache[item.id]) { this._cache[item.id] = item; } item.complete = true; LoadingItems.itemComplete(item); };
在每個item加載結束後,都會執行LoadingItems.itemComplete進行收尾。
proto.itemComplete = function (id) { var item = this.map[id]; if (!item) { return; } // 錯誤處理 var errorListId = this._errorUrls.indexOf(id); if (item.error && errorListId === -1) { this._errorUrls.push(id); } else if (!item.error && errorListId !== -1) { this._errorUrls.splice(errorListId, 1); } this.completed[id] = item; this.completedCount++; // 遍歷_queueDeps,找到全部依賴該資源的queue,將該資源添加到對應queue的completed數組中 LoadingItems.finishDep(item.id); // 進度回調 if (this.onProgress) { var dep = _queueDeps[this._id]; this.onProgress(dep ? dep.completed.length : this.completedCount, dep ? dep.deps.length : this.totalCount, item); } // 觸發該id加載結束的事件,全部依賴該資源的LoadingItems對象會觸發該事件 this.invoke(id, item); // 移除該id的全部監聽回調 this.removeAll(id); // 若是所有加載完成了,會執行allComplete,驅動onComplete回調 if (!this._appending && this.completedCount >= this.totalCount) { // console.log('===== All Completed '); this.allComplete(); } };
AssetLoader是Pipeline的第一個Pipe,這個Pipe的職責是進行初始化,從cc.AssetLibrary中取出該資源的完整信息,獲取該資源的類型,對rawAsset類型進行設置type,方便後面的pipe執行不一樣的處理,而非rawAsset則執行callback進入下一個Pipe處理。其實AssetLoader在這裏的做用看上去並不大,由於基本上全部的資源走到這裏都是直接執行回調或返回,從Creator最開始的代碼來看,默認只有Downloader和Loader兩個Pipe。且我在調試的時候註釋了Pipeline初始化AssetLoader的地方,在一個開發到後期的項目中測試發現對資源加載這塊毫無影響。
咱們調用loadRes加載的資源都會被轉爲uuid,因此都會經過cc.AssetLibrary.queryAssetInfo查詢到對應的信息。而後執行item.type = 'uuid',對應的raw類型資源,如紋理會在UuidLoader中進行依賴加載的處理,詳見Load部分。
var AssetLoader = function (extMap) { this.id = ID; this.async = true; this.pipeline = null; }; AssetLoader.ID = ID; var reusedArray = []; AssetLoader.prototype.handle = function (item, callback) { var uuid = item.uuid; if (!uuid) { return !!item.content ? item.content : null; } var self = this; cc.AssetLibrary.queryAssetInfo(uuid, function (error, url, isRawAsset) { if (error) { callback(error); } else { item.url = item.rawUrl = url; item.isRawAsset = isRawAsset; if (isRawAsset) { /* 基本上raw類型的資源也不會走到這個分支,通過各類調試都沒有讓程序運行到這個分支下, 由於全部的資源在加載的時候都是先獲取其uuid進行加載的。而沒有uuid的狀況基本在這個函數的第一行判斷uuid的時候就返回了。 我還嘗試了直接用cc.loader.load加載resources的資源,直接傳入resources下的文件會報路徑錯誤。 提示的錯誤相似 http://localhost:7456/loadingBar/image.png 404錯誤。 正確的路徑應該是在res/import/...下的,使用使用cc.url.raw能夠獲取到正確的路徑。 我將一個紋理修改成RAW類型資源進行加載,並使用cc.url.raw進行加載,直接在函數開始的uuid判斷這裏返回了。 另外一個嘗試是加載網絡中的資源,然而都在函數開始的uuid判斷處返回了。 因此這段代碼應該是被廢棄的,不被維護的代碼。*/ var ext = Path.extname(url).toLowerCase(); if (!ext) { callback(new Error(cc._getError(4931, uuid, url))); return; } ext = ext.substr(1); var queue = LoadingItems.getQueue(item); reusedArray[0] = { queueId: item.queueId, id: url, url: url, type: ext, error: null, alias: item, complete: true }; if (CC_EDITOR) { self.pipeline._cache[url] = reusedArray[0]; } queue.append(reusedArray); // 傳遞給特定type的Downloader item.type = ext; callback(null, item.content); } else { item.type = 'uuid'; callback(null, item.content); } } }); }; Pipeline.AssetLoader = module.exports = AssetLoader;