Try   HackMD

Unity Addressable Asset System 簡介

AssetBundle 資源載入

在開始介紹 Addressable 系統之前,我們先來簡單回顧一下 AssetBundle 的運作方式。
首先,在 Unity 裡面,資源的載入大該分為靜態加載Resources.Load、與AssetBundle.LoadAsset三種。

靜態加載

基本上就是直接在 Editor 的 Inspector 視窗中掛 Reference。
例如你可以在某個腳本中宣告 public GameObject myGameObj; 或是 [SerializeField] private Texture2D myTexture; 接著把目標資源拉進 Inspector 中的欄位當作參考。
這樣的方式就屬於靜態加載,在掛著參考的腳本生成實體時,這個參考資源會自動被載入。

Resources.Load

Unity 的 Resources 資料夾想必大家都很熟悉,Resources.Load 方法可以很方便的直接載入Resources 資料夾內的資源。然而,根據官方的 教學文件,Resources 系統的最佳使用方式則是:不要使用它
這邊列出幾個 Resources 系統存在的問題:

  • Resources 資料夾使用不當會增加遊戲啟動時間和構建時間
    • 在遊戲啟動時,遊戲會為 Resources 資料夾的結構建立一張表,讓 Resources.Load 可以知道要去哪個資料夾內載入資源,因此若有很多的 Resources 夾散落在各處,就會使遊戲建立這張表更花時間。
    • 所有放在 Resources 資料夾下的資源都會在 Build 遊戲時一起放進 apk 或 obb(android),因此 Resources 資料夾內存放越多資源,就會讓 Build 更花時間。
  • 隨著 Resources 資料夾數量的增加,這些資料夾中的資源管理變得非常困難
    • 你可以在 Assets 底下任何位置建立 Resources 資料夾,當專案越做越大,各種資料夾底下都有 Resources 資料夾時,你會開始難以找到某個資源到底放在哪個 Resources 資料夾內。
  • Resources 系統無法使用熱更新

當然 Resources 系統既然存在,就有適合使用它的地方,適合使用的情境可以直接參考上述教學文件。

AssetBundle.LoadAsset

在使用 AssetBundle 載入資源之前,需要先將資源進行打包,打包的方法各式各樣,每個專案都不盡相同,這邊我就舉最基本的例子。

//程式碼指定打包哪些資源
AssetBundleBuild build = new AssetBundleBuild();
build.assetBundleName = "testAB"
build.assetNames = new[] { "Assets/testAsset.prefab" }; //打包這個prefab資源
buildMap.Add(build);    
BuildPipeline.BuildAssetBundles(bundlePath, buildMap, buildOption, platform);
//直接打包所有已設定為AssetBundle的資源
BuildPipeline.BuildAssetBundles(bundlePath, buildOption, platform);

打包好資源後,AssetBundle 載入資源分為兩個步驟,先載入 AssetBundle 本身,再載入 AssetBundle 裡面的資源,直接上簡單的程式碼。

IEnumerator Load(){
    //載入AssetBundle本身
    UnityWebRequest webRequest = UnityWebRequestAssetBundle.GetAssetBundle(url);
    webRequest.SendWebRequest();
    yield return new WaitUntil(() => webRequest.isDone);
        
    //載入AssetBundle內的資源
    AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(webRequest);
    AssetBundleRequest request = bundle.LoadAssetAsync(prefab, typeof(GameObject));
    yield return request;
}

AssetBundle 資源回收

接下來講講大部分開發者(包含我自己)容易自動忽略的部分,資源的釋放。
AssetBundle 的 UnLoad 方法需要傳入 true 或 false 參數,以下做個簡易表格來比較兩者差異。

