Cocos Creator 資源加載流程剖析【一】——cc.loader與加載管線

這系列文章會對Cocos Creator的資源加載和管理進行深刻的剖析。主要包含如下內容:javascript

  • cc.loader與加載管線
  • Download部分
  • Load部分
  • 額外流程(MD5 Pipe)
  • 從編輯器到運行時
  • 場景切換流程

前面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

  • 咱們的CCLoader繼承於Pipeline,CCLoader提供了友好的資源管理接口(加載、獲取、釋放)以及一些輔助接口(如自動釋放、對Pipeline的修改)。
  • Pipeline中主要包含了多個Pipe和多個LoadingItems,這裏實現了一個Pipe到Pipe銜接流轉的過程,以及Pipe和LoadingItems的管理接口。
  • Pipe有多種子類,每一種Pipe都會對資源進行特定的加工,後面會對每一種Pipe都做詳細介紹。
  • LoadingItems爲一個加載隊列,繼承於CallbackInvoker,管理着LoadingItem(注意沒有複數),一個LoadingItem就是資源從開始加載到加載完成的上下文。這裏說的上下文,指的是與加載該資源相關的變量的集合,好比當前加載的狀態、url、依賴哪些資源、以及加載完成後的對象等等。

image

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提供瞭如下加載資源的接口:瀏覽器

  • load(resources, progressCallback, completeCallback)
  • loadRes(url, type, progressCallback, completeCallback)
  • loadResArray(urls, type, progressCallback, completeCallback)
  • loadResDir(url, type, progressCallback, completeCallback)

loadRes是咱們最經常使用的一個接口,該函數主要作了3個事情:緩存

  • 調用_getResUuid查詢uuid,該方法會調用AssetTable的getUuid方法查詢資源的uuid。從網絡上加載的資源以及SD卡中咱們存儲的資源,Creator並無爲它們生成uuid。因此這些不是在Creator項目中生成的資源不能使用loadRes來加載
  • 調用this.load方法加載資源。
  • 在加載完成後,該資源以及其引用的資源都會被標記爲禁止自動釋放(在場景切換的時候,Creator會自動釋放下個場景不使用的資源)。
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是如何處理資源的,具體可參考《從編輯器到運行時》一章。

Pipeline的流轉

在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

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;
相關文章
相關標籤/搜索