一.問題
首先,這裡說明一下,我這邊的GameObject有點籠統,就是表達的是遊戲中的具體例項。
二.概念
1)Asset是什麼?
遊戲中具體的資源,像texture,mesh,material,shader,script等,實實在在的遊戲專案資料夾中所需要堆放的資源。比如,var obj = Resource.Load<GameObject>("Prefabs/testItem"),這個obj就是Asset。
2)GameObject是什麼?
var gameItem= Instantiate(obj),這個gameItem就是可以存在於遊戲的實際場景中(這個比較簡單,不多說了)。GameObject是遊戲中實際使用的物件(就是你會在螢幕中實際看到的),是由Asset例項化後的物件。本質上其實還是Asset的衍變,是對部分Asset的引用和複製出來的新東西,其本質還是Asset。
3)AssetBundle是什麼?
由上述可知,我們在遊戲中生成實際的物體,需要Asset。而Asset,比如一張圖片,也是一個Asset,實際大小1M,遊戲中這種圖片很多,那就輕輕鬆鬆幾百M,幾個G的Asset,都是很有可能的。作為程式設計師,對於這種“原汁原味”拿過來肯定不行。比如我們工作中把檔案什麼的發給同事的時候都知道壓縮一下,可以傳輸的過程中小一些。當然了,我們在遊戲開發中使用Asset,也是需要類似的。於是,就推出了AssetBundle這一概念。當然我們推出AssetBundle,遠不止壓縮這一需求。但是你需要知道,主要是為了更好的傳輸,還有比如減少資源大小,利於網路那邊的傳輸,方便載入。
簡而言之,AssetBundle就是為了讓遊戲專案中大量Asset適應實際遊戲執行時而被壓縮後的一種二進位制檔案。
三.分析
1)Asset和GameObject的關係?
①複製+引用關係
Instaniate一個Prefab(Asset),是對Asset進行clone(複製)+引用結合的過程。GameObject,transform是clone的。其他mesh/texture/material/shader等,這些都是純引用的。引用的Asset物件不會被複制,只是一個簡單的指標指向已經load的Asset物件。專門要提一下Script Asset,Unity裡每個Script都是一個封閉的class定義,並沒有寫呼叫程式碼。光class是不會工作的。其實Unity引擎就是那個呼叫程式碼。clone一個script asset等於new一個class例項,例項才會工作 ,把它掛到Unity主執行緒的呼叫鏈去,class例項裡的update和start才會被執行。多個物體掛同一個指令碼,其實就是多個物體掛了掛了那個指令碼的類的例項。在new class過程中,資料區是複製的,程式碼區是共享的,算是一種複製+引用關係。引用關係的話,會有一對一,一對多,多對一的關係。如下圖:
②釋放
如果你Destroy(GameObject,這個是遊戲中具有例項),只是釋放了clone的asset,引用的asset並不會被幹掉。還有因為destroy並不知道有沒有被別的asset引用。但是如果你想把asset也釋放,有兩種方案。
I. Resources.UnloadUnusedAssets()
釋放當前所有的沒有被引用的(無用)asset,但是不能保證釋放掉當前的被引用的資源(因為可能還被其他資源引用,就不會去釋放了)。缺點:非同步,會卡。由於Unity資源的相互引用關係比較複雜,想要明確判斷某一資源不存在引用關係是有一定難度的,並且,如果我 們想要釋放的資源存在隱形的引用關係,UnloadUnUsedAssets將會無視這個資源而無任何反饋。根據實戰來看,最佳使用的時機是在場景切換進入新的場景後,Unity場景關閉會有效的銷燬所有的物件和所有程式碼的引用,即在新場景開頭最為穩妥。必要時加上GC.Collect().
II.Resouce.UnLoad(obj)
釋放當前例項的所有被引用的asset(不管這個asset是否還有被引用,所以,風險很大,除非保證確定被他引用的資源沒有再被其他資源引用,一般用於單獨的一張紋理釋放)缺點:風險太大,容易被報:UnloadAsset may only be used on individual assets and can not be used on GameObject's / Components or AssetBundles,不能用作解除安裝GameObject,只能用於紋理釋放。不然會報這個錯,我也不知道為啥,有知道的告我一下,額,還有就是這個方法很少用,應用的話,也只是對紋理釋放。
對於這種使用,對於資源大的且無引用的可以使用,如果資源消耗不大,可以等到場景切換,使用I中 Resources.unloadUnusedAssets方案。
III.GameObject.DestroyImmediate(asset,true)
代替上述方案,可以針對解除安裝asset(好像還可以直接使用GameObject.Destroy(asset) ,也可以直接使用。(待驗證))
IV.AssetBundle.unload(true) !!!慎用
這個也可以解除安裝asset,但是解除安裝的是這個assetBundle裡面的所有的asset。(後面詳說)
2)AssetBundle和Asset的關係
①包含,依賴關係
一個AssetBundle中可以包含一個或多個Asset。一個Asset依賴於AssetBundle。
②釋放
AssetBundle的釋放只能通過以下兩種方式釋放。即使系統在載入新場景的時候所有的記憶體物件都會被自動銷燬,包括你用AssetBundle.load載入的物件和Instaniate克隆的,但是不包括AssetBundle檔案本身的記憶體映象,那個必須要用Unload來釋放,用.Net術語說該資源是非託管的。
I.AssetBundle.Unload(false) 使用頻率較多
用AssetBundle.Load載入需要的asset之後應該立即使用unlaod(false),釋放assetbundle檔案本身的記憶體映象,但不銷燬該assetBundle載入過的asset物件。(儘量釋放一部分記憶體,大多數遊戲這麼做)
II.AssetBundle.Unload(true)
釋放該assetBundle檔案映象並釋放該assetBundle所有loaded的asset記憶體物件。(風險很大,因為一般不太能確定是否該loaded的asset是否還被其他資源引用)。
四.例項測試
AssetBundle和Asset 專案工程中大小分析
①首先,準備10個一樣大小的texture(為了測試結果更加準備,每個圖1.33M)
texture 的原大小,即Asset本身的大小:1.33M *10 = 13.3M
②LZ4和LZMA打包方式案例對比
I.BuildAssetBundleOptions.None 打包方式,該為預設壓縮,即LZMA,在使用AssetBundle之前需要解壓縮。使用LAMA格式壓縮的AssetBundle的包體最小
(高壓縮比),但是會增加相應的解壓縮時間和記憶體。
BuildPipeline.BuildAssetBundles(dir, BuildAssetBundleOptions.None,BuildTarget.StandaloneWindows64);
以上述方式打包完之後 每張 texture :613K *10 = 6M
執行下面程式碼。(為方便資料對比,是在找不到大的texture了)
使用AssetBundle.LoadFromFile 載入那10個AssetBundle之後的記憶體顯示
對比上2張圖的消耗,大概消耗了13.6M的Unity記憶體,0.5M其他記憶體。因為是十張紋理,原紋理沒有打包之前的大小大概是1.33m,也就是說,大概技術釋放的是原Asset的大小。
再Load其中的Asset
由上圖可知,申請的是Unity記憶體爆增100M左右,Mono的記憶體也相應的增加了。
II.ChunkBasedCompression,即LZ4壓縮方式,壓縮比一般,壓縮後的包體較大,但是解壓速度快,消耗記憶體小。
BuildPipeline.BuildAssetBundles(dir, BuildAssetBundleOptions.ChunkBasedCompression,BuildTarget.StandaloneWindows64);
以上述方式打包完之後 每張texture :1110K*10=11100K,11M左右
再執行同上一致的Load指令碼.
AssetBundle,Asset,GameObject 載入中的記憶體對比
同上述步驟一致,給出LoadAssetBundle,即AssetBundle.LoadFromFile後的記憶體大小
大概消耗2M記憶體。(上述LAMA方式打出的包是23.5M,相差10倍啊,果斷使用這種方式,我們專案也是這種方式)
再給出LoadAsset之後的記憶體圖
LoadAsset之後,Unity記憶體,Mono記憶體都增加很多
III.總結對比,I載入的時候LoadAssetBundle消耗明顯大於II方式10倍(甚至不止,因為總量越大,差距就越大)。移動遊戲中的記憶體多麼珍貴大家懂得。使用ChunkBasedCompression,即LZ4壓縮,更為划算。因為寧願犧牲一點包體大小,也要消耗記憶體小一些。(我們遊戲專案是這個需求,具體還是看專案吧)
當然還有其他打包選項,各有利弊,具體看遊戲實際情況需求。我這裡只是對比出了告訴你打包選項會決定你的打出的資源包體大小和遊戲中載入消耗記憶體。很重要。
具體打包選項BuildAssetBundleOptions參考:https://blog.csdn.net/AnYuanLzh/article/details/81485762
補充:由上,我們知道打包時壓縮方式會導致打出的包體和載入時的消耗都不同。載入時候的載入方式我們這邊使用AssetBundle.CreateFromFile直接載入AssetBundle,Unity其實還提供了WWW載入AssetBundle的方式。但是這裡不作詳述了(講不完...)。
五.總結
由上可知,文章雖說AB,Asset,GameObject三者聯絡,但是GameObject主要是由Asset例項化而來,GameObject是Asset的引用和複製的關係(主要引用),這個也可以說是Asset的一種。問題也就可以簡化為AB和Asset之間的關係。
由上圖,再總結一下,打包方式不同,載入方式不同,造成的消耗不同。即,如果想優化遊戲中的資源,需要注意打包AB的方式,以及載入AB的方式。當然,還要注意AB的解除安裝,和Asset的解除安裝。
六.參考
關於載入AssetBundle和載入Asset的區別,詳情就不多說了,見UWA:https://blog.uwa4d.com/archives/ABTheory.html
打包加密壓縮演算法區別參考:https://www.cnblogs.com/murongxiaopifu/p/5629415.html#autoid-3-3-0
附:測試Demo原始碼:連結:https://pan.baidu.com/s/1ZHPoQbuxgdUVh9PGbz6ArA 提取碼:7lkv