Unity中關於AssetBundle的相關知識總結

什麼是AssetBundle? 

        Asset在英文中是資產,有價值的東西的意思,在Unity中翻譯過來就是:資源,這個資源囊括了音頻(audioclip)、遊戲模型文件(.FBX)、紋理貼圖(Texture)、材質(Meterial)、3D模型(3DModels),這些資源以文件的形式被保存在電腦的硬盤中。(既有Unity內置格式的,如prefab,Material等,也有其他內置格式的,3dMax的內置格式FBX等)。Asset是存儲在硬盤上的在UnityProject的路徑下任何一個文件。

        UnityObject是被序列化(稍後介紹序列化)之後的能被Unity引擎使用的資源數據,比如骨骼動畫(Mesh)、預設物(Prefab)、圖片(Sprite)、音頻片段(Audioclip)、動畫片段(AnimationClip)等。Object之間可以互相引用,但被引用的這些Objects跟引用的Object必須是在同一個Assets文件夾下,Asset和Object是一對多的關係,一個Asset文件中至少有一個Object。

        Bundle在英文中的解釋是:a collection of things, or a quantity of material, tied or wrapped up together,即:事物的一個集合,也可以說是用繩子捆綁起來的這種捆、束的狀態,作爲動詞是捆綁的意思。

        從字面上來看,AssetBundle指的就是資源包的意思,即用一個文檔文件把上面所提到的資源給包起來,形成一個獨立的資源文件--AssetBundle,經過壓縮(也可以不壓縮)之後保存於硬盤上。