AssetBundle.UnLoad(true) AssetBundle.UnLoad(false)
可釋放AssetBundle本身,無法再使用AssetBundle.LoadAsset等功能
從AssetBundle載出來的資源會一並釋放 從AssetBundle載出來的資源不受影響
好處是
不會有重複載入資源的問題
好處是
不用擔心有某人正在使用的某資源會一起被釋放
壞處是
若有其他人使用到AssetBundle載出來的資源
使用到的部分會直接消失
壞處是
容易有重複載入資源的問題
導致記憶體空間的浪費
  • 某個遊戲物件使用到 AssetBundle 的部分被 UnLoad(true)
    這邊舉一個遊戲物件的材質被 UnLoad(true) 的例子,想必各位對這個顏色都非常熟悉吧。

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  • 重複載入資源
    這邊用幾張圖來說明一下「重複載入資源」是怎麼一回事。
    一個 AssetBundle 會長成這樣,裡面有一小部分存有檔案清單與壓縮資訊等內容,然後剩下的部份可能會有各式各樣包進去的資源。

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

    首先,我們先載入了某個 AssetBundle,並從裡面載入一份 Texture 到記憶體中,接著呼叫 AssetBundle.UnLoad(false)把 AssetBundle 釋放掉,這時候,剛剛載入的 Texture 還會存在記憶體中,儘管沒有人使用它。

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

    接下來,我們再一次的載入剛剛的 AssetBundle,然後再做一次上述的動作,載入 Texture 並釋放 AssetBundle,這時候就會有兩份 Texture 在記憶體內。

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

    如果上面的事情不斷的發生,到最後記憶體會變成下面這張圖的樣子,好幾份一樣的 Texture 堆在記憶體內,導致大量的浪費。

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

    這個問題當然還是有方法可以解決,可以使用Resources.UnloadUnusedAssets來釋放這些沒人使用的資源,不過這個方法開銷很高,千萬不要隨意使用,通常會在切換場景時使用較為恰當。

Addressable 資源載入

複習完 AssetBundle,讓我們回來介紹 Addressable 系統的運作方式。

資源打包

在 AssetBundle 中,打包資源需要撰寫繁雜的程式碼;而 Addressable 系統有專屬的介面,讓打包這件事變得簡單很多。
在 Addressable 介面裡,資源會以 Group 去做分群,你可以想像一個 Group 就是一包 bundle。
首先,你可以將你想要打包的資源拖拉進 Addressable 介面的某個 Group,或者你可以直接勾選資源的 Addrressable 欄位,它會自動被加進預設的 Group 內。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

接著你可以針對每個資源去設定各自的 address 與 label,在完成了所有資源的設定後,按下介面右上角的 Build 按鈕即可打包 bundle。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

關於 Addressable 的 bundle 其實還有許多細節的設定,例如打包與載入的路徑、Profile 的設定、客製化的 Build 腳本等等,礙於篇幅,之後有機會再做介紹。

資源載入

  • 使用 AssetReference
public AssetReference assetRef;
assetRef.InstantiateAsync();

你可以簡單的將public GameObject取代成public AssetReference,這個 AssetReference 只能參考是 Addressable 的資源,它會在你對其呼叫實例化時,進行載入並生成。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  • 使用 AssetLabelReference
public AssetLabelReference assetLabelRef;
Addressables.LoadAssetsAsync<GameObject>(assetLabelRef, null);

Addressable 系統提供了一個滿方便的功能,你可以使用 label 進行資源載入。假設有一種情境,你需要載入某個種類的資源,但這個種類的資源散布在各個 bundle 裡面,以傳統的 AssetBundle 做法,你可能會需要載入各個 bundle 再載入你需要的資源。然而在 Addressable 系統中,你可以在 Addressable 介面對這個種類的資源設定同一種 label,然後就可以使用這個 label 來簡單的一次載入你所需的資源。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  • 使用字串作為 key
string assetKey = "myAsset";
Addressables.LoadAssetsAsync<GameObject>(assetKey, null);    
List<string> assetKeys = new List<string>() { "keyA", "keyB"};
Addressables.LoadAssetsAsync<GameObject>(assetKeys, null);

在許多情況下,遊戲需要載入的資源可能不見得是某個特定的 asset 或某個固定的 label,因此無法使用拉參考的方式去載入資源,這時候我們可以使用字串作為 key 來載入資源。key 可以是上面的提到的 AssetReference 或 AssetLabelReference,也可以是資源的 address 或 label 字串,你可以使用單一的 key 或者一組 key list 來載入不特定的資源。舉個例子,遊戲 server 平常會傳送 stringA 給 client 去載入某一組資源,但是在春節活動期間,server 則會傳送 stringS 給 client 來載入另一組資源。

Addressable 資源回收

資源管理的最高指導原則,「鏡像的載入與釋放」,意思就是,有一個資源載入就需要有一個對應的資源釋放。這邊會簡單介紹 Addressable 系統中載入資源的 API,以及與其對應的釋放資源方法。

