Cocos Creator 新資源管理系統剖析

v2.4開始,Creator使用AssetBundle徹底重構了資源底層,提供了更加靈活強大的資源管理方式,也解決了以前版本資源管理的痛點(資源依賴與引用),本文將帶你深刻了解Creator的新資源底層。css

  • 資源與構建
  • 理解與使用AssetBundle
  • 新資源框架剖析
    • 加載管線
    • 文件下載
    • 文件解析
    • 依賴加載
    • 資源釋放

1.資源與構建

1.1 creator資源文件基礎

在瞭解引擎如何解析、加載資源以前,咱們先來了解一下這些資源文件(圖片、Prefab、動畫等)的規則,在creator項目目錄下有幾個與資源相關的目錄:html

  • assets 全部資源的總目錄,對應creator編輯器的資源管理器
  • library 本地資源庫,預覽項目時使用的目錄
  • build 構建後的項目默認目錄

在assets目錄下,creator會爲每一個資源文件和目錄生成一個同名的.meta文件,meta文件是一個json文件,記錄了資源的版本、uuid以及各類自定義的信息(在編輯器的屬性檢查器中設置),好比prefab的meta文件,就記錄了咱們能夠在編輯器修改的optimizationPolicy和asyncLoadAssets等屬性。node

{
  "ver": "1.2.7",
  "uuid": "a8accd2e-6622-4c31-8a1e-4db5f2b568b5",
  "optimizationPolicy": "AUTO",     // prefab建立優化策略
  "asyncLoadAssets": false,         // 是否延遲加載
  "readonly": false,
  "subMetas": {}
}

在library目錄下的imports目錄,資源文件名會被轉換成uuid,並取uuid前2個字符進行目錄分組存放,creator會將全部資源的uuid到assets目錄的映射關係,以及資源和meta的最後更新時間戳放到一個名爲uuid-to-mtime.json的文件中,以下所示。web