Unity中資源被序列化的基本原理

        在這裏拿一個Prefab作爲例子,ball(路徑:Assets/Prefab/xxx/ball.prefab)由三個子物體部分組成(如下圖),除此之外,它身上掛載着XXXBallController這個腳本組件。其他幾個組件(Transform、SpriteRenderer)都是Unity內置組件,原理都一樣。

                                                                               

                                                           一個Asset可以有多個GameObject組成

                                               

                                                                              Ball引用的物體

        當我們從場景中把這個做好的ball拖到Assets文件中的時候,Unity就會做“序列化”,將ball包含的相關數據以文本的形式寫到硬盤中並生成一個與它同名的.meta文件(如下圖),打開Assets的文件目錄,你會發現多了一個ball.prefab這個文件,雖然它的後綴是.Prefab,但他是文本可讀的。Unity能識別並通過fileID和/或GUID去找到其他資源。而序列化就像是給這個資源生成一張地圖,這張地圖中包含了所引用資源的地址,只要有了地址,不管引用的資源在Assets中的那個位置,我都能找到該資源並對它進行實例化操作。同時,Unity自動爲這個ball生成的.meta文件,表示Asset中又多了一個資源,需要對他進行標識,像是剛出生了一個人,需要給他一個全球唯一的身份證號,這樣做就是爲了別的Object引用他的時候能索引到它,又進入了上一個循環中,這就是Unity的資源管理機制。

                                  

                                       用sublime查看ball.prefab文件,裏面記錄了所有跟ball相關的標識符和ID

        這樣看來,Unity的序列化就像我們平時使用身份證號一樣,身份證號能代表我們自己,同樣只要知道了你的身份證號,我就可以定位找到你,序列化正是記錄與他相關的資源的ID號來定位資源。

        GUID保存在.meta文件中,它標識的是這個資源文件的在硬盤上的位置,它像一個索引,不管資源文件保存在哪,只要有了文件GUID,那就能定位到這個文件了;Local ID標識的是該資源文件中的每個對象,因爲Asset中可能包含多個對象。

        現在解析ball.prefab這個文件。爲了簡便,這裏只說明Unity對XXXBallController.cs這個資源的序列化,因爲其他資源的序列化都是相通的。第一行 是在序列化的時候Unity給他生成的本地ID(114768760368899920),這個ID的作用範圍僅在自己所包含的Assets中,保證在ball包含的衆多Assets中的唯一性。 表示其類型是一個Monobehaviour的腳本。numText、multyple10XText、selectedEffGO三者只有fileID,表示着三個物體只存在於ball中,因此,實例化時只根據fileID就能找到這個資源;而boomEff不僅有fileID,還有guid,表示boomEff在Assets中是一個單獨的資源,因此,實例化時,根據fileID去找這個資源是找不到的,必須拿到boomEff的guid才能拿到boomEff這個資源。m_Script同樣的道理。

                                                         

                            boomEff變量引用了add score eff.prefab,它的gui和ball中boomEff對應的guid相同

                                                               

                                序列化之後,Unity會在相同路徑下生成同名的.meta文件,標識它的guid

        反序列化也就是序列化的逆過程,根據序列化文件中記錄的這些標識符和ID號拿到資源然後爲自己所用的過程。

        這樣,我們清楚了Unity中關於資源管理機制的基本知識。這裏總結一下,Unity的資源管理就是,通過給每個在Assets中的資源生成GUID和/或LocalID的方式,讓每個資源有了當前上下文唯一的“身份證號”,不管以後它在磁盤的任何位置(只要在Assets文件夾中),只要拿着這個ID就能找到該資源;另外一個本地ID(local ID)則是爲了區別Asset中不同的Objects。這就是爲什麼.meta文件如此重要的原因之一,Unity編輯器保存有一個映射表,該映射表的作用是將特定的文件路徑和這些文件的guid映射起來。如果在Unity編輯器關閉時丟失了.meta文件,或者Asset文件沒有隨着.meta文件一起遷移,都會破壞對該資源中對象的引用。這樣一來,localID和GUID能夠提供一個穩定且強大的對資源數據的互相引用。

        然而,如你所見,GUID的位數是32位,位數過大會影響計算機的運算速度,因此爲了避免使用32位的GUID,Unity中的另一個概念應用而生:InstanceID,是Unity專門將複雜的大位數的GUID和LocalID轉換成簡單而本地唯一的一組整型值,每當在緩存中新生成一個對象時,InstanceID的值+1並將InstanceID與新的對象關聯,因此,Unity只要在內部不斷維護更新這個緩存就可以了。該緩存其實也是一個映射表,這樣,僅利用簡單的InstanceID就可以快速地獲取由該InstanceID表示的已加載對象;如果給的這個InstanceID表示的對象未加載到內存中,Unity會根據GUID-Object的映射將GUID和LocalID解析爲該對象的數據實體進行加載,IDà對象實體的查找,之後對資源的引用統一通過InstanceID。

        InstanceID的初始化發生在應用啓動的時候,應用最先需要用到的所有對象在此時被寫入該緩存映射表中,需要注意的是,這包括Resources文件夾中的所有數據,這也是爲什麼在Resources下不要放過多的資源的原因之一,因爲會拖慢應用啓動速度。應用啓動之後,隨着程序的運行,會有新資源的導入生成,新的AssetBundle對象的加載,都會被添加到緩存映射表中並生成InstanceID與之對應。而InstanceID的刪除僅在AssetBundle卸載某資源GUID和LocalID時才發生,卸載之後,InstanceID、GUID、LocalID與其之間的映射被刪除,節省內存;當再次加載該AssetBundle時,會爲AssetBundle中的每個對象重新創建一個新的InstanceID。

Unity中的另外一種資源加載方式—>Resources.load類

        Resources.load方式是我第一個學習的資源加載的方式,也是比較簡單的,在Assets文件下創建Resources文件夾,再把你要在運行時動態加載的資源放在該目錄下,代碼中調用Resources.load這樣的API(有很多重載方法)即可,注意路徑的正確性。

        但是,有幾個關於Resources類加載的坑必須先說明一下,Unity在打包的時候,Resources文件夾下的所有資源都未經壓縮、原封不動地被打包到應用中,包體變大,由於應用啓動時還要必須加載所有Resources下的所有文件,儘管加載的這些資源不是啓動第一個場景所必須的,因此啓動時間也變慢;另外,不利於項目的精細化內存管理。

        在打包的時候,項目中所有以“Resources”命名的文件夾下的資源都被合併打包進一個序列化文件中,該文件中包含元文件以及資源的索引信息,這有點類似於AssetBundle,如前所述,即對象名稱和對象的GUID/LocalID之間建立查找樹,該查找樹在大多數平臺上是一個平衡查找樹,其複雜度跟Resources下對象的數量呈線性關係。