資源載入與釋放

Addressables.LoadAssetAsync //載入單一資源
Addressables.LoadAssetsAsync //載入多個資源
資源載入的 API 有很多形式,就像上個章節所提到的,你可以使用 AssetReference、AssetLabelReference、或 key 來作為參數使用。

Addressables.Release //釋放資源
Release方法只會減少資源的 referenceCount(ref-Count),當資源的 ref-Count 歸零時才會"準備"被釋放,並且會減少所有依賴項目的 ref-Count。
你想問這句話是什麼意思?讓我來簡單說明一下 Addressable 系統中,資源什麼時候才會真正被回收。
現在我有一個 bundle,裡面有資源三寶,分別是 coffee、tea、和me。

  1. 第一步載入 coffee。
    各個資源的 ref-Count 狀態:coffee(1)、tea(0)、me(0)、bundle(1)
    (coffee被載入記憶體)
  2. 第二步載入 tea。
    各個資源的 ref-Count 狀態:coffee(1)、tea(1)、me(0)、bundle(2)
    (tea被載入記憶體)
  3. 第三步釋放 coffee。
    各個資源的 ref-Count 狀態:coffee(0)、tea(1)、me(0)、bundle(1)
    (coffee 的 ref-Count 歸零,但它還在記憶體內,只是"準備"被釋放)
  4. 第四步釋放 tea。
    各個資源的 ref-Count 狀態:coffee(0)、tea(0)、me(0)、bundle(0)
    (bundle 內的各項資源 ref-Count 都是零,因此真正釋放 bundle 本身與其內部資源)

這個例子應該可以讓大家理解資源什麼時候才會真正被回收,簡單用一句話說明這個規則:我們可以只載入 bundle 內的部分資源,但不能只釋放 bundle 內的部分資源
雖然在第三步之後,你依然可以使用 Resources.UnloadUnusedAssets 達到釋放 coffee 的目標,但這個方法就像 AssetBundle 章節所說的開銷很大,絕對不能隨便使用!

場景載入與釋放

Addressables.LoadSceneAsync //載入場景
Addressable 系統也有提供載入場景的方法,不過只接受單個 key 作為參數,Addressable 的載入方法皆有提供取得載入進度的 API,因此依然可以實作載入進度條喔!

Addressables.UnloadSceneAsync //釋放場景
SceneManager.LoadScene(LoadSceneMode.Single); //打開新場景(Single mode)也能釋放原場景
打開新場景可用使用 Addressable 的方法,也可以使用 SceneManager 原有的方法,在 Single 模式下,它們都能使原先使用 Addressables 載入的場景正確減少 ref-Count,若 ref-Count 歸零,原場景就會順利被釋放。

物件生成與銷毀

Addressables.InstantiateAsync //生成物件
Addressable 的物件生成方法,對還未載入的資源會自動幫你進行載入再做生成,在載入的過程也會自動載入所有依賴的資源,同時使這些資源的 ref-Count 增加。

Addressables.ReleaseInstance //銷毀物件
//關閉包含該物件的場景也能銷毀該物件
ReleaseInstance 方法只對 Addressables.InstantiateAsync 生成出來的物件有效喔!

資料載入與釋放

Addressables.LoadResourceLocationsAsync //載入資源清單
Addressables.GetDownloadSizeAsync //取得下載資料大小
LoadResourceLocationsAsync 方法一樣使用單個或一組 key 作為參數,它載入的資料是資源的"清單",並不會真正載入資源,這個方法可以應用在資源的查詢。舉個例子,你使用這個方法得到一份資源清單,這份清單中的資源有 GameObject、Texture、Material 等各種類型,你可以尋訪這個清單的所有資源,只要它是 GameObject,就使用它的資源路徑(IResourceLocation)作為參數去載入該資源並生成出來。

Addressables.Release(Handle) //釋放資料
資料釋放的 API 一樣使用 Release,不過這邊傳入的參數是載入資料時回傳的 Handle,可參考以下程式碼的簽章。

static AsyncOperationHandle<IList<IResourceLocation>> LoadResourceLocationsAsync(object key, Type type = null)
static void Release<TObject>(AsyncOperationHandle<TObject> handle)

Addressable 與 AssetBundle 比較