{
  "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目錄並無爲目錄生成任何東西。json

1.2 資源構建

在項目構建以後,資源會從library目錄下移動到構建輸出的build目錄中,基本只會導出參與構建的場景和resources目錄下的資源,及其引用到的資源。腳本資源會由多個js腳本合併爲一個js,各類json文件也會按照特定的規則進行打包。咱們能夠在Bundle的配置界面和項目的構建界面爲Bundle和項目設置數組

1.2.1 圖片、圖集、自動圖集

導入編輯器的每張圖片都會對應生成一個json文件,用於描述Texture的信息,以下所示,默認狀況下項目中全部的Texture2D的json文件會被壓縮成一個,若是選擇無壓縮,則每一個圖片都會生成一個Texture2D的json文件。緩存

{
  "__type__": "cc.Texture2D",
  "content": "0,9729,9729,33071,33071,0,0,1"
}

若是將紋理的Type屬性設置爲Sprite,Creator還會自動生成了SpriteFrame類型的json文件。
圖集資源除了圖片外,還對應一個圖集json,這個json包含了cc.SpriteAtlas信息,以及每一個碎圖的SpriteFrame信息
自動圖集在默認狀況下只包含了cc.SpriteAtlas信息,在勾選內聯全部SpriteFrame的狀況下,會合並全部SpriteFrame服務器

1.2.2 Prefab與場景

場景資源與Prefab資源很是相似,都是一個描述了全部節點、組件等信息的json文件,在勾選內聯全部SpriteFrame的狀況下,Prefab引用到的SpriteFrame會被合併到prefab所在的json文件中,若是一個SpriteFrame被多個prefab引用,那麼每一個prefab的json文件都會包含該SpriteFrame的信息。而在沒有勾選內聯全部SpriteFrame的狀況下,SpriteFrame會是單獨的json文件。微信

1.2.3 資源文件合併規則

當Creator將多個資源合併到一個json文件中,咱們能夠在config.json中的packs字段找到被打包的資源信息,一個資源有可能被重複打包到多個json中。下面舉一個例子,展現在不一樣的選項下,creator的構建規則:網絡

  • a.png 一個單獨的Sprite類型圖片
  • dir/b.png、c.png、AutoAtlas dir目錄下包含2張圖片,以及一個AutoAtlas
  • d.png、d.plist 普通圖集
  • e.prefab 引用了SpriteFrame a和b的prefab
  • f.prefab 引用了SpriteFrame b的prefab

下面是按不一樣規則構建後的文件,能夠看到,無壓縮的狀況下生成的文件數量是最多的,不內聯的文件會比內聯多,但內聯可能會致使同一個文件被重複包含,好比e和f這兩個Prefab都引用了同一個圖片,這個圖片的SpriteFrame.json會被重複包含,合併成一個json則只會生成一個文件。

資源文件 無壓縮 默認(不內聯) 默認(內聯) 合併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

默認選項在絕大多數狀況下都是一個不錯的選擇,若是是web平臺,建議勾選內聯全部SpriteFrame這能夠減小網絡io,提升性能,而原平生臺不建議勾選,這可能會增長包體大小以及熱更時要下載的內容。對於一些緊湊的Bundle(好比加載該Bundle就須要用到裏面全部的資源),咱們能夠配置爲合併全部的json。

2. 理解與使用 Asset Bundle

2.1 建立Bundle

Asset Bundle是creator 2.4以後的資源管理方案,簡單地說就是經過目錄來對資源進行規劃,按照項目的需求將各類資源放到不一樣的目錄下,並將目錄配置成Asset Bundle。可以起到如下做用:

  • 加快遊戲啓動時間
  • 減少首包體積
  • 跨項目複用資源
  • 方便實現子游戲
  • 以Bundle爲單位的熱更新

Asset Bundle的建立很是簡單,只要在目錄的屬性檢查器中勾選配置爲bundle便可,其中的選項官方文檔都有比較詳細的介紹。

其中關於壓縮的理解,文檔並無詳細的描述,這裏的壓縮指的並非zip之類的壓縮,而是經過packAssets的方式,把多個資源的json文件合併到一個,達到減小io的目的。

在選項上打勾很是簡單,真正的關鍵在於如何規劃Bundle,規劃的原則在於減小包體、加速啓動以及資源複用。根據遊戲的模塊來規劃資源是比較不錯的選擇,好比按子游戲、關卡副本、或者系統功能來規劃。

Bundle會自動將文件夾下的資源,以及文件夾中引用到的其它文件夾下的資源打包(若是這些資源不是在其它Bundle中),若是咱們按照模塊來規劃資源,很容易出現多個Bundle共用了某個資源的狀況。能夠將公共資源提取到一個Bundle中,或者設置某個Bundle有較高的優先級,構建Bundle的依賴關係,不然這些資源會同時放到多個Bundle中(若是是本地Bundle,這會致使包體變大)。

2.2 使用Bundle

Bundle的使用也很是簡單,若是是resources目錄下的資源,能夠直接使用cc.resources.load來加載

cc.resources.load("test assets/prefab", function (err, prefab) {
    var newNode = cc.instantiate(prefab);
    cc.director.getScene().addChild(newNode);
});

若是是其它自定義Bundle(本地Bundle或遠程Bundle均可以用Bundle名加載),可使用cc.assetManager.loadBundle來加載Bundle,而後使用加載後的Bundle對象,來加載Bundle中的資源。對於原平生臺,若是Bundle被配置爲遠程包,在構建時須要在構建發佈面板中填寫資源服務器地址。

cc.assetManager.loadBundle('01_graphics', (err, bundle) => {
    bundle.load('xxx');
});

原生或小遊戲平臺下,咱們還能夠這樣使用Bundle:

  • 若是要加載其它項目的遠程Bundle,則須要使用url的方式加載(其它項目指另外一個cocos工程)
  • 若是但願本身管理Bundle的下載和緩存,能夠放到本地可寫路徑,並傳入路徑來加載這些Bundle
// 當複用其餘項目的 Asset Bundle 時
cc.assetManager.loadBundle('https://othergame.com/remote/01_graphics', (err, bundle) => {
    bundle.load('xxx');
});

// 原平生臺
cc.assetManager.loadBundle(jsb.fileUtils.getWritablePath() + '/pathToBundle/bundleName', (err, bundle) => {
    // ...
});

// 微信小遊戲平臺
cc.assetManager.loadBundle(wx.env.USER_DATA_PATH + '/pathToBundle/bundleName', (err, bundle) => {
    // ...
});

其它注意項:

  • 加載Bundle僅僅只是加載了Bundle的配置和腳本而已,Bundle中的其它資源還須要另外加載
  • 目前原生的Bundle並不支持zip打包,遠程包下載方式爲逐文件下載,好處是操做簡單,更新方便,壞處是io多,流量消耗大
  • 不一樣Bundle下的腳本文件不要重名
  • 一個Bundle A依賴另外一個Bundle B,若是B沒有被加載,加載A時並不會自動加載B,而是在加載A中依賴B的那個資源時報錯

3. 新資源框架剖析

v2.4重構後的新框架代碼更加簡潔清晰,咱們能夠先從宏觀角度瞭解一下整個資源框架,資源管線是整個框架最核心的部分,它規範了整個資源加載的流程,並支持對管線進行自定義。

  • 公共文件
    • helper.js 定義了一堆公共函數,如decodeUuid、getUuidFromURL、getUrlWithUuid等等
    • utilities.js 定義了一堆公共函數,如getDepends、forEach、parseLoadResArgs等等
    • deserialize.js 定義了deserialize方法,將json對象反序列化爲Asset對象,並設置其__depends__屬性
    • depend-util.js 控制資源的依賴列表,每一個資源的全部依賴都放在_depends成員變量中
    • cache.js 通用緩存類,封裝了一個簡易的鍵值對容器
    • shared.js 定義了一些全局對象,主要是Cache和Pipeline對象,如加載好的assets、下載完的files以及bundles等
  • Bundle部分
    • config.js bundle的配置對象,負責解析bundle的config文件
    • bundle.js bundle類,封裝了config以及加載卸載bundle內資源的相關接口
    • builtins.js 內建bundle資源的封裝,能夠經過 cc.assetManager.builtins 訪問
  • 管線部分
    • CCAssetManager.js 管理管線,提供統一的加載卸載接口
    • 管線框架
      • pipeline.js 實現了管線的管道組合以及流轉等基本功能
      • task.js 定義了一個任務的基本屬性,並提供了簡單的任務池功能
      • request-item.js 定義了一個資源下載項的基本屬性,一個任務可能會生成多個下載項
    • 預處理管線
      • urlTransformer.js parse將請求參數轉換成RequestItem對象(並查詢相關的資源配置),combine負責轉換真正的url
      • preprocess.js 過濾出須要進行url轉換的資源,並調用transformPipeline
    • 下載管線
      • download-dom-audio.js 提供下載音效的方法,使用audio標籤進行下載
      • download-dom-image.js 提供下載圖片的方法,使用Image標籤進行下載
      • download-file.js 提供下載文件的方法,使用XMLHttpRequest進行下載
      • download-script.js 提供下載腳本的方法,使用script標籤進行下載
      • downloader.js 支持下載全部格式的下載器,支持併發控制、失敗重試、
    • 解析管線
      • factory.js 建立Bundle、Asset、Texture2D等對象的工廠
      • fetch.js 調用packManager下載資源,並解析依賴
      • parser.js 對下載完成的文件進行解析
  • 其它
    • releaseManager.js 提供資源釋放接口、負責釋放依賴資源以及場景切換時的資源釋放
    • cache-manager.d.ts 在非WEB平臺上,用於管理全部從服務器上下載下來的緩存
    • pack-manager.js 處理打包資源,包括拆包,加載,緩存等等

3.1 加載管線

creator使用管線(pipeline)來處理整個資源加載的流程,這樣的好處是解耦了資源處理的流程,將每個步驟獨立成一個單獨的管道,管道能夠很方便地進行復用和組合,而且方便了咱們自定義整個加載流程,咱們能夠建立一些本身的管道,加入到管線中,好比資源加密。

AssetManager內置了3條管線,普通的加載管線、預加載、以及資源路徑轉換管線,最後這條管線是爲前面兩條管線服務的。

// 正常加載
    this.pipeline = pipeline.append(preprocess).append(load);
    // 預加載
    this.fetchPipeline = fetchPipeline.append(preprocess).append(fetch);
    // 轉換資源路徑
    this.transformPipeline = transformPipeline.append(parse).append(combine);

3.1.1 啓動加載管線【加載接口】

接下來咱們看一下一個普通的資源是如何加載的,好比最簡單的cc.resource.load,在bundle.load方法中,調用了cc.assetManager.loadAny,在loadAny方法中,建立了一個新的任務,並調用正常加載管線pipeline的async方法執行任務。

注意要加載的資源路徑,被放到了task.input中、options是一個對象,對象包含了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實際上只建立了一個子任務,而後交由transformPipeline執行。對於加載一個普通的資源,子任務的input和options與父任務相同。

let subTask = Task.create({input: task.input, options: subOptions});
    task.output = task.source = transformPipeline.sync(subTask);

3.1.2 transformPipeline管線【準備階段】

transformPipeline由parse和combine兩個管線組成,parse的職責是爲每一個要加載的資源生成RequestItem對象並初始化其資源信息(AssetInfo、uuid、config等):

  • 先將input轉換成數組進行遍歷,若是是批量加載資源,每一個加載項都會生成RequestItem
  • 若是輸入的item是object,則先將options拷貝到item身上(實際上每一個item都會是object,若是是string的話,第一步就先轉換成object了)
    • 對於UUID類型的item,先檢查bundle,並從bundle中提取AssetInfo,對於redirect類型的資源,則從其依賴的bundle中獲取AssetInfo,找不到bundle就報錯
    • PATH類型和SCENE類型與UUID類型的處理基本相似,都是要拿到資源的詳細信息
    • DIR類型會從bundle中取出指定路徑的信息,而後批量追加到input尾部(額外生成加載項)
    • URL類型是遠程資源類型,無需特殊處理
function parse (task) {
    // 將input轉換成數組
    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') {
            // 先建立object
            item = Object.create(null);
            item[options.__requestType__ || RequestType.UUID] = input[i];
        }
        if (typeof item === 'object') {
            // local options will overlap glabal options
            // 將options的屬性複製到item身上,addon會複製options上有,而item沒有的屬性
            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中取出該資源的詳細信息
                    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: 
                        // 解包後動態添加到input列表尾部,後續的循環會自動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類型的資源根據路徑和type取出該資源的詳細信息
                        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:
                        // 場景類型,從bundle中的config調用getSceneInfo取出該場景的詳細信息
                        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對象中查詢的,bundle的信息則是從bundle自帶的config.json文件中初始化的,在打包bundle的時候,會將bundle中的資源信息寫入config.json中。

通過parse方法處理後,咱們會獲得一系列RequestItem,而且不少RequestItem都自帶了AssetInfo和uuid等信息,combine方法會爲每一個RequestItem構建出真正的加載路徑,這個加載路徑最終會轉換到item.url中。

function combine (task) {
    var input = task.output = task.input;
    for (var i = 0; i < input.length; i++) {
        var item = input[i];
        // 若是item已經包含了url,則跳過,直接使用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方法作的事情很簡單,基本只是建立了新的任務,在loadOneAssetPipeline中執行每一個子任務

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) {
        // 對每一個input項都建立一個子任務,並交由loadOneAssetPipeline執行
        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();
            }
        });
        // 執行子任務,loadOneAssetPipeline有fetch和parse組成
        loadOneAssetPipeline.async(subTask);
    }, function () {
        // 每一個input執行完成後,最後執行該函數
        options.__exclude__ = null;
        if (task.isFinish) {
            clear(task, true);
            return task.dispatch('error');
        }
        gatherAsset(task);
        clear(task, true);
        done();
    });
}