到這裏,各位肯定對Resources加載的優劣便很清楚了!

Unity中的AssetBundle有什麼用?爲什麼Unity引擎中會使用這個技術?

        AssetBundle允許我們將一個或多個資源以一個文檔的形式保存,它有自己的索引信息和序列化數據。AssetBundle有兩部分組成:頭信息(header information)和數據段(data segment),前者是該AssetBundle的信息表徵,如壓縮方式、資源清單、資源跟ID之間的映射表等,同樣包含一個平衡查找樹,也就是AssetBundle中對象的數量跟查找的時間呈線性關係;數據段中包含的是對象被序列化之後的數據,其大小取決於採用何種壓縮方式。

        好了,總結一下,AssetBundle文件包含一個或多個資源(如果你願意,你可以將所有資源貼上同一個AssetBundle名稱,這樣,擁有相同AssetBundle名稱的所有資源都被打包進同一個AssetBundle文檔中了)。

        生成AssetBundle的方法很簡單,加載AssetBundle資源的方式有四種,在此不再詳述(都是API,官網都有詳細的介紹),Unity已經爲我們提供了許多有關AssetBundle的類去加載和調用,要注意一點就是,AssetBundle對象之間的依賴關係,加載完AssetBundle1後,如果有對象引用了AssetBundle2中的資源,Unity是不會自動幫我們去加載AssetBundle2,只能我們手動去加載AssetBundle2,否則會出現不可預估的錯誤。下面詳細介紹一下關於AssetBundle資源的加載管理,涉及到內存相關的知識。

        同樣,Unity也不會自動替我們卸載那些從場景中刪除了的對象資源,如果不手動卸載,那麼只會在特定的時候(場景切換?)纔會觸發卸載。

        AssetBundle.Unload(unloadAllLoadedObjectes)中參數的重要性:當參數爲true的時候,從該AssetBundle中引用的所有對象都會被卸載刪除掉,即使這個對象仍在當前場景中。現詳細說明該函數的參數爲true和false的區別。

        假設有一個材質M,在AssetBundle AB中,從AB中加載M後,當前場景的某個gameobject引用該材質M。如果在加載完成後調用了AB.Unload(true),那麼材質M將會從當前場景中刪除,引用M的gameobject因缺少材質而出現渲染錯誤。當使用AB.Unload(false)卸載AB,則僅卸載AB中的頭信息(header information),對於已經從AB中加載出來的任何物體(例如材質M)不做處理,切斷了材質M和AB之間的一種映射關係。如果AB隨後被重新加載,M跟新加載的AB之間也沒有任何關係,相反,新AB中的對象物體會被重新複製到內存中。而且即使使用AB.LoadAsset()加載材質M,因爲那個舊的材質M與新的AB沒有任何關係,Unity會從新的AB中複製一份材質M出來,此時新的和舊的材質M(二者完全相同)資源同時存在於內存中。(呵呵,舊的那個M成了孤魂野鬼了,只能等到卸載場景的時候或手動調用Resources.unloadAllUnusedObjects的時候才被卸載釋放)。

        對於大多數項目來說,應該調用AssetBundle.Unload(true)以確保內存中同一資源物體在同一時間只存在一份拷貝,因此有兩個方法可以考慮:1、我們很清楚在應用運行的那個時間點處該卸載臨時AssetBundle資源,比如場景之間的切換,或者在場景正在加載的時期,這種方法最簡單也最常見;2、對AssetBundle資源中的每個對象資源維護一個引用計數,只有當AssetBundle中所有對象的引用爲0時才調用AssetBundle.Unload(true),這樣卸載和加載AssetBundle就可以確保同一時間內存中沒有兩份或以上的副本存在,從而節省內存。

        但是,在不得不用AssetBundle.Unload(false)時,要考慮如下兩個方法:1、不管場景中還是代碼中,刪除了所有對不需要的物體和對象的引用之後,因爲內存中還未刪除該對象的內存佔用,所以要主動調用Resources.UnLoadUnusedAssets()。2、場景加載選項選擇爲 non-additively,這樣在下一個場景加載之前,Unity會自動爲我們清空當前場景中的對象物體,自動調用了Resources.UnLoadUnusedAssets()。

