淺談AB包
Unity資源管理
在Unity中,一般來說,資源加載方式主要分為Resources加載和AssetBundle加載,
Unity有個特殊檔案夾Resources,放在這個檔案夾下的資源可以通過Resources.Load()來直接加載,即Resources加載資源方式,
當獲得AssetBundle之后,也可以呼叫AssetBundle對應的API來加載資源,
什么是AB包
AB包全名AssetBundle(資源包),是一種Unity提供的用于存放資源的包,通過將資源分布在不同的AB包中可以最大程度地減少運行時的記憶體壓力,并且可以有選擇地加載內容,
為什么要用AB包
1、熱更新,(要熱更新需要確保AB包打出來的資源具有唯一性,且相同資源的AB包檢驗碼相同,)
2、Resources加載雖然簡單方便,但是也有很多問題:
- 對記憶體管理造成一定的負擔,
- 在打開應用時加載時間很長,
- Resources檔案夾下的所有資源統一合并到一個序列化檔案中(可以看成統一打一個大包,巨型AB包有什么問題它就有什么問題),對資源優化有一定的限制,
- 不建議大量使用Resources,
使用方法
打AB包:
public static AssetBundleManifest BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);
BuildAssetBundleOptions列舉型別的值轉化為二進制都是只有一位是1,其他位都是0,如UncompressedAssetBundle是0000 0000 0001,IgnoreTypeTreeChanges是0000 0100 0000,DisableLoadAssetByFileName是1000 0000 0000,
BuildAssetBundles底層會對傳入的BuildAssetBundleOptions值進行處理,根據二進制位數來判斷使用哪種策略構建AB包,因此如果在構建AB包時想要使用多種策略,用&連接即可,
BuildTarget引數用來選擇針對的平臺,因為AB包在不同平臺下是不兼容的,
設定資源AB包名:
AssetImporter.assetBundleName // AB包名
AssetImporter.assetBundleVariant // AB包變體名
獲取AB包方法:
AssetBundle.LoadFromFile(string path)
AssetBundle.LoadFromFileAsync(string path)
AssetBundle.LoadFromMemory(byte[] binary)
AssetBundle.LoadFromMemoryAsync(byte[] binary)
AssetBundle.LoadFromStream(Stream stream)
AssetBundle.LoadFromStreamAsync(Stream stream)
WWW.assetBundle
LoadFromFile是從檔案中加載AB包,它從一個給定的路徑來加載AB包,如果AB包是LZ4加載方式,它只會加載AB包的Header,之后需要什么資源再加載那部分的AB包chunk,極大的減少了記憶體占用,(LoadFromFileAsync是它的異步版本)
LoadFromMemory是從記憶體中加載AB包,它從記憶體中的byte[]中加載AB包,它會完整的把AB包加載出來,(LoadFromMemoryAsync是它的異步版本)
LoadFromStream是從流中加載AB包,它從一個Stream中加載AB包,跟LoadFromFile一樣,如果AB包是LZ4加載方式,它也是只會加載AB包的Header,(LoadFromStreamAsync是它的異步版本)
WWW是Unity中的跟網路相關的類,可以通過該類從網路中下載資源,之后加載成AB包,
加載資源方法:
AssetBundle.LoadAsset(string assetName, Type resType)
AssetBundle.LoadAssetAsync(string assetName, Type resType)
LoadAsset是同步方法,LoadAssetAsync是異步方法,
還有很多關于AssetBundle的方法,官方API中有詳細的介紹,
AB包變體
即AssetBundleVariant,AB包變體被用來支持定制化引數,允許不同AB包中的不同Object在加載和決議instance ID參考時顯示為相同Object,
從概念上講,允許兩個Object顯示為共享相同的GUID和Local ID,但實際上由Variant ID來區分,
簡而言之,實際上就是一個資源的分類標簽,
如同一圖片的高清和低清資源,同一模型的高精度和低精度資源,
在Unity編輯器右下角設定AB包名的后面就是設定AB包變體名,
BuildAssetBundleOptions:
None - 0:默認方式,使用LZMA壓縮演算法,該演算法壓縮后包體很小,但是加載的時候需要花費很長的時間解壓,第一次解壓之后,該包又會使用LZ4壓縮演算法再次壓縮,這就是為什么第一次加載時間長,之后加載時間就沒那么長了,(LZMA需要完整解壓之后才能加載包內資源,LZ4不需要完整解壓就可以加載包內資源,)
UncompressedAssetBundle - 1:不壓縮,雖然包體大,但是加載快,
DisableWriteTypeTree - 8:不包含TypeTree資訊,雖然可以使得AB包更小,但是對低版本不兼容,不建議使用,
DeterministicAssetBundle - 16:創建一個哈希來映射存盤在AB包里的物件的id,
ForceRebuildAssetBundle - 32:強制重建AB包,
IgnoreTypeTreeChanges - 64:當做增量構建檢測時,忽略type tree的變化,
AppendHashToAssetBundleName - 128:添加哈希到AB包名,
ChunkBasedCompression - 256:使用基于塊的LZ4壓縮演算法,
StrictMode - 512:如果在構建時有任何錯誤,則不允許構建成功,
DryRunBuild - 1024:干構建,
DisableLoadAssetByFileName - 4069:禁止AB包通過檔案名加載資源,
DisableLoadAssetByFileNameWithExtension - 8192:禁止AB包通過檔案擴展名加載資源,
AssetBundleStripUnityVersion:構建時從壓縮檔案和序列化檔案的header中移除Unity版本號,
LZMA和LZ4
LZMA是流壓縮方式(stream-based),流壓縮再處理整個資料塊時使用同一個字典,它提供了最大可能的壓縮率,但是只支持順序讀取,所以加載AB包時,需要將整個包解壓,會造成卡頓和額外記憶體占用,
LZ4是塊壓縮方式(chunk-based),塊壓縮的資料被分為大小相同的塊,并被分別壓縮,如果需要實時解壓隨機讀取,塊壓縮是比較好的選擇,LoadFromFile()和LoadFromStream()都只會加載AB包的Header,相對LoadFromMemory()來說大大節省了記憶體,
記憶體占用
下面是AB包再記憶體中的占用情況:
這是從網路中下載資源的記憶體占用情況,
下載的資源包括AB包、圖片、材質、影片、音頻等,以Stream的形式存盤在記憶體中,(AB包中也可以有圖片、材質、影片、音頻等資源)
之后通過加載AB包的方法,將AB包加載到記憶體中去,
AB包內的資源需要通過AssetBundle.Load()來加載到記憶體中,
對于GameObject來說,通常情況下需要對其進行改動,所以它是完全復制一份該資源來進行的實體化,也就是說,當AB包中的GameObject從記憶體中卸載后,實體化的GameObject不會因此丟失,并且對實體化物件的修改不會影響到GameObject資源,
對于Shader和Texture來說,通常情況下不需要對其進行改動,所以它是通過參考來進行的實體化,也就是說,當AB包中的Shader和Texture資源從記憶體中卸載后,實體化的Shader和Texture會出現資源丟失的情況,并且對實體化物件的修改會影響到Shader和Texture資源,
對于Material和Mesh來說,有時候可能需要對其進行改動,所以它是通過參考+復制來進行的實體化,也就是說,當AB包中的Material和Mesh資源從記憶體中卸載后,實體化的Material和Mesh會出現資源丟失的情況,并且對實體化物件的修改不會影響到Material和Mesh資源,
總結大致流程為:
AB包先要從硬碟或者網路中加載到記憶體中,然后將AB包內的每一份資源加載到記憶體中,再之后在記憶體中實體化這些資源,每種資源有其自己不同的實體化方式,卸載資源的時候需要注意,
AB包內部結構
AssetBundleFileHeader:記錄了版本號、壓縮等主要描述資訊,
AssetFileHeader:包含一個檔案串列,記錄了每個資源的name、offset、length等資訊,
Asset1:
- AssetHeader:記錄了TypeTree大小、檔案大小、format等資訊,
- TypeTree(可選,有不要TypeTree的構建方式):記錄了Asset物件的class ID,Unity可以用class ID來序列化和反序列化一個類,(每個class對應了一個ID,如0是Object類,1是GameObject類等,具體可在Unity官網上查詢,)
- ObjectPath:記錄了path ID(資源唯一索引ID)等,
- AssetRef:記錄了AB包對外部資源對參考情況,
Asset2…
.manifest
這是AB包對應的.manifest檔案,
ManifestFileVersion: 0 # 檔案版本
CRC: 2657307167 # CRC校驗碼
Hashes: # 哈希
AssetFileHash: # AB包中所有資源的哈希,可用于增量更新檢測
serializedVersion: 2 # Unity序列化版本
Hash: 717e408ba50ee41b0960161fd2d5a827
TypeTreeHash: # AB包中所有型別的哈希,可用于增量更新檢測
serializedVersion: 2 # Unity序列化版本
Hash: 8d552bf2f5bdba1177c938cb98ca6f2f
HashAppended: 0
ClassTypes: # TypeTree
- Class: 1 # GameObject
Script: {instanceID: 0}
- Class: 21 # Material
Script: {instanceID: 0}
- Class: 28 # Texture2D
Script: {instanceID: 0}
- Class: 48 # Shader
Script: {instanceID: 0}
- Class: 114 # MonoBehaviour
Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3}
- Class: 114 # MonoBehaviour
Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3}
- Class: 114 # MonoBehaviour
Script: {fileID: -1200242548, guid: f70555f144d8491a825f0804e09c671c, type: 3}
- Class: 114 # MonoBehaviour
Script: {fileID: -146154839, guid: f70555f144d8491a825f0804e09c671c, type: 3}
- Class: 114 # MonoBehaviour
Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3}
- Class: 114 # MonoBehaviour
Script: {fileID: 1297475563, guid: f70555f144d8491a825f0804e09c671c, type: 3}
- Class: 114 # MonoBehaviour
Script: {fileID: 11500000, guid: 20e8969313b8e4614b498f042e99683a, type: 3}
- Class: 114 # MonoBehaviour
Script: {fileID: 11500000, guid: c86dbe77db44a434bb15895563508b65, type: 3}
- Class: 114 # MonoBehaviour
Script: {fileID: 11500000, guid: 1a7e2f4cb82d9b94a91270d550c880c0, type: 3}
- Class: 115 # MonoScript
Script: {instanceID: 0}
- Class: 128 # Font
Script: {instanceID: 0}
- Class: 198 # ParticleSystem
Script: {instanceID: 0}
- Class: 199 # ParticleSystemRenderer
Script: {instanceID: 0}
- Class: 213 # Sprite
Script: {instanceID: 0}
- Class: 222 # CanvasRenderer
Script: {instanceID: 0}
- Class: 224 # RectTransform
Script: {instanceID: 0}
- Class: 687078895 # SpriteAtlas
Script: {instanceID: 0}
Assets: # 包含資源
- Assets/Bundle/.../a.prefab
- Assets/Bundle/.../b.prefab
- Assets/Bundle/.../c.spriteatlas
Dependencies: # AB包依賴
- /Users/apple/.../AssetBundles/Android/q
- /Users/apple/.../AssetBundles/Android/w
- /Users/apple/.../AssetBundles/Android/e
- /Users/apple/.../AssetBundles/Android/r
- /Users/apple/.../AssetBundles/Android/t
特殊路徑
Resources
對應的是Resources特殊檔案夾路徑,(只讀)
在Unity下對應為:/Assets/Resources,
Application.streamingAssetsPath
對應的是StreamingAsset檔案夾路徑,(只讀)
在Unity下對應為:/Assets/StreamingAssets,
在Android下對應為:jar:file:///data/app/xxx.apk!/assets,
在iOS下對應為:Application/…/xxx.app/Data/Raw,
Application.persistentDataPath
對應的是應用持久化資料存盤檔案夾路徑,應用更新、覆寫安裝時,這里的資料都不會被清除,(可讀可寫)
在Unity下對應為:/該Unity專案檔案夾路徑,
在Android下對應為:/…/data/應用名/files,
在iOS下對應為:Application/…/Documents,iOS還會自動將persistentDataPath路徑下的檔案上傳到iCloud,會占用用戶的iCloud空間,如果persistentDataPath路徑下的檔案過多,蘋果審核可能被拒,所以,iOS平臺,有些資料得放temporaryCachePath路徑下,
Application.dataPath
對應的是應用Asset檔案夾路徑,(只讀,Android不可讀,因為改目錄指向的是個.apk檔案,而不是目錄)
在Unity下對應為:/Assets,
在Android下對應為:/data/app/…/xxx.apk,
在iOS下對應為:Application/…/xxx.app/Data,
Application.temporaryCachePath
對應的是應用臨時資料快取檔案夾路徑,(只讀)
在Unity下對應為:/該Unity專案檔案夾路徑,
在Android下對應為:/…/data/應用名/cache,
在iOS下對應為:Application/…/Library/Caches,
依賴問題
依賴問題,通俗的話來說就是A包中某資源用了B包中的某資源,然而如果A包加載了,B包沒有加載,這就會導致A包中的資源出現丟資源的現象,
在Unity5.0后,BuildAssetBundleOptions.CollectDependencies永久開啟,即Unity會自動檢測物體參考的資源并且一并打包,防止資源丟失遺漏的問題出現,
因為這個特性,有些情況下,如果沒指定某公共資源的存放在哪個AB包中,這個公共資源就會被自動打進參考它的AB包中,所以出現多個不同的AB包中有重復的資源存在的現象,這就是資源冗余,
這種情況下,哪怕資源是一模一樣,也無法進行合并優化,
要防止資源冗余,就需要明確指出資源存放在哪個AB包中,形成依賴關系,所以對于一些公共資源,建議單獨存放在一個AB包中,
在加載的時候,如果AB包之間相互依賴,那么加載一個AB包中的資源時,先需要加載出另一個AB包的資源,這樣就會導致不必要的消耗,所以說盡可能地減少AB包之間的依賴,并且公共資源盡量提前加載完成,
細粒度問題
細粒度問題即每個AB包分別放入多少資源的問題,一個好的策略至關重要,
加載資源時,先要加載AB包,再加載資源,如果AB包使用了LZMA或LZ4壓縮演算法,還需要先給AB包解壓,
| AB包數量較多,包內資源較少 | AB包數量較少,包內資源較多 |
|---|---|
| 加載一個AB包到記憶體的時間短,玩家不會有卡頓感,但每個資源實際上加載時間變長, | 加載一個AB包到記憶體的時間較長,玩家會有卡頓感,但之后包內的每個資源加載很快, |
| 熱更新靈活,要更新下載的包體較小, | 熱更新不靈活,要更新下載的包體較大, |
| IO次數過多,增大了硬體設備耗能和發熱壓力, | IO次數不多,硬體壓力小, |
簡單策略:
- 經常更新和不經常更新的物件拆分到不同的AB包中,
- 同時加載的物件放在一個AB包中,
- 不可能同時加載的物件拆分到不同的AB包中,
- 根據專案邏輯功能來分組打AB包,
- 根據同一型別物件來分組打AB包,
- 公共資源和非公共資源拆分到不同的AB包中,
卸載問題
當呼叫Resources.UnloadAsset()時,雖Object被銷毀,但Instance ID被保留且包含有效的GUID和Local ID參考,
當呼叫AssetBundle.Unload(true)時,不僅Object被銷毀,而且Instance ID的GUID和Local ID參考變無效,
當呼叫AssetBundle.Unload(false)時,雖Object不被銷毀,但Instance ID的GUID和Local ID參考變無效,場景中的物體會與該AB包分離鏈接,即該物體的instance ID參考的GUID和Local ID會斷開參考,無法再通過該instance ID找到GUID和Local ID,
如果再次加載該AB包時,分離了鏈接的物體不會受該新加載的AB包管理,因此如果不注意的話可能會導致一些不可控的問題,Unity中有Resources.UnloadUnusedAssets()方法可以很好地解決這個問題,
AB包的加密
因為AB包存放著游戲的各種資源,所以如果AB包不加密,那么別人在得到AB包的時候可以直接看到AB包內所有的資源,經過一定特殊操作后可以直接從AB包中匯出圖片、音頻、影片,甚至可以在Unity中直接實體化出來另存為Prefab,
加密思路如下:
1、在構建完AB包后,可以將AB包中的內容以byte[]形式讀取,
2、之后選用任意加密方式對該byte[]加密,
3、加密完后重新寫入AB包中,
4、AB包加密完成,
這樣對AB包加密之后,如果使用AssetBundle.LoadFromFile()來加載加密的AB包是會報錯的,因為Unity以及無法識別加密過后的內容了,這樣也就防止了別人隨意對AB包進行的讀取和加載,保證了資源的安全性,
解密思路如下:
1、先以byte[]形式讀取AB包中的內容,
2、之后使用對應的解密演算法對該byte[]進行解密,
3、解密過后的byte[]通過AssetBundle.LoadFromMemory()來進行加載,
4、AB包加載完成,
總的來說,這種二進制加密AB包的方式雖然有效,但是加載時間和記憶體占用是一個需要考慮的問題,很多時候選擇不進行加密,一方面原因是因為需要多占用一份記憶體的問題,代價過大,雖然說從byte[]加載成AB包之后,byte[]可以從記憶體中釋放,但是在加載的程序中還是會有一個記憶體占用的巔峰,
另一種簡單的加密方式,即可以實作直接手段加載不出AB包,而且相對上述二進制加密AB包方式加載更快、耗費更小,
本質是通過在AB包中添加偏移量來實作加密,
public static AssetBundle LoadFromFile(string path, uint crc, ulong offset);
AssetBundle.LoadFromFile()的第三個引數是AB包內容的byte偏移量,也就是說從offset個byte開始讀取AB包的內容,
因此如果在構建完AB包之后,在AB包前插入N個隨機byte,那么此時想要加載該AB包,如不知道這個N值,則是無法成功讀取和加載AB包的,這也就實作了加密,
從Stream中加載
AssetBundle.LoadFromStream()不像AssetBundle.LoadFromMemory()會多占用一份記憶體,
public static AssetBundle LoadFromStream(Stream stream, uint crc, uint managedReadBufferSize)
這是從托管流中加載AB包的方法,它跟LoadFromFile()一樣,只會讀取AB包的頭檔案,
使用Stream加載的限制:
1、AB包資料必須是從Stream的0位置開始,
2、當從AssetBundle資料的末尾開始并嘗試讀取資料時,Stream實作必須回傳讀取的0位元組且不引發例外,
3、Stream必須是可讀(CanRead回傳true)和可搜尋(CanSeek回傳true)的,
4、可以從任何Unity執行緒中呼叫Seek()和Read(),
CRC校驗
AB包加載資源的完整方法實際上是AssetBundle.LoadFromFile(string path, uint crc, ulong offset),三個引數,其中第二個引數就是CRC校驗符,
每個AB包的.manifest檔案中也有CRC校驗符,用于校驗資料完整性,
各種ID
序列化后,資源用GUID和Local ID管理,
GUID對應Asset,GUID存在.meta檔案中,提供了檔案特定位置的抽象,是一種映射,無需關心資源在磁盤上的存放位置,
Local ID對應Asset內的每一個Object,(Asset中)
雖然GUID和Local ID比較好用,但是畢竟因為存在磁盤上,讀取比較耗時,因此Unity快取一個instance ID對應Object,通過instance ID快速找到Object,instance ID是一種快速獲取物件實體的ID,包含著對GUID和Local ID的參考,決議instance ID可以快速回傳instance表示的已加載物件,如果為加載目標物件,則可以將檔案GUID和Local ID決議為物件源資料,從而允許Unity即時加載物件,每次AB包重新加載時,都會為每個物件創建新的instance ID,
總結
沒有最好的打AB包方式,只有最適合專案的打AB包方式,
參考資料
https://docs.unity3d.com/ScriptReference/AssetBundle.html
https://docs.unity3d.com/Manual/ClassIDReference.html
https://www.xuanyusong.com/?s=AssetBundle
https://blog.csdn.net/lodypig/category_6315960.html
https://blog.csdn.net/BillCYJ/article/details/99712313
https://learn.unity.com/tutorial/assets-resources-and-assetbundles#5c7f8528edbc2a002053b5a6
后記
這是很早之前學習AB包的時候寫的筆記,有很多地方理解不到位,歡迎各位進行指正和討論,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/247238.html
標籤:其他
下一篇:動態規劃-數字三角形模型
