天天看點

Cocos Creator 資源加載流程剖析【三】——Load部分

Load流程是整個資源加載管線的最後一棒,由Loader這個pipe負責(loader.js)。通過Download流程拿到内容之後,需要對内容做一些“加載”處理。使得這些内容可以在遊戲中使用。這裡并不是所有的資源都需要進行一個加載處理,目前隻有圖檔、Json、Plist、Uuid(Prefab、場景)等資源才會執行加載的流程,其他的資源在Download流程之後就可以在遊戲中使用了。
  • Loader處理

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

loadUuid用于加載creator内部統一規劃的資源,每個uuid都會對應一個json對象,可能是prefab、spriteFrame,等等。在loadUuid這個方法中,最關鍵的操作就是cc.deserialize反序列化把資源對象建立了出來,其次就是加載依賴資源。

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等資源外,都不支援延遲加載),或是設定了延遲加載的場景才可以延遲加載。

// 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數組。

depends數組是該資源所依賴的資源數組,loadDepends會調用pipeline.flowInDeps進行加載,如果該數組為空則不加載依賴,執行完成回調。dependKeys數組是item的屬性,記錄了該資源依賴的所有資源,在做資源釋放的時候會用到。預加載的内容會直接進入dependKeys,而正常加載的資源在加載完成後才會被添加到dependKeys中。

最後調用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方法被調用,在這裡會執行這些延遲資源的加載流程。是以延遲加載的資源在節點被激活時會自動加載。下圖是一個延遲加載圖檔的調用堆棧。

Cocos Creator 資源加載流程剖析【三】——Load部分

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也是被禁止延遲加載的。
Cocos Creator 資源加載流程剖析【三】——Load部分