AssetBundle Addressable
資源打包 寫程式管理哪些資源要打包在一起 將資源拉進 Addressables 視窗
資源載入 寫程式向目標位置要求bundle
寫程式載入 bundle 再載入資源
在 Group 設定載入位置
寫程式直接載入載入資源
資源釋放 需要考慮使用 UnLoad(true)
還是使用 UnLoad(false)
資源的 ref-Count
讓系統可以自動管理資源釋放
資源改名/改路徑 程式碼要跟著改 只要不修改資源的 address
就不需要任何修改
資源關聯性 相關聯的資源包都要寫程式載入 系統會自動處理關聯性自動載入
重複打包資源 有可能重複打包資源
同個資源可能會重複包進不同bundle
不會重複打包資源
同個資源不會讓你包進不同bundle
載入所有"砲塔" 如果各種砲塔散在
各個 bundle 裡面就很麻煩
可以使用 label 載入
不用擔心他們散在不同 bundle 裡面
  • Addressable 解決哪些問題
    從上面簡易的表格可以發現,Addressable 系統比起 AssetBundle 多了一些優勢
    • 加速迭代: Addressable 系統的打包與載入更加便捷,並且對資源的重新命名和更改路徑等修改不再需要調整程式碼,能一定程度的加快迭代速度。
    • 更加方便的載入資源:不再需要知道資源放在哪個 bundle 就可以直接載入資源,除了能使用資源的 address 之外,也可以透過 label 一次載入不同 bundle 中的資源
    • 資源依賴的管理:Addressable 系統會自動載入資源的所有依賴,比起 AssetBundle 需要考慮要先載入哪些依賴資源,使用上更方便。
    • 記憶體管理:Addressable 系統會自動計算資源的 ref-Count ,並且在 ref-Count 歸零時自動釋放,自動化的記憶體管理,可以免去 AssetBundle 中處理資源釋放的麻煩。

舊系統如何轉移到 Addressable

直接轉換

  • 靜態加載的部分需要做程式碼的修改,例如public GameObject[SerializedField] Texture這類拉資源參考的部分,改成public AssetReference[SerializedField] AssetReference
  • 另外像Instantiate()改用Addressables.InstantiateAsync()
  • 載入資源則使用Addressables.LoadAssetAsync

直接轉換的內容會比較繁雜,目前並沒有比較好轉移的方式,只能去爬程式碼並手動轉換。

Resources 資料夾

  • 你可以將 Resources 資料夾內的資源設定為 Addressable,當 Resources 資料夾內的資源被標記為 Addressable 時,系統會自動建立一個名為"Resources_moved"的新資料夾(當然新資料夾已經存在就不再建立),並將資源從 Resources 資料夾移過去。此步驟會有提示訊息如下圖。
  • 原本的載入程式碼:Resources.LoadAsync<GameObject>(“map/city.prefab”);
    改為:Addressables.LoadAssetAsync<GameObject>(“map/city.prefab”);
    舊路徑的關聯位置會被保留,因此作為參數的資源路徑不用特別做修改,只需要改成Addressables.LoadAssetAsync即可。

AssetBundle

  • 若專案已經有 AssetBundle 的設置,第一次打開 Addressable 系統的介面時,Unity 會有提示訊息詢問是否要將 AssetBundle 全部轉入到 Addressable 群組裡。

AssetGraph

AssetGraph 提供了四種 Addressable 的節點,在這邊做簡單介紹(AssetGraph 能夠講的內容也可以很多,請容許我不詳細介紹><)。

  • Set Asset Address

    這個節點可以將資源設定成 Addressable,並且可以透過資源路徑的 Pattern 去自訂資源的 address。
  • Set Asset Group

    這個節點會將 Addressable 的資源設定成指定的 Group。
  • Set Asset Label

    這個節點會將 Addressable 的資源加上給定的 label,可以選擇覆蓋原先的 label,不然就會新增你給的 label。
  • Build Addressable Assets

    這個結點會將設定好得 Addressable 資源打包,節點上可以設定profile、打包使用的腳本、是否要更新之前的打包等。

這邊舉一個簡單的 AssetGraph 使用例子。

圖片中灰色的節點主要用來選擇哪些路徑下的資源需要被打包進 bundle,中間藍色的節點是用來將左側連進來的資源進行分類,資源被分類完送出去後連到黃色節點,每個黃色節點就是一個 bundle,可以調整 bundle 的設定,最後再將這些黃色節點連接到紅色的 build 節點。