還有一個問題,當手機應用程序被用戶掛起(或程序在用戶註銷電腦)時,unity會終端對圖形渲染上下文(graphics context)的控制權,而當重新獲得圖形渲染上下文的控制權時,Unity必須將紋理、shader等渲染數據重新傳給GPU,如果在程序掛起之前這些從AssetBundle中加載出來的渲染數據被卸載或者不在可用,那麼渲染就會出現錯誤(洋紅色)。

關於AssetBundle資源分發給客戶端的兩種方式-------隨包傳輸和安裝後下載傳輸

    a) 隨包傳輸

        最簡單的一種方式,不需要額外的資源下載管理代碼,適用於更快速構建項目、較簡單版本迭代開發,將打包的AssetBundle資源直接保存在StreamingAssets中;另外,一個可更新的項目,通常在第一個版本發佈的時候,採用這種方式,這樣節省了用戶的初試安裝時間。我們介紹一下什麼是StreamingAssets。

        在用戶安裝應用的時候,將所有類型的數據寫入到存儲卡中最簡單的方法,就是在應用打包之前,把這些數據放在/Assets/StreamingAssets/文件夾下,打包在StreamingAssets/下的任何資源將會被複制到應用中,當然包括AssetBundle資源。應用安裝之後,程序運行時可通過Application.StreamingAssetsPath獲取StreamingAssets下資源的路徑,從而加載裏面的任何資源。Android平臺開發時,StreamingAssets文件夾下的資源在打包是通常會被壓縮,因此加載會比較慢,Unity版本的不同,壓縮算法可能有所不同,可以用解壓縮軟件(如7-zip)打開這個apk文件,看看StreamingAssets文件是否被壓縮了,如果壓縮了,那麼不建議你使用AssetBundle.LoadFromFile()加載,而是使用UnityWebRequest.GetAssetBundle,因爲後者可以在第一次運行時不僅會解壓資源,而且會將解壓之後的資源放在緩存中以期後續更快的調用速度,時間跟空間總是相互對立的,加載耗時下降了,必然會導致存儲空間的增大,這是優化的不變的哲學,如何把握這二者的關係,全看你的用心。

    b) 安裝後下載傳輸

        通常這種方式適合於手機移動端的資源更新,用戶不必重新下載整個應用apk,只需要下載應用的部分(通常只有幾十兆大小),節省了用戶流量和時間,更重要的,對開發者而言,避免了漫長而不確定的應用審覈過程,是一個雙贏的方法。接下來介紹Unity是如何在服務端下載更新的資源的。

        Unity爲我們提供了一個類:UnityWebRequest類,它裏面有實現的接口,幫我們從服務端下載資源到本地存儲中,如果下載的資源通過LZMA方式壓縮,那麼它會再次將該資源解壓或從新以LZ4的方式壓縮;如果下載的資源是LZ4的壓縮方式,則不再處理,如果緩存已滿,Unity將根據LRU(Least Recent Used)從緩存中刪除數據。