loadOneAssetPipeline如其函數名所示,就是加載一個資源的管線,它分爲2步,fetch和parse:

  • fetch方法用於下載資源文件,由packManager負責下載的實現,fetch會將下載完的文件數據放到item.file中
  • parse方法用於將加載完的資源文件轉換成咱們可用的資源對象
    • 對於原生資源,調用parser.parse進行解析,該方法會根據資源類型調用不一樣的解析方法
      • import資源調用parseImport方法,根據json數據反序列化出Asset對象,並放到assets中
      • 圖片資源會調用parseImage、parsePVRTex或parsePKMTex方法解析圖像格式(但不會建立Texture對象)
      • 音效資源調用parseAudio方法進行解析
      • plist資源調用parsePlist方法進行解析
    • 對於其它資源
      • 若是uuid在task.options.__exclude__中,則標記爲完成,並添加引用計數
      • 不然,根據一些複雜的條件來決定是否加載資源的依賴
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裏面已經加載了這個資源,則直接完成
        if (file || (!reload && !isNative && assets.has(uuid))) return done();
        // 下載文件,這是一個異步的過程,文件下載完會被放到item.file中,並執行done驅動管線
        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);
        });
    },
    // 將資源文件轉換成資源對象的過程
    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) {
            // 對於原生資源,調用parser.parse進行處理,將處理完的資源放到item.content中,並結束流程
            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__中,直接結束
            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);
                    // 開啓了options.__asyncLoadAssets__,或asset.__asyncLoadAssets__爲false,直接結束,不加載依賴
                    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中沒有,則進行解析,並加載依賴
                    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來完成下載的工做,當要下載一個文件時,有2個問題須要考慮:

  • 該文件是否被打包了,好比因爲勾選了內聯全部SpriteFrame,致使SpriteFrame的json文件被合併到prefab中
  • 當前平臺是原平生臺仍是web平臺,對於一些本地資源,原平生臺須要從磁盤讀取