至於 Addressable 的節點應該如何使用呢,一樣看個簡單的例子。

圖片中灰色與藍色的節點功能是和上面的例子相同的,黃色的節點從左到右依序是「將資源設定成 Addressable」->「設定 label」->「設定 Group」,最後的紅色節點是 Addressable 的 build。
Addressable 的節點使用上有一點需要特別注意,你需要先將資源設定成 Addressable,再對他們設定 label 與 Group 才會有效。

知道了 Addressable 節點後,我們要如何從 AssetGraph 轉移到 Addressable 呢?
其實很簡單,我們只需要把 AssetGraph 中的黃色節點取代成 Addressable 的三個節點就可以了!當然要記得調整各個節點上的設定。
還有就是,既然我們想要轉移到 Addressable 系統,那就不用再使用 build 節點,只要把 Addressable 的三個節點設定好,按下 AssetGraph 介面的執行後,這現資源就會依照你的設定加到 Addressable 介面當中,未來要包 bundle 就使用 Addressable 介面的 build 就可以囉。

Addressable Feature

文章的最後,我想要特別介紹一些 Addressable 系統中我認為非常好用的功能。

Play Mode Script


在 Addressable 介面上方的工具列中,有一個"Play Mode Script"欄位,這個欄位的功能是,當你在 Editor 進入 Play Mode 的時候,系統要用哪種模式去使用你的資源,模式有以下三種。

  1. Use Asset Database
    這個模式讓你在 Play Mode 載入資源時,直接從你的專案 Assets 載入,你可以不用打包 bundle,加快擬開發專案的迭代速度。
  2. Simulate Groups
    這個模式一樣不需要打包 bundle,但它可以模擬你的 bundle 配置,在載入資源時處裡資源的相依關係,實際上它還是直接從專案 Assets 載入資源,但你可以假裝它是從 bundle 載入,主要是用來讓你觀察,你在遊戲執行期間載入 bundle 的狀況,觀察方法會在下一個小節「Profiler (Event Viewer)」介紹。
  3. Use Existing Build
    這個模式就會真正從 bundle 載入資源,因此你需要先做資源的打包,這樣在進入 Play Mode 時才能順利載入資源。

Profiler (Event Viewer)


Addressable 系統提供了 Profiler 功能,讓你可以查看資源的 ref-Count。
圖片中,可以看到當我使用Addressables.LoadAssetAsync載入"BigWin.prefab"時,綠色的區域會往上長;相對的,當我使用Addressables.Release釋放他們,綠色區域也會往下降。這個綠色的區域代表的正是該資源的 ref-Count。
有了這個功能,當你在開發遊戲時,如果遇到莫名的幀率下降,就可以使用這個功能來查看遊戲中的各項資源是不是有被正確釋放。

Analyze - Check Duplicate Bundle Dependences


Addressable 系統還提供了很有趣的分析功能,其中"Check Duplicate Bundle Dependences"可以幫你檢查你的 bundle 中,是否有哪些相依的資源是重複打包的。
讓我們來看一個例子:
假設我有一個 Cube.prefab 與一個 Sphere.prefab,他們倆都使用同一個材質與同一張貼圖,但是他們被分別打包在不同 bundle 裡面。當我分別載入他們倆之後,記憶體實際上會有兩份同樣的材質與貼圖,導致了記憶體空間的浪費,就像下圖中狀況。

為了避免這種情況,最正確的做法就是,在包 bundle 時,需要先想好哪些是共用資源並將他們抽出來,製作成單獨的一包 bundle,這樣一來,我們無論先載入哪一個 prefab,Addressable 會幫我們自動載入相依資源,也就是獨立抽出來的共用資源包,接著載入另一個 prefab 時,就不會重複載入共用資源了,就像下圖展示的情況。

然而,即使我們在打包 bundle 的時候非常的小心謹慎,百密必有一疏(是這樣用吧?),總是會有不小心漏掉的地方,這時候就是這邊要講的分析功能上場的時候了。

你可以在分析面板中選擇"Check Duplicate Bundle Dependences",然後按下"Analyze Selected Rules",就會看到面板中幫你列出哪些相依資源是被重複打包的,這時候你有兩種方式可以解決這個問題。
第一種方式是你可以按照分析的結果去自己手動調整 bundle。
第二種方式則是按下"Fix Selected Rules",系統就會自動幫你把這些重複的相依資源獨立出來組成一個 Group,是不是非常方便!

