<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Cocos Creator 新資源管理系統(tǒng)剖析

          共 23131字,需瀏覽 47分鐘

           ·

          2021-01-18 21:42

          v2.4 開始,Cocos Creator 使用 AssetBundle 完全重構(gòu)了資源底層,提供了更加靈活強(qiáng)大的資源管理方式,也解決了之前版本資源管理的痛點(diǎn)(資源依賴與引用),本文將帶你深入了解 Creator 的新資源底層。

          目錄

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

          1.資源與構(gòu)建

          1.1 creator 資源文件基礎(chǔ)

          在了解引擎如何解析、加載資源之前,我們先來了解一下這些資源文件(圖片、Prefab、動(dòng)畫等)的規(guī)則。

          在 creator 項(xiàng)目目錄下有幾個(gè)與資源相關(guān)的目錄:

          • assets 所有資源的總目錄,對(duì)應(yīng) creator 編輯器的資源管理器
          • library 本地資源庫,預(yù)覽項(xiàng)目時(shí)使用的目錄
          • build 構(gòu)建后的項(xiàng)目默認(rèn)目錄

          assets 目錄下,creator 會(huì)為每個(gè)資源文件和目錄生成一個(gè)同名的.meta文件,meta 文件是一個(gè) json 文件,記錄了資源的版本、uuid 以及各種自定義的信息(在編輯器的屬性檢查器中設(shè)置),比如 prefab 的 meta 文件,就記錄了我們可以在編輯器修改的 optimizationPolicyasyncLoadAssets 等屬性。

          {
            "ver""1.2.7",
            "uuid""a8accd2e-6622-4c31-8a1e-4db5f2b568b5",
            "optimizationPolicy""AUTO",     // prefab創(chuàng)建優(yōu)化策略
            "asyncLoadAssets"false,         // 是否延遲加載
            "readonly"false,
            "subMetas": {}
          }

          library 目錄下的 imports 目錄,資源文件名會(huì)被轉(zhuǎn)換成 uuid,并取 uuid 前2個(gè)字符進(jìn)行目錄分組存放,creator 會(huì)將所有資源的 uuidassets 目錄的映射關(guān)系,以及資源和 meta 的最后更新時(shí)間戳放到一個(gè)名為 uuid-to-mtime.json 的文件中,如下所示:

          {
            "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 目錄并沒有為目錄生成任何東西。

          1.2 資源構(gòu)建

          在項(xiàng)目構(gòu)建之后,資源會(huì)從 library 目錄下移動(dòng)到構(gòu)建輸出的 build 目錄中,基本只會(huì)導(dǎo)出參與構(gòu)建的場(chǎng)景和 resources 目錄下的資源,及其引用到的資源。腳本資源會(huì)由多個(gè) js 腳本合并為一個(gè) js,各種 json 文件也會(huì)按照特定的規(guī)則進(jìn)行打包。

          我們可以在 Bundle 的配置界面和項(xiàng)目的構(gòu)建界面,為 Bundle 和項(xiàng)目設(shè)置:

          1.2.1 圖片、圖集、自動(dòng)圖集

          • https://docs.cocos.com/creator/manual/zh/asset-workflow/sprite.html
          • https://docs.cocos.com/creator/manual/zh/asset-workflow/atlas.html
          • https://docs.cocos.com/creator/manual/zh/asset-workflow/auto-atlas.html

          導(dǎo)入編輯器的每張圖片都會(huì)對(duì)應(yīng)生成一個(gè) json 文件,用于描述 Texture 的信息。

          如下所示,默認(rèn)情況下項(xiàng)目中所有的 Texture2Djson 文件會(huì)被壓縮成一個(gè),如果選擇無壓縮,則每個(gè)圖片都會(huì)生成一個(gè) Texture2Djson 文件。

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

          如果將紋理的 Type 屬性設(shè)置為 Sprite,Creator 還會(huì)自動(dòng)生成了 SpriteFrame 類型的 json 文件。

          圖集資源除了圖片外,還對(duì)應(yīng)一個(gè)圖集 json,這個(gè) json 包含了 cc.SpriteAtlas 信息,以及每個(gè)碎圖的 SpriteFrame 信息。

          自動(dòng)圖集在默認(rèn)情況下只包含了 cc.SpriteAtlas 信息,在勾選內(nèi)聯(lián)所有 SpriteFrame 的情況下,會(huì)合并所有 SpriteFrame。

          1.2.2 Prefab與場(chǎng)景

          • https://docs.cocos.com/creator/manual/zh/asset-workflow/prefab.html
          • https://docs.cocos.com/creator/manual/zh/asset-workflow/scene-managing.html

          場(chǎng)景資源與 Prefab 資源非常類似,都是一個(gè)描述了所有節(jié)點(diǎn)、組件等信息的 json文件,在勾選內(nèi)聯(lián)所有 SpriteFrame 的情況下,Prefab 引用到的 SpriteFrame 會(huì)被合并到 prefab 所在的 json 文件中。

          如果一個(gè) SpriteFrame 被多個(gè) prefab 引用,那么每個(gè) prefabjson 文件都會(huì)包含該 SpriteFrame 的信息。

          而在沒有勾選內(nèi)聯(lián)所有 SpriteFrame 的情況下,SpriteFrame 會(huì)是單獨(dú)的 json文件。

          1.2.3 資源文件合并規(guī)則

          當(dāng) Creator 將多個(gè)資源合并到一個(gè) json 文件中,我們可以在 config.json 中的 packs 字段找到被打包的資源信息,一個(gè)資源有可能被重復(fù)打包到多個(gè) json 中。

          下面舉一個(gè)例子,展示在不同的選項(xiàng)下,creator 的構(gòu)建規(guī)則:

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

          下面是按不同規(guī)則構(gòu)建后的文件,可以看到,無壓縮的情況下生成的文件數(shù)量是最多的,不內(nèi)聯(lián)的文件會(huì)比內(nèi)聯(lián)多,但內(nèi)聯(lián)可能會(huì)導(dǎo)致同一個(gè)文件被重復(fù)包含,比如 ef 這兩個(gè) Prefab 都引用了同一個(gè)圖片,這個(gè)圖片的 SpriteFrame.json 會(huì)被重復(fù)包含,合并成一個(gè) json 則只會(huì)生成一個(gè)文件。

          默認(rèn)選項(xiàng)在絕大多數(shù)情況下都是一個(gè)不錯(cuò)的選擇。

          如果是 web 平臺(tái),建議勾選內(nèi)聯(lián)所有 SpriteFrame 這可以減少網(wǎng)絡(luò) io,提高性能。

          原生平臺(tái)則不建議勾選,這可能會(huì)增加包體大小以及熱更時(shí)要下載的內(nèi)容。

          對(duì)于一些緊湊的 Bundle(比如加載該 Bundle 就需要用到里面所有的資源),我們可以配置為合并所有的 json。

          2. 理解與使用 Asset Bundle

          2.1 創(chuàng)建 Bundle

          Asset Bundle 是 creator 2.4 之后的資源管理方案,簡(jiǎn)單地說,就是通過目錄來對(duì)資源進(jìn)行規(guī)劃,按照項(xiàng)目的需求將各種資源放到不同的目錄下,并將目錄配置成 Asset Bundle。能夠起到以下作用:

          • 加快游戲啟動(dòng)時(shí)間
          • 減小首包體積
          • 跨項(xiàng)目復(fù)用資源
          • 方便實(shí)現(xiàn)子游戲
          • 以Bundle為單位的熱更新

          Asset Bundle 的創(chuàng)建非常簡(jiǎn)單,只要在目錄的屬性檢查器中勾選配置為 bundle 即可,其中的選項(xiàng)官方文檔都有比較詳細(xì)的介紹。

          其中關(guān)于壓縮的理解,文檔并沒有詳細(xì)的描述,這里的壓縮指的并不是 zip 之類的壓縮,而是通過 packAssets 的方式,把多個(gè)資源的 json 文件合并到一個(gè),達(dá)到減少 io 的目的。

          在選項(xiàng)上打勾非常簡(jiǎn)單,真正的關(guān)鍵在于如何規(guī)劃 Bundle。規(guī)劃的原則在于減少包體、加速啟動(dòng)以及資源復(fù)用。根據(jù)游戲的模塊來規(guī)劃資源是比較不錯(cuò)的選擇,比如按子游戲、關(guān)卡副本、或者系統(tǒng)功能來規(guī)劃。

          Bundle 會(huì)自動(dòng)將文件夾下的資源,以及文件夾中引用到的其它文件夾下的資源打包(如果這些資源不是在其它 Bundle 中),如果我們按照模塊來規(guī)劃資源,很容易出現(xiàn)多個(gè) Bundle 共用了某個(gè)資源的情況。

          可以將公共資源提取到一個(gè) Bundle 中,或者設(shè)置某個(gè) Bundle 有較高的優(yōu)先級(jí),構(gòu)建 Bundle 的依賴關(guān)系,否則這些資源會(huì)同時(shí)放到多個(gè) Bundle 中(如果是本地 Bundle,這會(huì)導(dǎo)致包體變大)。

          2.2 使用 Bundle

          • 關(guān)于加載資源 https://docs.cocos.com/creator/manual/zh/scripting/load-assets.html
          • 關(guān)于釋放資源 https://docs.cocos.com/creator/manual/zh/asset-manager/release-manager.html

          Bundle 的使用也非常簡(jiǎn)單,如果是 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 或遠(yuǎn)程 Bundle 都可以用 Bundle 名加載),可以使用 cc.assetManager.loadBundle 來加載 Bundle,然后使用加載后的 Bundle 對(duì)象,來加載 Bundle 中的資源。

          對(duì)于原生平臺(tái),如果 Bundle 被配置為遠(yuǎn)程包,在構(gòu)建時(shí)需要在構(gòu)建發(fā)布面板中填寫資源服務(wù)器地址。

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

          原生或小游戲平臺(tái)下,我們還可以這樣使用 Bundle:

          如果要加載其它項(xiàng)目的遠(yuǎn)程 Bundle,則需要使用 url 的方式加載(其它項(xiàng)目指另一個(gè) cocos 工程)

          如果希望自己管理 Bundle 的下載和緩存,可以放到本地可寫路徑,并傳入路徑來加載這些 Bundle

          // 當(dāng)復(fù)用其他項(xiàng)目的 Asset Bundle 時(shí)
          cc.assetManager.loadBundle('https://othergame.com/remote/01_graphics', (err, bundle) => {
              bundle.load('xxx');
          });

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

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

          其它注意項(xiàng):

          • 加載  Bundle 僅僅只是加載了 Bundle 的配置和腳本而已,Bundle 中的其它資源還需要另外加載
          • 目前原生的 Bundle 并不支持 zip 打包,遠(yuǎn)程包下載方式為逐文件下載,好處是操作簡(jiǎn)單,更新方便,壞處是 io 多,流量消耗大
          • 不同 Bundle 下的腳本文件不要重名
          • 一個(gè) Bundle A 依賴另一個(gè) Bundle B,如果 B 沒有被加載,加載 A 時(shí)并不會(huì)自動(dòng)加載 B,而是在加載 A 中依賴 B 的那個(gè)資源時(shí)報(bào)錯(cuò)

          3. 新資源框架剖析

          v2.4重構(gòu)后的新框架代碼更加簡(jiǎn)潔清晰,我們可以先從宏觀角度了解一下整個(gè)資源框架,資源管線是整個(gè)框架最核心的部分,它規(guī)范了整個(gè)資源加載的流程,并支持對(duì)管線進(jìn)行自定義。

          公共文件:

          • helper.js 定義了一堆公共函數(shù),如 decodeUuid、getUuidFromURL、getUrlWithUuid 等等
          • utilities.js 定義了一堆公共函數(shù),如 getDependsforEach、parseLoadResArgs 等等
          • deserialize.js 定義了 deserialize 方法,將 json 對(duì)象反序列化為 Asset 對(duì)象,并設(shè)置其 __depends__ 屬性
          • depend-util.js 控制資源的依賴列表,每個(gè)資源的所有依賴都放在 _depends 成員變量中
          • cache.js 通用緩存類,封裝了一個(gè)簡(jiǎn)易的鍵值對(duì)容器
          • shared.js 定義了一些全局對(duì)象,主要是 CachePipeline 對(duì)象,如加載好的 assets、下載完的 files 以及 bundles

          Bundle 部分:

          • config.js bundle 的配置對(duì)象,負(fù)責(zé)解析 bundleconfig 文件
          • bundle.js bundle 類,封裝了 config 以及加載卸載 bundle 內(nèi)資源的相關(guān)接口
          • builtins.js 內(nèi)建 bundle 資源的封裝,可以通過 cc.assetManager.builtins 訪問

          管線部分:

          • CCAssetManager.js 管理管線,提供統(tǒng)一的加載卸載接口

          • 管線框架

            • pipeline.js 實(shí)現(xiàn)了管線的管道組合以及流轉(zhuǎn)等基本功能
            • task.js 定義了一個(gè)任務(wù)的基本屬性,并提供了簡(jiǎn)單的任務(wù)池功能
            • request-item.js 定義了一個(gè)資源下載項(xiàng)的基本屬性,一個(gè)任務(wù)可能會(huì)生成多個(gè)下載項(xiàng)
          • 預(yù)處理管線

            • urlTransformer.js parse 將請(qǐng)求參數(shù)轉(zhuǎn)換成 RequestItem 對(duì)象(并查詢相關(guān)的資源配置),combine  負(fù)責(zé)轉(zhuǎn)換真正的 url
            • preprocess.js 過濾出需要進(jìn)行 url 轉(zhuǎn)換的資源,并調(diào)用 transformPipeline
          • 下載管線

            • download-dom-audio.js  提供下載音效的方法,使用 audio 標(biāo)簽進(jìn)行下載
            • download-dom-image.js 提供下載圖片的方法,使用 Image 標(biāo)簽進(jìn)行下載
            • download-file.js 提供下載文件的方法,使用 XMLHttpRequest 進(jìn)行下載
            • download-script.js 提供下載腳本的方法,使用 script 標(biāo)簽進(jìn)行下載
            • downloader.js 支持下載所有格式的下載器,支持并發(fā)控制、失敗重試、
          • 解析管線

            • factory.js 創(chuàng)建 BundleAsset、Texture2D 等對(duì)象的工廠
            • fetch.js 調(diào)用 packManager 下載資源,并解析依賴
            • parser.js 對(duì)下載完成的文件進(jìn)行解析
          • 其它releaseManager.js 提供資源釋放接口、負(fù)責(zé)釋放依賴資源以及場(chǎng)景切換時(shí)的資源釋放cache-manager.d.ts 在非 WEB 平臺(tái)上,用于管理所有從服務(wù)器上下載下來的緩存pack-manager.js 處理打包資源,包括拆包,加載,緩存等等

          3.1 加載管線

          creator 使用管線(pipeline)來處理整個(gè)資源加載的流程,這樣的好處是解耦了資源處理的流程,將每一個(gè)步驟獨(dú)立成一個(gè)單獨(dú)的管道,管道可以很方便地進(jìn)行復(fù)用和組合,并且方便了我們自定義整個(gè)加載流程,我們可以創(chuàng)建一些自己的管道,加入到管線中,比如資源加密。

          AssetManager 內(nèi)置了3條管線,普通的加載管線、預(yù)加載、以及資源路徑轉(zhuǎn)換管線,最后這條管線是為前面兩條管線服務(wù)的。

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

          3.1.1 啟動(dòng)加載管線【加載接口】

          接下來,我們看一下一個(gè)普通的資源是如何加載的,比如最簡(jiǎn)單的 cc.resource.load,在 bundle.load 方法中,調(diào)用了 cc.assetManager.loadAny,在 loadAny 方法中,創(chuàng)建了一個(gè)新的任務(wù),并調(diào)用正常加載管線 pipelineasync 方法執(zhí)行任務(wù)。

          注意要加載的資源路徑,被放到了 task.input中、options是一個(gè)對(duì)象,對(duì)象包含了 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, typetype, 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 由兩部分組成 preprocessload。preprocess 由以下管線組成 preprocess、transformPipeline { parse、combine },preprocess 實(shí)際上只創(chuàng)建了一個(gè)子任務(wù),然后交由 transformPipeline 執(zhí)行。對(duì)于加載一個(gè)普通的資源,子任務(wù)的 inputoptions 與父任務(wù)相同。

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

          3.1.2 transformPipeline 管線【準(zhǔn)備階段】

          transformPipelineparsecombine 兩個(gè)管線組成,parse 的職責(zé)是為每個(gè)要加載的資源生成 RequestItem對(duì)象并初始化其資源信息(AssetInfo、uuid、config等):

          先將 input轉(zhuǎn)換成數(shù)組進(jìn)行遍歷,如果是批量加載資源,每個(gè)加載項(xiàng)都會(huì)生成RequestItem;

          如果輸入的 itemobject,則先將 options 拷貝到 item 身上(實(shí)際上每個(gè) item 都會(huì)是 object,如果是 string 的話,第一步就先轉(zhuǎn)換成 object了)

          • 對(duì)于 UUID 類型的 item,先檢查 bundle,并從 bundle 中提取 AssetInfo,對(duì)于 redirect 類型的資源,則從其依賴的 bundle 中獲取 AssetInfo,找不到 bundle 就報(bào)錯(cuò)
          • PATH 類型和 SCENE 類型與 UUID 類型的處理基本類似,都是要拿到資源的詳細(xì)信息
          • DIR 類型會(huì)從 bundle 中取出指定路徑的信息,然后批量追加到 input尾部(額外生成加載項(xiàng))
          • URL 類型是遠(yuǎn)程資源類型,無需特殊處理

          RequestItem 的初始信息,都是從 bundle 對(duì)象中查詢的,bundle 的信息則是從 bundle 自帶的 config.json 文件中初始化的,在打包 bundle 的時(shí)候,會(huì)將 bundle 中的資源信息寫入 config.json 中。

          經(jīng)過 parse 方法處理后,我們會(huì)得到一系列 RequestItem,并且很多 RequestItem 都自帶了 AssetInfouuid 等信息,combine 方法會(huì)為每個(gè) RequestItem 構(gòu)建出真正的加載路徑,這個(gè)加載路徑最終會(huì)轉(zhuǎn)換到 item.url 中。

          3.1.3 load管線【加載流程】

          load 方法做的事情很簡(jiǎn)單,基本只是創(chuàng)建了新的任務(wù),在 loadOneAssetPipeline 中執(zhí)行每個(gè)子任務(wù)。

          loadOneAssetPipeline 如其函數(shù)名所示,就是加載一個(gè)資源的管線,它分為2步,fetchparse

          fetch方法:

          用于下載資源文件,由 packManager 負(fù)責(zé)下載的實(shí)現(xiàn),fetch 會(huì)將下載完的文件數(shù)據(jù)放到 item.file 中。

          parse方法:

          用于將加載完的資源文件轉(zhuǎn)換成我們可用的資源對(duì)象:

          對(duì)于原生資源,調(diào)用 parser.parse 進(jìn)行解析,該方法會(huì)根據(jù)資源類型調(diào)用不同的解析方法

          • import 資源調(diào)用 parseImport 方法,根據(jù) json 數(shù)據(jù)反序列化出 Asset 對(duì)象,并放到 assets
          • 圖片資源會(huì)調(diào)用 parseImage、parsePVRTexparsePKMTex方法解析圖像格式(但不會(huì)創(chuàng)建Texture對(duì)象)
          • 音效資源調(diào)用 parseAudio 方法進(jìn)行解析
          • plist 資源調(diào)用 parsePlist 方法進(jìn)行解析

          對(duì)于其它資源,如果 uuidtask.options.__exclude__ 中,則標(biāo)記為完成,并添加引用計(jì)數(shù);否則,根據(jù)一些復(fù)雜的條件來決定是否加載資源的依賴。

          3.2 文件下載

          creator 使用 packManager.load 來完成下載的工作,當(dāng)要下載一個(gè)文件時(shí),有2個(gè)問題需要考慮:

          該文件是否被打包了?比如由于勾選了內(nèi)聯(lián)所有 SpriteFrame,導(dǎo)致 SpriteFramejson 文件被合并到 prefab

          當(dāng)前平臺(tái)是原生平臺(tái)還是 web 平臺(tái)?對(duì)于一些本地資源,原生平臺(tái)需要從磁盤讀取。

          3.2.1 Web 平臺(tái)的下載

          web 平臺(tái)的 download 實(shí)現(xiàn)如下:

          • 用一個(gè) downloaders 數(shù)組來管理各種資源類型對(duì)應(yīng)的下載方式
          • 使用 files 緩存來避免重復(fù)下載
          • 使用 _downloading 隊(duì)列來處理并發(fā)下載同一個(gè)資源時(shí)的回調(diào),并保證時(shí)序
          • 支持了下載的優(yōu)先級(jí)、重試等邏輯

          downloaders 是一個(gè) map,映射了各種資源類型對(duì)應(yīng)的下載方法,在 web 平臺(tái)主要包含以下幾類下載方法:圖片類、文件類、字體類、聲音類、視頻類等等,具體的實(shí)現(xiàn)方式,感興趣的可以點(diǎn)擊「閱讀原文」查看詳情介紹和代碼。

          3.2.2 原生平臺(tái)下載

          原生平臺(tái)的引擎相關(guān)文件可以在引擎目錄的 resources/builtin/jsb-adapter/engine 目錄下,資源加載相關(guān)的實(shí)現(xiàn)在 jsb-loader.js 文件中,這里的 downloader 重新注冊(cè)了回調(diào)函數(shù)。

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

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

          在原生平臺(tái)下,downloadAsset 等方法都會(huì)調(diào)用 download 來進(jìn)行資源的下載,在資源下載之前會(huì)調(diào)用 transformUrl 對(duì) url 進(jìn)行檢測(cè),主要判斷該資源是網(wǎng)絡(luò)資源還是本地資源,如果是網(wǎng)絡(luò)資源,是否已經(jīng)下載過了。只有沒下載過的網(wǎng)絡(luò)資源,才需要進(jìn)行下載。不需要下載的在文件解析的地方會(huì)直接讀文件。

          3.3 文件解析

          loadOneAssetPipeline 中,資源會(huì)經(jīng)過 fetchparse 兩個(gè)管線進(jìn)行處理,fetch 負(fù)責(zé)下載而 parse 負(fù)責(zé)解析資源,并實(shí)例化資源對(duì)象。在 parse 方法中調(diào)用了 parser.parse 將文件內(nèi)容傳入,解析成對(duì)應(yīng)的 Asset 對(duì)象,并返回。

          3.3.1 Web 平臺(tái)解析

          Web 平臺(tái)下的 parser.parse 主要做的是對(duì)解析中的文件的管理,為解析中、解析完的文件維護(hù)一個(gè)列表,避免重復(fù)解析。同時(shí)維護(hù)了解析完成后的回調(diào)列表,而真正的解析方法在 parsers 數(shù)組中。

              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 映射了各種類型文件的解析方法。

          注意:在 parseImport 方法中,反序列化方法會(huì)將資源的依賴放到 asset.__depends__ 中,結(jié)構(gòu)為數(shù)組,數(shù)組中每個(gè)對(duì)象包含3個(gè)字段,資源id uuid、owner 對(duì)象、prop 屬性。比如一個(gè) Prefab 資源,下面有2個(gè)節(jié)點(diǎn),都引用了同一個(gè)資源,depends 列表需要為這兩個(gè)節(jié)點(diǎn)對(duì)象分別記錄一條依賴信息 [{uuid:xxx, owner:1, prop:tex}, {uuid:xxx, owner:2, prop:tex}]

          3.3.2 原生平臺(tái)解析

          在原生平臺(tái)下,jsb-loader.js 中重新注冊(cè)了各種資源的解析方法:

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

          圖片的解析方法竟然是 downloader.downloadDomImage?跟蹤原生平臺(tái)調(diào)試了一下,確實(shí)是調(diào)用的這個(gè)方法,創(chuàng)建了 Image 對(duì)象并指定 src 來加載圖片,這種方式加載本地磁盤的圖片也是可以的,但紋理對(duì)象又是如何創(chuàng)建的呢?

          通過 Texture2D 對(duì)應(yīng)的 json 文件,creator 在加載真正的原生紋理之前,就已經(jīng)創(chuàng)建好了 Texture2D 這個(gè) Asset 對(duì)象,而在加載完原生圖片資源后,會(huì)將 Image 對(duì)象設(shè)置為 Texture2D 對(duì)象的 _nativeAsset,在這個(gè)屬性的 set 方法中,會(huì)調(diào)用 initWithDatainitWithElement,這里才真正使用紋理數(shù)據(jù)創(chuàng)建了用于渲染的紋理對(duì)象。

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

          而對(duì)于 parseJson、parseText、parseArrayBuffer 等實(shí)現(xiàn),這里只是簡(jiǎn)單地調(diào)用了文件系統(tǒng)讀取文件而已。像一些拿到文件內(nèi)容之后,需要進(jìn)一步解析才能使用的資源呢?比如模型、骨骼等資源依賴二進(jìn)制的模型數(shù)據(jù),這些數(shù)據(jù)的解析在哪里呢?

          沒錯(cuò),跟上面的 Texture2D 一樣,都是放在對(duì)應(yīng)的 Asset 資源本身,有些在_nativeAsset 字段的 setter 回調(diào)中初始化,而有些會(huì)在真正使用這個(gè)資源時(shí)才惰性地進(jìn)行初始化。

          像圖集、Prefab 這些資源又是怎么初始化的呢?

          Creator 還是使用 parseImport 方法進(jìn)行解析,因?yàn)檫@些資源對(duì)應(yīng)的類型是 import,原生平臺(tái)下并沒有覆蓋這種類型對(duì)應(yīng)的 parse 函數(shù),而這些資源會(huì)直接反序列化成可用的 Asset 對(duì)象。

          3.4 依賴加載

          creator 將資源分為兩大類,普通資源和原生資源,普通資源包括 cc.Asset 及其子類,如 cc.SpriteFrame、cc.Texture2Dcc.Prefab 等等。

          原生資源包括各種格式的紋理、音樂、字體等文件,在游戲中我們無法直接使用這些原生資源,而是需要讓 creator 將他們轉(zhuǎn)換成對(duì)應(yīng)的 cc.Asset 對(duì)象之后才能使用。

          在 creator 中,一個(gè) Prefab 可能會(huì)依賴很多資源,這些依賴也可以分為普通依賴和原生資源依賴,creator 的 cc.Asset 提供了 _parseDepsFromJson_parseNativeDepFromJson 方法來檢查資源的依賴。loadDepends 通過 getDepends 方法搜集了資源的依賴。

          loadDepends 創(chuàng)建了一個(gè)子任務(wù)來負(fù)責(zé)依賴資源的加載,并調(diào)用 pipeline 執(zhí)行加載,實(shí)際上無論有無依賴需要加載,都會(huì)執(zhí)行這段邏輯,加載完成后執(zhí)行以下重要邏輯:

          • 初始化 assset :在依賴加載完成后,將依賴的資源賦值到 asset 對(duì)應(yīng)的屬性后調(diào)用 asset.onLoad
          • 將資源對(duì)應(yīng)的 filesparsed 緩存移除,并緩存資源到 assets 中(如果是場(chǎng)景的話,不會(huì)緩存)
          • 執(zhí)行 repeatItem.callbacks 列表中的回調(diào)(在 loadDepends 的開頭構(gòu)造,默認(rèn)記錄傳入的 done 方法)

          3.4.1 依賴解析

          dependUtil 是一個(gè)控制依賴列表的單例,通過傳入 uuidasset 對(duì)象來解析該對(duì)象的依賴資源列表,返回的依賴資源列表可能包含以下4個(gè)字段:

          deps 依賴的 Asset資源nativeDep 依賴的原生資源preventPreloadNativeObject 禁止預(yù)加載原生對(duì)象,這個(gè)值默認(rèn)是 falsepreventDeferredLoadDependents 禁止延遲加載依賴,默認(rèn)為 false,對(duì)于骨骼動(dòng)畫、TiledMap 等資源為 trueparsedFromExistAsset 是否直接從 asset.__depends__ 中取出。

          dependUtil 還維護(hù)了 _depends 緩存來避免依賴的重復(fù)查詢,這個(gè)緩存會(huì)在首次查詢某資源依賴時(shí)添加,當(dāng)該資源被釋放時(shí)移除。

          3.5 資源釋放

          這一小節(jié)重點(diǎn)介紹在 Creator 中釋放資源的三種方式以及其背后的實(shí)現(xiàn),最后介紹在項(xiàng)目中如何排查資源泄露的情況。

          3.5.1 Creator 的資源釋放

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

          3.5.2 場(chǎng)景自動(dòng)釋放

          當(dāng)一個(gè)新場(chǎng)景運(yùn)行的時(shí)候會(huì)執(zhí)行 Director.runSceneImmediate 方法,這里調(diào)用了 _autoRelease 來實(shí)現(xiàn)老場(chǎng)景資源的自動(dòng)釋放(如果老場(chǎng)景勾選了自動(dòng)釋放資源)。

              runSceneImmediate: function (scene, onBeforeLoadScene, onLaunched) {
                  // 省略代碼...
                  var oldScene = this._scene;
                  if (!CC_EDITOR) {
                      // 自動(dòng)釋放資源
                      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 的實(shí)現(xiàn)非常簡(jiǎn)潔干脆,將持久節(jié)點(diǎn)的引用從老場(chǎng)景遷移到新場(chǎng)景,然后直接調(diào)用資源的 decRef 減少引用計(jì)數(shù),而是否釋放老場(chǎng)景引用的資源,則取決于老場(chǎng)景是否設(shè)置了 autoReleaseAssets。

          具體實(shí)現(xiàn)方式可戳「閱讀原文」查看閱讀相關(guān)代碼。

          3.5.3 引用計(jì)數(shù)和手動(dòng)釋放資源

          剩下兩種釋放資源的方式,本質(zhì)上都是調(diào)用 releaseManager.tryRelease 來實(shí)現(xiàn)資源釋放,區(qū)別在于 decRef 是根據(jù)引用計(jì)數(shù)和 autoRelease 來決定是否調(diào)用 tryRelease,而 releaseAsset 是強(qiáng)制釋放。

          資源釋放的完整流程大致如下圖所示:

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

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

          tryRelease 支持延遲釋放和強(qiáng)制釋放2種模式,當(dāng)傳入 force 參數(shù)為 true 時(shí)直接進(jìn)入釋放流程,否則 creator 會(huì)將資源放入待釋放的列表中,并在 EVENT_AFTER_DRAW 事件中執(zhí)行 freeAssets 方法真正清理資源。不論何種方式,資源會(huì)傳入到 _free 方法處理,這個(gè)方法做了以下幾件事情。

          • _toDelete中移除
          • 在非 force釋放時(shí),需要檢查是否還有其它引用,如果是則返回
          • assets 緩存中移除
          • 自動(dòng)釋放依賴資源
          • 調(diào)用資源的 destroy 方法銷毀資源
          • dependUtil 中移除資源的依賴記錄

          3.5.4 資源釋放的問題

          最后我們來聊一聊資源釋放的問題與定位,在加入引用計(jì)數(shù)后,最常見的問題還是沒有正確增減引用計(jì)數(shù)導(dǎo)致的內(nèi)存泄露(循環(huán)引用、少調(diào)用了 decRef 或多調(diào)用了 addRef),以及正在使用的資源被釋放的問題(和內(nèi)存泄露相反,資源被提前釋放了)。

          從目前的代碼來看,如果正確使用了引用計(jì)數(shù),新的資源底層是可以避免內(nèi)存泄露等問題的。

          這種問題怎么解決呢?

          首先是定位出哪些資源出了問題,如果是被提前釋放,我們可以直接定位到這個(gè)資源,如果是內(nèi)存泄露,當(dāng)我們發(fā)現(xiàn)問題時(shí)程序往往已經(jīng)占用了大量的內(nèi)存,這種情況下可以切換到一個(gè)空?qǐng)鼍埃⑶謇碣Y源,把資源清理完后,可以檢查 assets 中殘留的資源是否有未被釋放的資源。

          要了解資源為什么會(huì)泄露,可以通過跟蹤 addRefdecRef 的調(diào)用得到,下面提供了一個(gè)示例方法,用于跟蹤某資源的 addRefdecRef調(diào)用,然后調(diào)用資源的 dump方法打印出所有調(diào)用的堆棧。

          結(jié)語

          本教程包含詳細(xì)的代碼解讀,為了保障手機(jī)端的閱讀體驗(yàn),沒有全部放進(jìn)來,歡迎大家點(diǎn)擊文末閱讀原文按鈕前往社區(qū)查看!

          非常感謝寶爺的無私分享,快來給寶爺點(diǎn)贊吧!

          瀏覽 271
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人簧片在线浏览观看 | 男女黄色在线观看 | 黄在线观看网站 | 亚洲色图图片区 | 欧美黑人一级 |