Unity內置的AssetBundle資源緩存系統

        AssetBundle資源被下載下來之後,必須保存在本地,以保證下次啓動時不再重複下載。UnityWebRequest的API有一個重載,其參數是AssetBundle的版本號,AssetBundle不會生成也不會保存版本號,緩存系統會獲取並保存最後一次傳遞給UnityWebRequest的版本號,作爲檢查本地是否有與當前傳遞的版本號匹配的AssetBundle,若沒有,Unity會下載一個新的副本並與這個新的版本號關聯。需要注意,AssetBundle的緩存系統的唯一標識是AssetBundle的名稱,緩存系統會將相同名稱的AssetBundle資源表示爲同一個AssetBundle。如何給AssetBundle一個合適的版本號(version number)參數呢?比如CRC值。上面提到過,應用程序在安裝過程中,會將StreamingAssets文件下的資源寫入到儲存卡並可以通過Application.StreamingAssetsPath獲取資源路徑來加載,而就在這時,Unity會將加載的資源解壓並放入緩存中,之後的操作就可以正常使用UnityWebRequest。

什麼樣的資源可以放在同一個AssetBundle包中呢?(打包策略的選擇)

        分組的關鍵是合理地將資源按照需求放在不同的AssetBundle包中,詳細的介紹在文末連接中有非常詳細的介紹,此處不再贅述。

 使用AssetBundle常見的一些問題

        1. 資源的重複加載

        如果對象資源沒有被打上AssetBundle的標籤,而這個對象資源被很多其他AssetBundle中的對象引用,那麼打包的最終結果就是,在很多的AssetBundle中都有這個沒有AssetBundle標籤的資源對象。造成同一資源多次打包,加載到內存中就是同一資源出現兩份或多份備份。

        有以下幾種解決方法,1)、打包到不同AssetBundle中的不同資源對象沒有依賴項。所有的依賴項資源都統一放在一個AssetBundle中,從而不需要複製這些依賴項資源。這種方法對於有許多共享依賴項資源的項目並不可行,因爲會產生許多零散的AssetBundle包。2)、獨立打包AssetBundle,這樣有共享資源的兩個AssetBundle就不會在同一時間被加載。兩個互相沒有依賴共享的AssetBundle,當然不需要考慮加載時還要不要加載另一個AssetBundle,因爲是他們是獨立的。3)、將自己依賴的所有資源對象都打包進自己的AssetBundle中,這就完全杜絕了重複資源加載的問題,但複雜性急劇上升,程序必須明確AssetBundle之間的依賴關係以確保在調用AssetBundle.LoadAssets()之前所有資源都已準備就緒。

        可以寫一個編輯器腳本,將每個AssetBundle的所有依賴項資源打包進這個AssetBundle中,或者確保兩個AssetBundle不會共享還沒有被打成AssetBundle包的依賴項(這個依賴項在打包的時候漏掉了,所以會重複打包,重複加載)。

        內存佔用和時間消耗之間對立統一的又一次體現,很明顯,將所有的依賴項資源都打進各自的AssetBundle中,包體中AssetBundle資源大了,但卻保證了應用的運行時緩存不會佔用過高,速度也就上去了,以空間換時間。

        2. 圖片圖集的重複加載

        確保圖集中的所有Sprite被打包到了同一個AssetBundle包中,如果Sprite被分散而非全部打包到多個AssetBundle包中,則這張圖集並不會被打包到AssetBundle中,造成圖集的重複加載。簡而言之就是,如果不把圖集中所有的Sprite打到AssetBundle包中,那麼每個AssetBundle都無法擁有這個圖集,在用到這張Sprite的時候,只能重複加載該圖集。

        3. 安卓包貼圖紋理壓縮格式的選擇

        安卓設備五花八門,幾乎每個手機廠家都有自己的系統CPU芯片和GPU等硬件配置,每款手機對渲染要求可能不盡而同,市面上的安卓機都支持OpenGL圖形渲染程序GPU的ES2,但ES2的一個缺點是無法支持貼圖的透明通道。此處不再贅述。

        最後,該文章部分來源於翻譯,但更多的是根據自己的理解翻譯過來的,如果您覺得哪裏表述欠佳,或有任何錯誤,歡迎留言,我們一起來探討。

參考鏈接:https://learn.unity.com/tutorial/assets-resources-and-assetbundles

發佈了6 篇原創文章 · 獲贊 1 · 訪問量 390
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章