Bundle Pack Separately

你可以在 Addressable 介面中選取 Group,接著在 Inspector 視窗中的 Advanced Options 裡面找到 Bundle Mode 設定值,這個設定值預設是"Pack Together",意思是這個 Group 下的資源會整個打包成一份 bundle。你可以將這個設定值調整成"Pack Separately",意思是將這個 Group 下的資源每個資源包成一個 bundle。
分別打包成多個 bundle 有什麼好處?讓我們再拿出資源三寶(coffee、tea、me)來實驗一下。

  1. 事前準備
    我們將資源三寶們包在同一個 bundle 中。

  2. 實驗一
    首先,我們將資源三寶們都載入進記憶體,接著把 tea 釋放掉,然後觀察記憶體的狀態,會發現 tea 仍然存在於記憶體當中,這個現象符合我們前面提到的規則:我們可以只載入 bundle 內的部分資源,但不能只釋放 bundle 內的部分資源

  3. 實驗二
    既然有這個規則在,那我們乾脆就把資源三寶們分開打包成三個獨立的 bundle 就好了呀,可是我們又不想要將他們拆成三個 Group,不然就不能稱為三寶了。
    這時候我們就可以使用"Pack Separately"的模式,雖然三寶們還在同一個 Group,但他們會被分開打包成三個 bundle,這時候我們就能真正釋放單個資源。

    然而,當我們載入 coffee 和 tea 的時候,卻觀察到記憶體有重複載入相同資源的現象。

  4. 實驗三
    我們把 coffee 和 tea 展開來看看內部的資源,他們使用了一樣的 cup material、cup mash、cup texture,因此如果將他們分開來打包,就會有重複打包這些相依資源的問題,解決這個問題的方法在「Analyze - Check Duplicate Bundle Dependences」小節中有詳細說明過了,我們就使用一樣的方法讓系統自動將重複的相依資源抽取出來組成一個 Group,然後基於同樣的原因,我們不想要讓這個相依資源群組成一大包,所以同樣使用"Pack Separately"模式讓這個 Group 的資源分開打包。

    完成之後我們再進行載入三寶的實驗,記憶體會長得像下圖,沒有重複打包相依資源的問題,也能夠獨立釋放單個資源。

  5. 實驗四
    從上面幾個實驗看下來,"Pack Separately"這個功能看似非常美好,但是當我們將專案中所有的 Group 都設定為 "Pack Separately" 模式後,我們開始意識到一個問題,難道這樣無腦的將所有資源全部分開打包,導致一個專案內會有上百上千個 bundle,這樣對記憶體的開銷也絕對不是好事。
    我們知道一個 bundle 除了資源之外,還另外包含了資源壓縮資訊、資源清單、兩個檔案讀取 buffer等等,其中檔案讀取 buffer 佔據了多數的空間,在大部分的平台中,一個 bundle 的檔案讀取 buffer 為 64 KB,因此一個 bundle 的基本開銷至少就是 64x2 = 128 KB。
    回到專案的情況,當專案的 bundle 數量來到 1000 上下,這些 bundle 的基本開銷就至少來到將近 128 MB 了,顯然,這樣的打包方式也不完全合理。

  6. 實驗結論
    從一連串的實驗看起來,"Pack Separately"功能絕對非常有用,但是也不能將這個功能當作網路吃到飽的無限使用,最好的打包策略還是,在打包的時候就考慮好哪些資源屬於同時載入且同時釋放的,那就把這類資源打包在一起,既可以保持 bundle 的細緻度的同時,又能夠使 bundle 數量不會過多。
    實驗參考:Tales from the optimization trenches: Saving memory with Addressables
    這篇文章的作者提供了自己的做法,它會幫你分析哪些資源屬於同時載入且同時釋放的,並且將它們包在同一包裡面。根據作者提供的數據,他們的做法可以讓一個有 8718 個 bundle 的專案,下降到 5199 個 bundle,同時節省了大約 40% 的記憶體開銷(from 311 MB to 184 MB)。

最後

以上是我花了一些時間研究並整理出來的內容,其實還有部分細節礙於篇幅沒有充分的說明,如果有哪個地方我描述得比較模糊,非常歡迎大家提出來一起討論!