Load流程是整個資源加載管線的最後一棒,由Loader這個pipe負責(loader.js)。經過Download流程拿到內容以後,須要對內容作一些「加載」處理。使得這些內容能夠在遊戲中使用。這裏並非全部的資源都須要進行一個加載處理,目前只有圖片、Json、Plist、Uuid(Prefab、場景)等資源纔會執行加載的流程,其餘的資源在Download流程以後就能夠在遊戲中使用了。json
Loader的handle接收一個item和callback,根據item的type在this.extMap中獲取對應的loadFunc。數組
Loader.prototype.addHandlers = function (extMap) { this.extMap = JS.mixin(this.extMap, extMap); }; Loader.prototype.handle = function (item, callback) { var loadFunc = this.extMap[item.type] || this.extMap['default']; return loadFunc.call(this, item, callback); };
Loader的this.extMap記錄了各類資源類型的下載方式,全部的類型最終都對應這5個加載方法,loadNothing、loadJSON、loadImage、loadPlist、loadUuid,它們對應實現了各類類型資源的加載,經過Loader.addHandlers能夠添加或修改任意資源的加載方式。加載結束後將可用的內容返回。緩存
// 無需加載,即通過前面的下載已經可用了,例如font、script等資源 function loadNothing (item, callback) { return null; } // 使用JSON.parse進行解析並返回 function loadJSON (item, callback) { if (typeof item.content !== 'string') { return new Error('JSON Loader: Input item doesn\'t contain string content'); } try { var result = JSON.parse(item.content); return result; } catch (e) { return new Error('JSON Loader: Parse json [' + item.id + '] failed : ' + e); } } // 建立Texture2D,並根據圖片的內容初始化Texture2D,最後添加到cc.textureCache中 function loadImage (item, callback) { if (sys.platform !== sys.WECHAT_GAME && !(item.content instanceof Image)) { return new Error('Image Loader: Input item doesn\'t contain Image content'); } var rawUrl = item.rawUrl; var tex = cc.textureCache.getTextureForKey(rawUrl) || new Texture2D(); tex.url = rawUrl; tex.initWithElement(item.content); tex.handleLoadedTexture(); cc.textureCache.cacheImage(rawUrl, tex); return tex; } // 使用cc.plistParser進行解析並返回 function loadPlist (item, callback) { if (typeof item.content !== 'string') { return new Error('Plist Loader: Input item doesn\'t contain string content'); } var result = cc.plistParser.parse(item.content); if (result) { return result; } else { return new Error('Plist Loader: Parse [' + item.id + '] failed'); } }
loadUuid用於加載creator內部統一規劃的資源,每一個uuid都會對應一個json對象,多是prefab、spriteFrame,等等。在loadUuid這個方法中,最關鍵的操做就是cc.deserialize反序列化把資源對象建立了出來,其次就是加載依賴資源。app
uuid的解析首先須要一個json對象,若是item的content是string類型,則進行解析,若是是object類型,則直接使用item.content,若是既不是string也不是object則直接報錯。異步
function loadUuid (item, callback) { if (CC_EDITOR) { MissingClass = MissingClass || Editor.require('app://editor/page/scene-utils/missing-class-reporter').MissingClass; } // 獲取json對象,若是是string則進行解析,若是是object則直接使用,報錯則返回Error對象 var json; if (typeof item.content === 'string') { try { json = JSON.parse(item.content); } catch (e) { return new Error('Uuid Loader: Parse asset [' + item.id + '] failed : ' + e.stack); } } else if (typeof item.content === 'object') { json = item.content; } else { return new Error('JSON Loader: Input item doesn\'t contain string content'); } // 根據是否場景對象、編輯器環境來決定classFinder的實現。 var classFinder; var isScene = isSceneObj(json); if (isScene) { if (CC_EDITOR) { // 編輯器 + 場景的模式下,使用MissingClass.classFinder做爲包裹函數 MissingClass.hasMissingClass = false; classFinder = function (type, data, owner, propName) { var res = MissingClass.classFinder(type, data, owner, propName); if (res) { return res; } return cc._MissingScript.getMissingWrapper(type, data); }; classFinder.onDereferenced = MissingClass.classFinder.onDereferenced; } else { // 非編輯器下,使用cc._MissingScript.safeFindClass,也是調用了JS._getClassById // 區別是在解析失敗後會調用cc.deserialize.reportMissingClass(id); classFinder = cc._MissingScript.safeFindClass; } } else { classFinder = function (id) { // JS爲引擎的platform\js.js,而_getClassById方法從_idToClass[classId]中返回Class // _idToClass爲id到類的一個註冊map,key爲id,value爲class // 使用CCClass定義繼承自cc.Component的類會被自動註冊到_idToClass中 // platform\CCClass.js中的var cls = define(name, base, mixins, options); // 最終調用了JS.setClassName,Creator的類的實現細節是另一個大話題 // 這裏只須要了解,全部可拖拽到prefab上的類都會被註冊到JS._idToClass中,這裏的id就是類名 var cls = JS._getClassById(id); if (cls) { return cls; } cc.warnID(4903, id); return Object; }; } // 進行反序列化,反序列化出asset var tdInfo = cc.deserialize.Details.pool.get(); var asset; try { // deserialize的實現位於platform\deserialize.js // 具體的實現很是複雜,大體能夠理解爲new出對應的類,並從json對象中反序列化該類的全部屬性 // 因此返回的asset是這個json最頂層object對應的類,好比cc.SpriteFrame或者自定義的組件 // 該資源所依賴的全部資源會被反序列化到tdInfo中,在tdInfo.uuidList中。 asset = cc.deserialize(json, tdInfo, { classFinder: classFinder, target: item.existingAsset, customEnv: item }); } catch (e) { cc.deserialize.Details.pool.put(tdInfo); var err = CC_JSB ? (e + '\n' + e.stack) : e.stack; return new Error('Uuid Loader: Deserialize asset [' + item.id + '] failed : ' + err); } // 若是是在編輯器下的場景存在類丟失,進行報告(應該是報紅) asset._uuid = item.uuid; if (CC_EDITOR && isScene && MissingClass.hasMissingClass) { MissingClass.reportMissingClass(asset); } // 判斷是否可延遲加載,並調用loadDepends var deferredLoad = canDeferredLoad(asset, item, isScene); loadDepends(this.pipeline, item, asset, tdInfo, deferredLoad, callback); }
canDeferredLoad方法會根據資源類型監測是否能夠延遲加載,當item的deferredLoadRaw爲true且該資源支持延遲加載(在代碼中搜索preventDeferredLoadDependents能夠發現除了TileMap、DragonBones、Spine等資源外,都不支持延遲加載),或是設置了延遲加載的場景才能夠延遲加載。async
// can deferred load raw assets in runtime // 檢查是否延遲加載Raw Assets function canDeferredLoad (asset, item, isScene) { if (CC_EDITOR || CC_JSB) { return false; } var res = item.deferredLoadRaw; if (res) { // check if asset support deferred // 檢查該資源是否支持延遲加載 if (asset instanceof cc.Asset && asset.constructor.preventDeferredLoadDependents) { res = false; } } else if (isScene) { // 若是是prefab或scene,取其asyncLoadAssets屬性 if (asset instanceof cc.SceneAsset || asset instanceof cc.Prefab) { res = asset.asyncLoadAssets; } } return res; }
loadDepends方法會加載依賴,主要作了2個事情,延遲加載和依賴加載。編輯器
延遲加載指的是資源A依賴了B、C、D,其中資源D延遲加載了,那麼BC加載完成即算這個資源加載完成,並執行回調,D也會進行加載,但何時加載完這裏並不關心。在實際應用中的表現就是加載一個場景,基礎部分的內容加載完成了,進入了該場景以後再陸續看到其餘內容加載完成。函數
根據deferredLoadRawAssetsInRuntime,對raw類型資源進行延遲加載,延遲加載的內容會進入dependKeys數組,而不延遲加載的內容進入depends數組。ui
depends數組是該資源所依賴的資源數組,loadDepends會調用pipeline.flowInDeps進行加載,若是該數組爲空則不加載依賴,執行完成回調。dependKeys數組是item的屬性,記錄了該資源依賴的全部資源,在作資源釋放的時候會用到。預加載的內容會直接進入dependKeys,而正常加載的資源在加載完成後纔會被添加到dependKeys中。this
最後調用pipeline.flowInDeps加載depends數組,flowInDeps的完成回調中,若是item加載完成且沒有報錯,調用loadCallback,若是未加載完成,插入到item的queue的 _callbackTable[dependSrc]中或添加queue的監聽(這兩個操做的意義都是在item加載完成後執行loadCallback),loadCallback將依賴對象的依賴屬性進行賦值,並添加該資源的id到dependKeys中。
當反序列化出來的asset._preloadRawFiles有值時,會將callback進行包裹,在異步加載完RawFiles才執行最終的callback。實際並無什麼做用。
function loadDepends (pipeline, item, asset, tdInfo, deferredLoadRawAssetsInRuntime, callback) { // tdInfo.uuidList爲這個prefab或場景所依賴的uuid類型的資源 var uuidList = tdInfo.uuidList; var objList, propList, depends; var i, dependUuid; // cache dependencies for auto release // dependKeys用於緩存該資源的依賴,在資源釋放的時候會用到 var dependKeys = item.dependKeys = []; /******************************* 過濾決定哪些資源要加載,哪些要延遲,得出depends數組 **********************************/ // 若是支持延遲加載 if (deferredLoadRawAssetsInRuntime) { objList = []; propList = []; depends = []; // parse depends assets for (i = 0; i < uuidList.length; i++) { dependUuid = uuidList[i]; var obj = tdInfo.uuidObjList[i]; var prop = tdInfo.uuidPropList[i]; var info = cc.AssetLibrary._getAssetInfoInRuntime(dependUuid); if (info.raw) { // skip preloading raw assets // 對於raw類型的資源不進行加載,tdInfo.uuidObjList[i][prop] = url var url = info.url; obj[prop] = url; dependKeys.push(url); } else { objList.push(obj); propList.push(prop); // declare depends assets // 對於非raw類型的資源,進入depends進行加載,但帶上deferredLoadRaw標記 // 意爲該uuid引用的其餘raw類型的資源進行延遲加載 depends.push({ type: 'uuid', uuid: dependUuid, deferredLoadRaw: true, }); } } } else { objList = tdInfo.uuidObjList; propList = tdInfo.uuidPropList; depends = new Array(uuidList.length); // declare depends assets // 不支持延遲加載則直接進入depends數組,這裏沒有deferredLoadRaw標記 for (i = 0; i < uuidList.length; i++) { dependUuid = uuidList[i]; depends[i] = { type: 'uuid', uuid: dependUuid }; } } /******************************* tdInfo.rawProp和asset._preloadRawFiles的處理 **********************************/ // declare raw // 有些json文件包含了一些raw屬性,以$_$rawType結尾,這裏會直接加載item.url,但目前還未碰到過這樣類型的資源。 // 下面2個說法是錯誤的。 // 若是這個uuid資源自己就是一個raw資源,加載本身? // 若是這個uuid資源存在raw屬性,例如一個腳本拖拽了一個Texture2D類型的資源做爲它的成員變量? if (tdInfo.rawProp) { objList.push(asset); propList.push(tdInfo.rawProp); depends.push(item.url); } // preload raw files // 預加載它的raw文件,這裏是asset的屬性,但從引擎代碼沒有看到哪裏對這個屬性賦值過 // 不過prefab等文件卻是有一個_rawFiles的屬性,但從代碼上看也與這個方法無關,看上去倒像是預留的一個接口 // 提供給開發者作某種資源類型的完成回調包裝。 if (asset._preloadRawFiles) { var finalCallback = callback; callback = function () { asset._preloadRawFiles(function (err) { finalCallback(err || null, asset); }); }; } // fast path // 若是沒有資源要加載就直接返回 if (depends.length === 0) { cc.deserialize.Details.pool.put(tdInfo); return callback(null, asset); } /******************************* 調用pipeline.flowInDeps進行依賴加載,資源加載完成後調用loadCallback **********************************/ // Predefine content for dependencies usage // 加載depends,加載完成後註冊到item.dependKeys中,並賦值給this.obj[this.prop] item.content = asset; pipeline.flowInDeps(item, depends, function (errors, items) { // 這個回調在全部的item都加載完成後執行,因此item都是有的,但有可能有報錯 var item, missingAssetReporter; for (var src in items.map) { item = items.map[src]; if (item.uuid && item.content) { item.content._uuid = item.uuid; } } for (var i = 0; i < depends.length; i++) { var dependSrc = depends[i].uuid; var dependUrl = depends[i].url; var dependObj = objList[i]; var dependProp = propList[i]; item = items.map[dependUrl]; if (item) { var thisOfLoadCallback = { obj: dependObj, prop: dependProp }; // 資源加載完成的回調,關聯依賴對象obj的prop爲item的value function loadCallback (item) { var value = item.isRawAsset ? item.rawUrl : item.content; this.obj[this.prop] = value; if (item.uuid !== asset._uuid && dependKeys.indexOf(item.id) < 0) { dependKeys.push(item.id); } } // 若是資源已經加載完了,且沒有報錯,則執行loadCallback回調 if (item.complete || item.content) { if (item.error) { if (CC_EDITOR && item.error.errorCode === 'db.NOTFOUND') { if (!missingAssetReporter) { var MissingObjectReporter = Editor.require('app://editor/page/scene-utils/missing-object-reporter'); missingAssetReporter = new MissingObjectReporter(asset); } missingAssetReporter.stashByOwner(dependObj, dependProp, Editor.serialize.asAsset(dependSrc)); } else { cc._throw(item.error); } } else { loadCallback.call(thisOfLoadCallback, item); } } else { // item was removed from cache, but ready in pipeline actually // 該item從cache中移除了?但在pipeline中? // 這裏監聽了該item的加載完成事件,在加載完成時調用loadCallback var queue = LoadingItems.getQueue(item); // Hack to get a better behavior // 這個behavior很是的bad,_callbackTable是CallbacksHandler的成員變量 // 兩個操做都是添加監聽,但前者是直接拿到監聽該事件的回調數組,強行插入 var list = queue._callbackTable[dependSrc]; if (list) { list.unshift(loadCallback, thisOfLoadCallback); } else { queue.addListener(dependSrc, loadCallback, thisOfLoadCallback); } } } } if (CC_EDITOR && missingAssetReporter) { missingAssetReporter.reportByOwner(); } cc.deserialize.Details.pool.put(tdInfo); callback(null, asset); }); }
CCLoader的flowInDeps,實現以下,傳入資源的owner,依賴列表urlList,以及urlList的回調。當一個依賴又有依賴的時候,queue的append又會走到這個新資源的loadUuid,去加載那一層所依賴的資源。而flowInDeps開頭的var item = this._cache[res.url] 也確保了資源不會被重複加載。
proto.flowInDeps = function (owner, urlList, callback) { // 準備_sharedList,已加載或正在加載的資源push item,未加載的push res _sharedList.length = 0; for (var i = 0; i < urlList.length; ++i) { var res = getResWithUrl(urlList[i]); if (!res.url && ! res.uuid) continue; var item = this._cache[res.url]; if (item) { _sharedList.push(item); } else { _sharedList.push(res); } } // 建立一個新的隊列,當有owner時,將子隊列的進度同步到ownerQueue var queue = LoadingItems.create(this, owner ? function (completedCount, totalCount, item) { if (this._ownerQueue && this._ownerQueue.onProgress) { this._ownerQueue._childOnProgress(item); } } : null, function (errors, items) { callback(errors, items); // Clear deps because it's already done // Each item will only flowInDeps once, so it's still safe here // 加載完成後清除owner.deps數組 owner && owner.deps && (owner.deps.length = 0); items.destroy(); }); if (owner) { var ownerQueue = LoadingItems.getQueue(owner); // Set the root ownerQueue, if no ownerQueue defined in ownerQueue, it's the root // 設置queue的ownerQueue queue._ownerQueue = ownerQueue._ownerQueue || ownerQueue; } var accepted = queue.append(_sharedList, owner); _sharedList.length = 0; return accepted; };
在creator編輯器中能夠設置場景和prefab的延遲加載,設置了延遲加載以後,場景或prefab所引用的一些Raw類型資源如cc.Texture2D、cc.AudioClip等會延遲加載,同時,玩家進入場景後可能會看到一些資源陸續顯示出來,而且激活新界面時也可能會看到界面中的元素陸續顯示出來,所以這種加載方式更適合網頁遊戲。
具體的實現是在loadUuid中執行canDeferredLoad時,它的asset.asyncLoadAssets爲一個Object。在後面的loadDepends方法中會執行deferredLoadRawAssetsInRuntime的判斷。全部Raw類型的資源會被延遲加載,而非Raw類型的資源會被添加到depends數組中進行加載。最終加載完成時咱們能夠獲得一個不完整的資源(由於它有一部分依賴被延遲加載了)。
從整個Pipeline的加載流程來看,並無任何地方去加載這些被延遲的Raw類型資源,而在底層加載圖片的地方進行斷點,能夠發現當場景或Prefab被激活時(添加到場景中),會有一個ensureLoadTexture方法被調用,在這裏會執行這些延遲資源的加載流程。因此延遲加載的資源在節點被激活時會自動加載。下圖是一個延遲加載圖片的調用堆棧。
ensureLoadTexture的實現以下所示,AudioClip也相似,在調用play播放聲音時會執行preload,檢測到聲音沒有被加載時會執行cc.loader.load方法加載聲音。
/** * !#en If a loading scene (or prefab) is marked as `asyncLoadAssets`, all the textures of the SpriteFrame which * associated by user's custom Components in the scene, will not preload automatically. * These textures will be load when Sprite component is going to render the SpriteFrames. * You can call this method if you want to load the texture early. * !#zh 當加載中的場景或 Prefab 被標記爲 `asyncLoadAssets` 時,用戶在場景中由自定義組件關聯到的全部 SpriteFrame 的貼圖都不會被提早加載。 * 只有當 Sprite 組件要渲染這些 SpriteFrame 時,纔會檢查貼圖是否加載。若是你但願加載過程提早,你能夠手工調用這個方法。 */ ensureLoadTexture: function () { if (!this._texture) { this._loadTexture(); } }, _loadTexture: function () { if (this._textureFilename) { // 這裏返回的tex多是一個未加載完成的紋理,如紋理未加載完成,可監聽其加載完成回調 var texture = cc.textureCache.addImage(this._textureFilename); this._refreshTexture(texture); } }, _refreshTexture: function (texture) { var self = this; if (self._texture !== texture) { var locLoaded = texture.loaded; this._textureLoaded = locLoaded; this._texture = texture; function textureLoadedCallback () { if (!self._texture) { // clearTexture called while loading texture... // 在加載紋理的時候調用了clearTexture方法 return; } self._textureLoaded = true; var w = texture.width, h = texture.height; // 若是在Canvas模式下,圖片有旋轉,須要進行旋轉的特殊處理(_cutRotateImageToCanvas) if (self._rotated && cc._renderType === cc.game.RENDER_TYPE_CANVAS) { var tempElement = texture.getHtmlElementObj(); tempElement = _ccsg.Sprite.CanvasRenderCmd._cutRotateImageToCanvas(tempElement, self.getRect()); var tempTexture = new cc.Texture2D(); tempTexture.initWithElement(tempElement); tempTexture.handleLoadedTexture(); self._texture = tempTexture; self._rotated = false; w = self._texture.width; h = self._texture.height; self.setRect(cc.rect(0, 0, w, h)); } if (self._rect) { self._checkRect(self._texture); } else { self.setRect(cc.rect(0, 0, w, h)); } if (!self._originalSize) { self.setOriginalSize(cc.size(w, h)); } if (!self._offset) { self.setOffset(cc.v2(0, 0)); } // dispatch 'load' event of cc.SpriteFrame // cc.SpriteFrame的觸發load事件 self.emit("load"); } // 若是圖片已加載完,則直接執行回調,不然監聽texture的load方法 if (locLoaded) { textureLoadedCallback(); } else { texture.once("load", textureLoadedCallback); } } },
在Creator的官方文檔中介紹到「Spine 和 TiledMap 依賴的資源永遠都不會被延遲加載」,這主要是由於它們對Raw資源是一個強依賴,也就是說節點被激活時就必須使用到它們的紋理,因此不能延遲加載。那麼它們是如何實現禁止延遲加載的呢?
在canDeferredLoad方法中,若是資源的asset.constructor.preventDeferredLoadDependents爲true時,會強制返回false。在引擎中進行搜索能夠發現,除了Spine和TiledMap,還有DragonBones也是被禁止延遲加載的。