// packManager.load的實現
    load (item, options, onComplete) {
        // 若是資源沒有被打包,則直接調用downloader.download下載(download內部也有已下載和加載中的判斷)
        if (item.isNative || !item.info || !item.info.packs) return downloader.download(item.id, item.url, item.ext, item.options, onComplete);
        // 若是文件已經下載過了,則直接返回
        if (files.has(item.id)) return onComplete(null, files.get(item.id));

        var packs = item.info.packs;
        // 若是pack已經在加載中,則將回調添加到_loading隊列,等加載完成後觸發回調
        var pack = packs.find(isLoading);
        if (pack) return _loading.get(pack.uuid).push({ onComplete, id: item.id });

        // 下載一個新的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,內部實現包含2種解包,一種針對prefab、圖集等json數組的分割解包,另外一種針對Texture2D的content進行解包
            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平臺的下載

web平臺的download實現以下:

  • 用一個downloaders數組來管理各類資源類型對應的下載方式
  • 使用files緩存來避免重複下載
  • 使用_downloading隊列來處理併發下載同一個資源時的回調,並保證時序
  • 支持了下載的優先級、重試等邏輯
download (id, url, type, options, onComplete) {
        // 取出downloaders中對應類型的下載回調
        let func = downloaders[type] || downloaders['default'];
        let self = this;
        // 避免重複下載
        let file, downloadCallbacks;
        if (file = files.get(id)) {
            onComplete(null, file);
        }
        // 若是在下載中,添加到隊列
        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 {
            // 進行下載,並設置好下載失敗的重試
            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隊列中移除,並執行callbacks回調
            // 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是一個map,映射了各類資源類型對應的下載方法,在web平臺主要包含如下幾類下載方法:

  • 圖片類 downloadImage
    • downloadDomImage 使用Html的Image元素,指定其src屬性來下載
    • downloadBlob 以文件下載的方式下載圖片
  • 文件類,這裏能夠分爲二進制文件、json文件和文本文件
    • downloadArrayBuffer 指定arraybuffer類型調用downloadFile,用於skel、bin、pvr等文件下載
    • downloadText 指定text類型調用downloadFile,用於atlas、tmx、xml、vsh等文件下載
    • downloadJson 指定json類型調用downloadFile,並在下載完後解析json,用於plist、json等文件下載
  • 字體類 loadFont 構建css樣式,指定url下載
  • 聲音類 downloadAudio
    • downloadDomAudio 建立Html的audio元素,指定其src屬性來下載
    • downloadBlob 以文件下載的方式下載音效
  • 視頻類 downloadVideo web端直接返回了
  • 腳本 downloadScript 建立Html的script元素,指定其src屬性來下載並執行
  • Bundle downloadBundle 同時下載了Bundle的json和腳本

downloadFile使用了XMLHttpRequest來下載文件,具體實現以下:

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 原平生臺下載

原平生臺的引擎相關文件能夠在引擎目錄的resources/builtin/jsb-adapter/engine目錄下,資源加載相關的實如今jsb-loader.js文件中,這裏的downloader從新註冊了回調函數。

downloader.register({
    // JS
    '.js' : downloadScript,
    '.jsc' : downloadScript,

    // Images
    '.png' : downloadAsset,
    '.jpg' : downloadAsset,
    ...
});

在原平生臺下,downloadAsset等方法都會調用download來進行資源的下載,在資源下載以前會調用transformUrl對url進行檢測,主要判斷該資源是網絡資源仍是本地資源,若是是網絡資源,是否已經下載過了。只有沒下載過的網絡資源,才須要進行下載。不須要下載的在文件解析的地方會直接讀文件。

// func傳入的是下載完成以後的處理,好比腳本下載完成後須要執行,此時會調用window.require
// 若是說要下載的是json資源之類的,傳入的func是doNothing,也就是直接調用onComplete方法
function download (url, func, options, onFileProgress, onComplete) {
    var result = transformUrl(url, options);
    // 若是是本地文件,直接指向func
    if (result.inLocal) {
        func(result.url, options, onComplete);
    }
    // 若是在緩存中,更新資源的最後使用時間(lru)
    else if (result.inCache) {
        cacheManager.updateLastTime(url)
        func(result.url, options, function (err, data) {
            if (err) {
                cacheManager.removeCache(url);
            }
            onComplete(err, data);
        });
    }
    else {
        // 未下載的網絡資源,調用downloadFile進行下載
        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;
    // 經過正則匹配是否是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會調用原平生臺的jsb_downloader來下載資源,並保存到本地磁盤中

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中,資源會通過fetch和parse兩個管線進行處理,fetch負責下載而parse負責解析資源,並實例化資源對象。在parse方法中調用了parser.parse將文件內容傳入,解析成對應的Asset對象,並返回。

3.3.1 Web平臺解析

Web平臺下的parser.parse主要作的是對解析中的文件的管理,爲解析中、解析完的文件維護一個列表,避免重複解析。同時維護瞭解析完成後的回調列表,而真正的解析方法在parsers數組中。

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方法中,反序列化方法會將資源的依賴放到asset.__depends__中,結構爲數組,數組中每一個對象包含3個字段,資源id uuid、owner 對象、prop 屬性。好比一個Prefab資源,下面有2個節點,都引用了同一個資源,depends列表須要爲這兩個節點對象分別記錄一條依賴信息 [{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
    };
    
    // 圖片並不會解析成Asset對象,而是解析成對應的圖片對象
    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對象的解析,經過deserialize實現,大體流程是解析json而後找到對應的class,並調用對應class的_deserialize方法拷貝數據、初始化變量,並將依賴資源放到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 原平生臺解析

在原平生臺下,jsb-loader.js中從新註冊了各類資源的解析方法:

parser.register({
    '.png' : downloader.downloadDomImage,
    '.binary' : parseArrayBuffer,
    '.txt' : parseText,
    '.plist' : parsePlist,
    '.font' : loadFont,
    '.ExportJson' : parseJson,
    ...
});

圖片的解析方法居然是downloader.downloadDomImage?跟蹤原平生臺調試了一下,確實是調用的這個方法,建立了Image對象並指定src來加載圖片,這種方式加載本地磁盤的圖片也是能夠的,但紋理對象又是如何建立的呢?經過Texture2D對應的json文件,creator在加載真正的原生紋理以前,就已經建立好了Texture2D這個Asset對象,而在加載完原生圖片資源後,會將Image對象設置爲Texture2D對象的_nativeAsset,在這個屬性的set方法中,會調用initWithData或initWithElement,這裏才真正使用紋理數據建立了用於渲染的紋理對象。

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
        },

而對於parseJson、parseText、parseArrayBuffer等實現,這裏只是簡單地調用了文件系統讀取文件而已。像一些拿到文件內容以後,須要進一步解析才能使用的資源呢?好比模型、骨骼等資源依賴二進制的模型數據,這些數據的解析在哪裏呢?沒錯,跟上面的Texture2D同樣,都是放在對應的Asset資源自己,有些在_nativeAsset字段的setter回調中初始化,而有些會在真正使用這個資源時才惰性地進行初始化。

// 在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方法進行解析,由於這些資源對應的類型是import,原平生臺下並無覆蓋這種類型對應的parse函數,而這些資源會直接反序列化成可用的Asset對象。

3.4 依賴加載

creator將資源分爲兩大類,普通資源和原生資源,普通資源包括cc.Asset及其子類,如cc.SpriteFrame、cc.Texture2D、cc.Prefab等等。原生資源包括各類格式的紋理、音樂、字體等文件,在遊戲中咱們沒法直接使用這些原生資源,而是須要讓creator將他們轉換成對應的cc.Asset對象以後才能使用。

在creator中,一個Prefab可能會依賴不少資源,這些依賴也能夠分爲普通依賴和原生資源依賴,creator的cc.Asset提供了_parseDepsFromJson_parseNativeDepFromJson方法來檢查資源的依賴。loadDepends經過getDepends方法蒐集了資源的依賴。

loadDepends建立了一個子任務來負責依賴資源的加載,並調用pipeline執行加載,實際上不管有無依賴須要加載,都會執行這段邏輯,加載完成後執行如下重要邏輯:

  • 初始化assset:在依賴加載完成後,將依賴的資源賦值到asset對應的屬性後調用asset.onLoad
  • 將資源對應的files和parsed緩存移除,並緩存資源到assets中(若是是場景的話,不會緩存)
  • 執行repeatItem.callbacks列表中的回調(在loadDepends的開頭構造,默認記錄傳入的done方法)
// 加載指定asset的依賴項
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 = [];
    // 增長引用計數來避免加載依賴的過程當中資源被釋放,調用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) {
            // 在全部依賴項加載完成以後回調
            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];
                // 構造一個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);
                }

                // 調用setProperties將對應的依賴資源設置到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();
            }
            
            // 這個repeatItem可能有不少個地方都加載了它,要通知全部回調加載完成
            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是一個控制依賴列表的單例,經過傳入uuid和asset對象來解析該對象的依賴資源列表,返回的依賴資源列表可能包含如下4個字段:

  • deps 依賴的Asset資源
  • nativeDep 依賴的原生資源
  • preventPreloadNativeObject 禁止預加載原生對象,這個值默認是false
  • preventDeferredLoadDependents 禁止延遲加載依賴,默認爲false,對於骨骼動畫、TiledMap等資源爲true
  • parsedFromExistAsset 是否直接從asset.__depends__中取出

dependUtil還維護了_depends緩存來避免依賴的重複查詢,這個緩存會在首次查詢某資源依賴時添加,當該資源被釋放時移除

// 根據json信息獲取其資源依賴列表,實際上json信息就是asset對象
    parse (uuid, json) {
        var out = null;
        // 若是是場景或者Prefab,data會是一個數組,scene or prefab
        if (Array.isArray(json)) {
            // 若是已經解析過了,在_depends中有依賴列表,則直接返回
            if (this._depends.has(uuid)) return this._depends.get(uuid)
            out = {
                // 對於Prefab或場景,直接使用_parseDepsFromJson方法返回
                deps: cc.Asset._parseDepsFromJson(json),
                asyncLoadAssets: json[0].asyncLoadAssets
            };
        }
        // 若是包含__type__,獲取其構造函數,並從json中查找依賴資源 get deps from json
        // 實際測試,預加載的資源會走下面這個分支,預加載的資源並無把json反序列化成Asset對象
        else if (json.__type__) {
            if (this._depends.has(uuid)) return this._depends.get(uuid);
            var ctor = js._getClassById(json.__type__);
            // 部分資源重寫了_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 
        // 若是沒有__type__字段,則沒法找到它對應的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會返回相似這樣的對象 {__isNative__: true, uuid: this._uuid, ext: this._native}
                out.nativeDep = asset._nativeDep;
            }
        }
        // 第一次找到依賴,直接放到_depends列表中,cache dependency list
        this._depends.add(uuid, out);
        return out;
    }

CCAsset默認的_parseDepsFromJson_parseNativeDepFromJson實現以下,_parseDepsFromJson經過調用parseDependRecursively遞歸json,將json對象及其子對象的全部__uuid__所有找到放到depends數組中。Texture2D、TTFFont、AudioClip的實現爲直接返回空數組,而SpriteFrame的實現爲返回cc.assetManager.utils.decodeUuid(json.content.texture),這個字段記錄了SpriteFrame對應紋理的uuid。

_parseNativeDepFromJson在改asset的_native有值的狀況下,會返回{ __isNative__: true, ext: json._native}。實際上大部分的native資源走的是_nativeDep,這個屬性的get方法會返回一個包含相似這樣的對象{__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 資源釋放

這一小節重點介紹在Creator中釋放資源的三種方式以及其背後的實現,最後介紹在項目中如何排查資源泄露的狀況。

3.5.1 Creator的資源釋放

Creator支持如下3種資源釋放的方式:

釋放方式 釋放效果
勾選:場景->屬性檢查器->自動釋放資源 在場景切換後,自動釋放新場景不使用的資源
引用計數釋放res.decRef 使用addRef和decRef維護引用計數,在decRef後引用計數爲0時自動釋放
手動釋放cc.assetManager.releaseAsset(texture); 手動釋放資源,強制釋放

3.5.2 場景自動釋放

當一個新場景運行的時候會執行Director.runSceneImmediate方法,這裏調用了_autoRelease來實現老場景資源的自動釋放(若是老場景勾選了自動釋放資源)。

runSceneImmediate: function (scene, onBeforeLoadScene, onLaunched) {
        // 省略代碼...
        var oldScene = this._scene;
        if (!CC_EDITOR) {
            // 自動釋放資源
            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的實現很是簡潔乾脆,將持久節點的引用從老場景遷移到新場景,而後直接調用資源的decRef減小引用計數,而是否釋放老場景引用的資源,則取決於老場景是否設置了autoReleaseAssets。

// do auto release
    _autoRelease (oldScene, newScene, persistNodes) { 
        // 全部持久節點依賴的資源自動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);
            }
        }

        // 釋放老場景的依賴
        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 引用計數和手動釋放資源

剩下兩種釋放資源的方式,本質上都是調用releaseManager.tryRelease來實現資源釋放,區別在於decRef是根據引用計數和autoRelease來決定是否調用tryRelease,而releaseAsset是強制釋放。資源釋放的完整流程大體以下圖所示:

sPlF8f.png

// CCAsset.js 減小引用
    decRef (autoRelease) {
        this._ref--;
        autoRelease !== false && cc.assetManager._releaseManager.tryRelease(this);
        return this;
    }

    // CCAssetManager.js 手動釋放資源
    releaseAsset (asset) {
        releaseManager.tryRelease(asset, true);
    },

tryRelease支持延遲釋放和強制釋放2種模式,當傳入force參數爲true時直接進入釋放流程,不然creator會將資源放入待釋放的列表中,並在EVENT_AFTER_DRAW事件中執行freeAssets方法真正清理資源。不論何種方式,資源會傳入到_free方法處理,這個方法作了如下幾件事情。

  • 從_toDelete中移除
  • 在非force釋放時,須要檢查是否還有其它引用,若是是則返回
  • 從assets緩存中移除
  • 自動釋放依賴資源
  • 調用資源的destroy方法銷燬資源
  • 從dependUtil中移除資源的依賴記錄

checkCircularReference返回值若是大於0,表示資源還有被其它地方引用,其它地方指全部咱們addRef的地方,該方法會先記錄asset當前的refCount,而後消除掉資源和依賴資源中對asset的引用,這至關於資源A內部掛載了組件B和C,它們都引用了資源A,此時資源A的引用計數爲2,而組件B和C實際上是要跟着A釋放的,而A被B和C引用着,計數就不爲0沒法釋放,因此checkCircularReference先排除了內部的引用。若是資源的refCount減去了內部的引用次數還大於1,說明有其它地方還引用着它,不能釋放。

tryRelease (asset, force) {
        if (!(asset instanceof cc.Asset)) return;
        if (force) {
            releaseManager._free(asset, force);
        }
        else {
            _toDelete.add(asset._uuid, asset);
            // 在下次Director繪製完成以後,執行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) {
                // 檢查資源內部的循環引用
                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);
    },
    
    // 釋放_toDelete中的資源並清空
    function freeAssets () {
        eventListener = false;
        _toDelete.forEach(function (asset) {
            releaseManager._free(asset);
        });
        _toDelete.clear();
    }

asset.destroy作了什麼?資源對象是如何被釋放掉的?像紋理、聲音這樣的資源又是如何被釋放掉的呢?Asset對象自己並無destroy方法,而是Asset對象所繼承的CCObject對象實現了destroy,這裏的實現只是將對象放到了一個待釋放的數組中,並打上ToDestroy的標記。Director每一幀都會調用deferredDestroy來執行_destroyImmediate進行資源釋放,這個方法會對對象的Destroyed標記進行判斷和操做、調用_onPreDestroy方法執行回調、以及_destruct方法進行析構。

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每一幀都會調用這個方法
function deferredDestroy () {
    var deleteCount = objectsToDestroy.length;
    for (var i = 0; i < deleteCount; ++i) {
        var obj = objectsToDestroy[i];
        if (!(obj._objFlags & Destroyed)) {
            obj._destroyImmediate();
        }
    }
    // 當咱們在a.onDestroy中調用b.destroy,objectsToDestroy數組的大小會變化,咱們只銷毀在此次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;
    }
    // 執行回調
    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作的事情就是將對象的屬性清空,好比將object類型的屬性置爲null,將string類型的屬性置爲'',compileDestruct方法會返回一個該類的析構函數,compileDestruct先收集了普通object和cc.Class這兩種類型下的全部屬性,並根據類型構建了一個propsToReset用來清空屬性,支持JIT的狀況下會根據要清空的屬性生成一個相似這樣的函數返回function(o) {o.a='';o.b=null;o.['c']=undefined...},而非JIT狀況下會返回一個根據propsToReset遍歷處理的函數,前者佔用更多內存,但效率更高。

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又作了什麼呢?主要是將各類事件、定時器進行註銷,對子節點、組件等進行刪除,詳情能夠看下面這段代碼。

// Node的_onPreDestroy
    _onPreDestroy () {
        // 調用_onPreDestroyBase方法,實際是調用BaseNode.prototype._onPreDestroy,這個方法下面介紹
        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();

        // 移除全部觸摸和鼠標事件監聽
        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;
        }

        // 回收到對象池中
        this._backDataIntoPool();

        if (this._reorderChildDirty) {
            cc.director.__fastOff(cc.Director.EVENT_AFTER_UPDATE, this.sortAllChildren, this);
        }

        if (!destroyByParent) {
            if (CC_EDITOR) {
                // 確保編輯模式下的,節點的被刪除後能夠經過ctrl+z撤銷(從新添加到原來的父節點)
                this._parent = null;
            }
        }
    },
    
    // BaseNode的_onPreDestroy
    _onPreDestroy () {
        var i, len;

        // 加上Destroying標記
        this._objFlags |= Destroying;
        var parent = this._parent;
        
        // 根據檢測父節點的標記判斷是否是由父節點的destroy發起的釋放
        var destroyByParent = parent && (parent._objFlags & Destroying);
        if (!destroyByParent && (CC_EDITOR || CC_TEST)) {
            // 從編輯器中移除
            this._registerIfAttached(false);
        }

        // 把全部子節點進行釋放,它們的_onPreDestroy也會被執行
        var children = this._children;
        for (i = 0, len = children.length; i < len; ++i) {
            children[i]._destroyImmediate();
        }

        // 把全部的組件進行釋放,它們的_onPreDestroy也會被執行
        for (i = 0, len = this._components.length; i < len; ++i) {
            var component = this._components[i];
            component._destroyImmediate();
        }

        // 註銷事件監聽,好比otherNode.on(type, callback, thisNode) 註冊了事件
        // thisNode被釋放時,須要註銷otherNode身上的監聽,避免事件回調到已銷燬的對象上
        var eventTargets = this.__eventTargets;
        for (i = 0, len = eventTargets.length; i < len; ++i) {
            var target = eventTargets[i];
            target && target.targetOff(this);
        }
        eventTargets.length = 0;

        // 若是本身是常駐節點,則從常駐節點列表中移除
        if (this._persistNode) {
            cc.game.removePersistRootNode(this);
        }

        // 若是是本身釋放的本身,而不是從父節點釋放的,要通知父節點,把這個失效的子節點移除掉
        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();

        // 移除全部的監聽
        var eventTargets = this.__eventTargets;
        for (var i = eventTargets.length - 1; i >= 0; --i) {
            var target = eventTargets[i];
            target && target.targetOff(this);
        }
        eventTargets.length = 0;

        // 編輯器模式下中止監控
        if (CC_EDITOR && !CC_TEST) {
            _Scene.AssetsWatcher.stop(this);
        }

        // destroyComp的實現爲調用組件的onDestroy回調,各個組件會在回調中銷燬自身的資源
        // 好比RigidBody3D組件會調用body的destroy方法,而Animation組件會調用stop方法
        cc.director._nodeActivator.destroyComp(this);

        // 將組件從節點身上移除
        this.node._removeComponent(this);
    },

3.5.4 資源釋放的問題

最後咱們來聊一聊資源釋放的問題與定位,在加入引用計數後,最多見的問題仍是沒有正確增減引用計數致使的內存泄露(循環引用、少調用了decRef或多調用了addRef),以及正在使用的資源被釋放的問題(和內存泄露相反,資源被提早釋放了)。

從目前的代碼來看,若是正確使用了引用計數,新的資源底層是能夠避免內存泄露等問題的

這種問題怎麼解決呢?首先是定位出哪些資源出了問題,若是是被提早釋放,咱們能夠直接定位到這個資源,若是是內存泄露,當咱們發現問題時程序每每已經佔用了大量的內存,這種狀況下能夠切換到一個空場景,並清理資源,把資源清理完後,能夠檢查assets中殘留的資源是否有未被釋放的資源。

要了解資源爲何會泄露,能夠經過跟蹤addRef和decRef的調用獲得,下面提供了一個示例方法,用於跟蹤某資源的addRef和decRef調用,而後調用資源的dump方法打印出全部調用的堆棧:

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