轉自:http://www.cnblogs.com/ybgame/p/3973177.html
這篇文章從AssetBundle的打包,使用,管理以及內存占用各個方面進行了比較全面的分析,對AssetBundle使用過程中的一些坑進行填補指引以及噴!
AssetBundle是Unity推薦的資源管理方式,官方列舉了諸如熱更新,壓縮,靈活等等優點,但AssetBundle的坑是非常深的,很多隱藏細節讓你使用起來需要十分謹慎,一不小心就會掉入深坑,打包沒規划好,20MB的資源“壓縮”到了30MB,或者大量的包導致打包以及加載時的各種低效,或者莫名其妙地丟失關聯,或者內存爆掉,以及各種加載失敗,在網上研究了大量關於AssetBundle的文章,但每次看完之后,還是有不少疑問,所以只能通過實踐來解答心中的疑問,為確保結果的准確性,下面的測試在編輯器下,Windows,IOS下都進行了測試比較。
首先你為什么要選擇AssetBundle,縱使他有千般好處,但一般選擇AssetBundle的原因就是,要做熱更新,動態更新游戲資源,或者你Resource下的資源超過了它的極限(2GB還是4GB?),如果你沒有這樣的需求,那么建議你不要使用這個壞東西,鬧心~~
當你選擇了AssetBundle之后,以及我開始噴AssetBundle之前,我們需要對AssetBundle的工作流程做一個簡單的介紹:
AssetBundle可以分為打包AssetBundle以及使用AssetBundle
打包需要在UnityEditor下編寫一些簡單的代碼,來取出你要打包的資源,然后調用打包方法進行打包
Object obj = AssetDatabase.LoadMainAssetAtPath("Assets/Test.png");
BuildPipeline.BuildAssetBundle(obj, null,
Application.streamingAssetsPath + "/Test.assetbundle",
BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets
| BuildAssetBundleOptions.DeterministicAssetBundle, BuildTarget.StandaloneWindows);

在使用的時候,需要用WWW來加載Bundle,然后再用加載出來的Bundle來Load資源
WWW w = new WWW("file://" + Application.streamingAssetsPath + "/Test.assetbundle");
myTexture = w.assetBundle.Load("Test");
【一,打包】
接下來我們來看一下打包:
1.資源的搜集
在打包前我們可以通過遍歷目錄的方式來自動化地進行打包,可以有選擇性地將一些目錄打包成一個Bundle,這塊也可以用各種配置文件來管理資源,也可以用目錄規范來管理
我這邊是用一個目錄規范對資源進行大的分類,分為公共以及游戲內,游戲外幾個大模塊,然后用一套簡單命名規范來指引打包,例如用OBO(OneByOne)作為目錄后綴來指引將目錄下所有資源獨立打包,默認打成一個包,用Base前綴來表示這屬於公共包,同級目錄下的其他目錄需要依賴於它
使用Directory的GetFiles和GetDirectories可以很方便地獲取到目錄以及目錄下的文件
Directory.GetFiles("Assets/MyDirs", "*.*", SearchOption.TopDirectoryOnly);
Directory.GetDirectories(Application.dataPath + "/Resources/Game", "*.*", SearchOption.AllDirectories);
2.資源讀取
GetFiles搜集到的資源路徑可以被加載,加載之前需要判斷一下后綴是否.meta,如果是則不取出該資源,然后將路徑轉換至Assets開頭的相對路徑,然后加載資源
string newPath = "Assets" + mypath.Replace(Application.dataPath, "");
newPath = newPath.Replace("\\", "/");
Object obj = AssetDatabase.LoadMainAssetAtPath(newPath);
3.打包函數
我們調用BuildPipeline.BuildAssetBundle來進行打包:
BuildPipeline.BuildAssetBundle有5個參數,第一個是主資源,第二個是資源數組,這兩個參數必須有一個不為null,如果主資源存在於資源數組中,是沒有任何關系的,如果設置了主資源,可以通過Bundle.mainAsset來直接使用它
第三個參數是路徑,一般我們設置為 Application.streamingAssetsPath + Bundle的目標路徑和Bundle名稱
第四個參數有四個選項,BuildAssetBundleOptions.CollectDependencies會去查找依賴,BuildAssetBundleOptions.CompleteAssets會強制包含整個資源,BuildAssetBundleOptions.DeterministicAssetBundle會確保生成唯一ID,在打包依賴時會有用到,其他選項沒什么意義
第五個參數是平台,在安卓,IOS,PC下,我們需要傳入不同的平台標識,以打出不同平台適用的包,
注意,Windows平台下打出來的包,不能用於IOS
在打對應的包之前應該先選擇對應的平台再打包
4.打包的決策
在打包的時候,我們需要對包的大小和數量進行一個平衡,所有資源打成一個包,一個資源打一個包,都是比較極端的做法,他們的問題也很明顯,更多情況下我們需要靈活地將他們組合起來
打成一個包的缺點是加載了這個包,我們不需要的東西也會被加載進來,占用額外內存,而且不利於熱更新
打成多個包的缺點是,容易造成冗余,首先影響包的讀取速度,然后包之間的內容可能會有重復,且太多的包不利於資源管理
哪些模塊打成一個包,哪些模塊打成多個包,需要根據實際情況來,例如游戲中每個怪物都需要打成一個包,因為每個怪物之間是獨立的,例如游戲的基礎UI,可以打成一個包,因為他們在各個界面都會出現
PS.想打包進AssetBundle中的二進制文件,文件名的后綴必須為“.bytes”
【二,解包】
解包的第一步是將Bundle加載進來,new一個WWW傳入一個URL即可加載Bundle,我們可以傳入一個Bundle的網址,從網絡下載,也可以傳入本地包的路徑,一般我們用file://開頭+Bundle路徑,來指定本地的Bundle,用
http://或
https://開頭+Bundle網址來指定網絡Bundle
string.Format("file://{0}/{1}", Application.streamingAssetsPath, bundlePath);
在安卓下路徑不一樣,如果是安卓平台的本地Bundle,需要用jar:file://作為前綴,並且需要設置特殊的路徑才能加載
string.Format("jar:file://{0}!/assets/{1}", Application.dataPath, bundlePath);
傳入指定的URL之后,我們可以用WWW來加載Bundle,加載Bundle需要消耗一些時間,所以我們一般在協同里面加載Bundle,如果加載失敗,你可以在www.error中得到失敗的原因
IEnumerator LoadBundle(string url)
{
WWW www = = new WWW(url);
yield return www;
if (www.error != null)
{
Debug.LogError("Load Bundle Faile " + url + " Error Is " + www.error);
yield break;
}
//Do something ...
}
除了創建一個WWW之外,還有另一個方法可以加載Bundle,
WWW.LoadFromCacheOrDownload(url, version),使用這個函數對內存的占用會小很多,但每次重新打包都需要將該Bundle對應的版本號更新(第二個參數version),否則可能會使用之前的包,而不是最新的包,LoadFromCacheOrDownload會將Bundle從網絡或程序資源中,解壓到一個磁盤高速緩存,一般可以理解為解壓到本地磁盤,如果本地磁盤已經存在該版本的資源,就直接使用解壓后的資源。對於AssetBundle所有對內存占用的情況,后面會有一小節專門介紹它
LoadFromCacheOrDownload會記錄所有Bundle的使用情況,並在適當的時候刪除最近很少使用的資源包,它允許存在兩個版本號不同但名字一樣的資源包,這意味着你更新這個資源包之后,如果沒有更新代碼中的版本號,你可能取到的會是舊版本的資源包,從而產生其他的一些BUG。另外,當你的磁盤空間不足的時候(硬盤爆了),LoadFromCacheOrDownload只是一個普通的new WWW!后面關於內存介紹的小節也會對這個感嘆號進行介紹的
拿到Bundle之后,我們就需要Load里面的資源,有Load,LoadAll以及LoadAsyn可供選擇
//將所有對象加載資源
Object[] objs = bundle.LoadAll();
//加載名為obj的資源
Object obj = bundle.Load("obj");
//異步加載名為resName,類型為type的資源
AssetBundleRequest res = bundle.LoadAsync(resName, type);
yield return res;
var obj = res.asset;
我們經常會把各種游戲對象做成一個Prefab,那么Prefab也會是我們Bundle中常見的一種資源,使用Prefab時需要注意一點,
在Bundle中加載的Prefab是不能直接使用的,它需要被實例化之后,才能使用,而對於這種Prefab,實例化之后,這個Bundle就可以被釋放了
//需要先實例化
GameObject obj = GameObject.Instantiate(bundle.Load("MyPrefab")) as GameObject;
對於從Bundle中加載出來的Prefab,可以理解為我們直接從資源目錄下拖到腳本上的一個Public變量,是未被實例化的Prefab,只是一個模板
如果你用上面的代碼來加載資源,當你的資源慢慢多起來的時候,你可能會發現一個很坑爹的問題,你要加載的資源加載失敗了,例如你要加載一個GameObject,但是整個加載過程並沒有報錯,而當你要使用這個GameObject的時候,出錯了,而同樣的代碼,我們在PC上可能沒有發現這個問題,當我們打安卓或IOS包時,某個資源加載失敗了。
出現這種神奇的問題,首先是懷疑打包的問題,包太大了?刪掉一些內容,不行!重新打一個?還是不行!然后發現來來回回,都是這一個GameObject報的錯,難道是這個GameObject里面部分資源有問題?對這個GameObject各種分析,把它大卸八塊,處理成一個很簡單的GameObject,還是不行!難道是名字的問題?把這個GameObject的名字改了一下,可以了!
本來事情到這就該結束了,但是,這也太莫名其妙了吧!而且,最重要的是,哥就喜歡原來的名字!!把這個資源改成新的名字,怎么看怎么變扭,怎么看都沒有原來的名字好看,所以繼續折騰了起來~
首先單步跟蹤到這個資源的Load,資源被成功Load出來了,但是Load出來的東西有點怪怪的,明顯不是一個GameObject,而是一個莫名其妙的東西,可能是Unity生成的一個中間對象,也許是一個索引對象,反正不是我要的東西,打包的GameObject怎么會變成這個玩意呢?於是在加載Bundle的地方,把Bundle LoadAll了一下,然后查看這個Bundle里面的內容
在這里我們可以看到,有一個叫RoomHallView和RoomMainView的GameObject,並且,LoadAll之后的資源比我打包的資源要多很多,看樣子所有關聯到的資源都被自動打包進去了,數組的427是RoomHallView的GameObject,而431才是RoomMainView的GameObject。可以看到名字叫做RoomMainView和RoomHallView的對象有好幾個,GameObject,Transform,以及一個只有名字的對象,它的類型是一個ReferenceData。
仔細查看可以發現,RoomHallView的GameObject是排在數組中所有名為RoomHallView對象的最前面,而RoomMainView則是ReferenceData排在前面,當我們Load或者LoadAsyn時,是一次數組的遍歷,當遍歷到名字匹配的對象時,則將對象返回,LoadAsyn會對類型進行匹配,但由於我們傳入的是Object,而幾乎所有的對象都是Object,所以返回的結果就是第一個名字匹配的對象
在Load以及LoadAsyn時,除了名字,把要加載對象的類型也傳入,再調試,原來的名字也可以正常被讀取到了,這個細節非常的坑,因為在官網並沒有提醒,而且示例的sample也沒有說應該注意這個地方,並且出現問題的幾率很小。所以一旦出現,就坑死了
bundle.Load("MyPrefab", typeof(GameObject))
另外,
不要在IOS模擬器上測試AssetBundle,你會收到bad url的錯誤
【三,依賴】
依賴和打包息息相關,之所以把依賴單獨分開來講,是因為這玩意太坑了.......
【1.打包依賴】
在我們打包的時候,將兩個資源打包成單獨的包,那么兩個資源所共用的資源,就會被打包成兩份,這就造成了冗余,所以我們需要將公共資源抽出來,打成一個Bundle,然后后面兩個資源,依賴這個公共包,那么還有另外一種方法,就是把它們三打成一個包,但這不利於后期維護
我們使用BuildPipeline.PushAssetDependencies()和BuildPipeline.PopAssetDependencies()來開啟Bundle之間的依賴關系,當我們調用PushAssetDependencies之后,會開啟依賴模式,當我們依次打包 A B C時,如果A包含了B的資源,B就不會再包含這個資源,而是直接依賴A的,如果A和B包含了C的資源,那么C的這個資源舊不會被打包進去,而是依賴A和B。這時候只要有同樣的資源,就會向前依賴,當我們希望,B和C依賴A,但B和C之間不互相依賴,就需要嵌套Push Pop了,當我們調用PopAssetDependencies就會結束依賴
string path = Application.streamingAssetsPath;
BuildPipeline.PushAssetDependencies();
BuildTarget target = BuildTarget.StandaloneWindows;
BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath("Assets/UI_tck_icon_houtui.png"), null,
path + "/package1.assetbundle",
BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets
| BuildAssetBundleOptions.DeterministicAssetBundle, target);
BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath("Assets/New Material.mat"), null,
path + "/package2.assetbundle",
BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets
| BuildAssetBundleOptions.DeterministicAssetBundle, target);
BuildPipeline.PushAssetDependencies();
BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath("Assets/Cube.prefab"), null,
path + "/package3.assetbundle",
BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets
| BuildAssetBundleOptions.DeterministicAssetBundle, BuildTarget.StandaloneWindows);
BuildPipeline.PopAssetDependencies();
BuildPipeline.PushAssetDependencies();
BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath("Assets/Cubes.prefab"), null,
path + "/package4.assetbundle",
BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets
| BuildAssetBundleOptions.DeterministicAssetBundle, target);
BuildPipeline.PopAssetDependencies();
BuildPipeline.PopAssetDependencies();
上面的代碼演示了如何使用依賴,這個測試使用了一個紋理,一個材質,一個正方體Prefab,還有兩個正方體組成的Prefab,材質使用了紋理,而兩組正方體都使用了這個材質,上面的代碼用Push開啟了依賴,打包紋理,然后打包材質(材質自動依賴了紋理),然后嵌套了一個Push,打包正方體(正方體依賴前面的材質和紋理),然后Pop,接下來再嵌套了一個Push,打包那組正方體(不依賴前面的正方體,依賴材質和紋理)
如果我們只開啟最外面的Push Pop,而不嵌套Push Pop,那么兩個正方體組成的Prefab就會依賴單個正方體的Prefab,依賴是一把雙刃劍,它可以去除冗余,但有時候我們又需要那么一點點冗余
【2.依賴丟失】
當我們的Bundle之間有了依賴之后,就不能像前面那樣簡單地直接Load對應的Bundle了,我們需要
把Bundle所依賴的Bundle先加載進來,這個加載只是WWW或者
LoadFromCacheOrDownload,並不需要對這個Bundle進行Load,如果BundleB依賴BundleA,當我們要加載BundleB的資源時,假設BundleA沒有被加載進來,或者已經被Unload了,那么BundleB依賴BundleA的部分就會丟失,例如每個正方體上都掛着一個腳本,當我們不嵌套Push Pop時,單個正方體的Bundle沒有被加載或者已經被卸載,我們加載的那組正方體上的腳本就會丟失,
腳本也是一種資源,當一個腳本已經被打包了,依賴這個包的資源,就不會被再打進去
Cubes和Cube都掛載同一個腳本,TestObje,Cubes依賴Cube,將Cube所在的Bundle Unload,再Load Cubes的Bundle,Cubes的腳本丟失,腳本,紋理,材質等一切資源,都是如此
【3.更新依賴】
在打包的時候我們需要指定BuildAssetBundleOptions.DeterministicAssetBundle選項,這個選項會為每個資源生成一個唯一的ID,當這個資源被重新打包的時候,確定這個ID不會改變,包的依賴是根據這個ID來的,使用這個選項的好處是,當資源需要更新時,依賴於該資源的其他資源,不需要重新打包
A -> B -> C
當A依賴B依賴C時,B更新,需要重新打包C,B,而A不需要動,打包C的原因是,因為B依賴於C,如果不打包C,直接打包B,那么C的資源就會被重復打包,而且B和C的依賴關系也會斷掉
【四,內存】
在使用WWW加載Bundle時,會開辟一塊內存,這塊內存是Bundle文件解壓之后的內存,這意味着這塊內存很大,通過Bundle.Unload可以釋放掉這塊內存,Unload true和Unload false 都會釋放掉這塊內存,而這個Bundle也不能再用,如果要再用,需要重新加載Bundle,
需要注意的是,依賴這個Bundle的其他Bundle,在Load的時候,會報錯
得到Bundle之后,我們用Bundle.Load來加載資源,這些資源會從Bundle的內存被復制出來,作為Asset放到內存中,這意味着,這塊內存,也很大,Asset內存的釋放,與Unity其他資源的釋放機制一樣,可以通過Resources.UnloadUnuseAsset來釋放沒有引用的資源,也可以通過Bundle.Unload(true)來強制釋放Asset,這會導致所有引用到這個資源的對象丟失該資源
上面兩段話可以得出一個結論,在new WWW(url)的時候,會開辟一塊內存存儲解壓后的Bundle,而在資源被Load出來之后,又會開辟一塊內存來存儲Asset資源,WWW.LoadFromCacheOrDownload(url)的功能和new WWW(url)一樣,但LoadFromCacheOrDownload是將Bundle解壓到磁盤空間而不是內存中,所以LoadFromCacheOrDownload返回的WWW對象,本身並不會占用過多的內存(只是一些索引信息,每個資源對應的磁盤路徑,在Load時從磁盤取出),針對手機上內存較小的情況,
使用
WWW.
LoadFromCacheOrDownload代替new WWW可以有效地節省內存。但LoadFromCacheOrDownload大法也有不靈驗的時候,當它不靈驗時,L
oadFromCacheOrDownload
返回的WWW對象將占用和new WWW一樣的內存,所以
不管你的Bundle是如何創建出來的,都需要在不使用的時候,及時地Unload掉。
另外使用LoadFromCacheOrDownload需要注意的問題是——第二個參數,版本號,Bundle重新打包之后,版本號沒有更新,取出的會是舊版本的Bundle,並且一個Bundle緩存中可能會存在多個舊版本的Bundle,例如1,2,3 三個版本的Bundle
在Bundle Load完之后,不需要再使用該Bundle了,進行Unload,如果有其他Bundle依賴於該Bundle,則應該等依賴於該Bundle的Bundle不需要再Load之后,Unload這個Bundle,一般出現在大場景切換的時候。
我們知道在打包Bundle的時候,有一個參數是mainAsset,如果傳入該參數,那么資源會被視為主資源打包,在得到Bundle之后,可以用AssetBundle.mainAsset直接使用,那么是否在WWW獲取Bundle的時候,就已經將mainAsset預先Load出來了呢?不是!
在我們調用
AssetBundle.mainAsset取出mainAsset時,它的get方法會阻塞地去Load mainAsset,然后返回,AssetBundle.mainAsset等同於Load("mainAssetName")
PS.重復Load同一個資源並不會開辟新的內存來存儲這個資源
【五,其他】
在使用AssetBundle的開發過程中,我們經常會對資源進行調整,調整之后需要對資源進行打包才能生效,對開發效率有很大的影響,所以在開發中我們使用Resource和Bundle兼容的方式
首先將資源管理封裝到一個Manager中,從Bundle中Load資源還是從Resource里面Load資源,都由它決定,這樣可以保證上層邏輯代碼不需要關心當前的資源管理類型
當然,我們所有要打包的對象,都在Resource目錄下,並且使用嚴格的目錄規范,然后使用腳本對象,來記錄每個資源所在的Bundle,以及所對應的Resource目錄,在資源發生變化的時候,更新腳本對象,Manager在運行時使用腳本對象的配置信息,這里的腳本對象我們是使用代碼自動生成的,當然,你也可以用配置表,效果也是一樣的
版本管理也可以交由腳本對象來實現,每次打包的資源,需要將其版本號+1,腳本對象可存儲所有資源的版本號,版本號可以用於LoadFromCacheOrDownload時傳入,也可以手動寫入配置表,在我設計的腳本對象中,每個資源都會有一個所屬Bundle,Resource下相對路徑,版本號等三個屬性
在版本發布的時候,你需要先打包一次Bundle,並且將Resource目錄改成其他的名字,然后再打包,確保Resource目錄下的資源沒有被重復打包,而如果你想打的是Resource版本,則需要將StreamingAssets下的Bundle文件刪除
腳本對象的使用如下:
1.先設計好存儲結構
2.寫一個繼承於ScriptObject的類,用可序列化的容器存儲數據結構(List或數組),Dictionary等容器無法序列化,public之后在
[Serializable]
public class ResConfigData
{
public string ResName; //資源名字
public string BundleName; //包名字
public string Path; //資源路徑
public int Vesrion; //版本號
}
[System.Serializable]
public class ResConfig : ScriptableObject
{
public List<ResConfigData> ConfigDatas = new List<ResConfigData>();
}
4.在指定的路徑讀取對象,讀取不到則創建對象
ResConfig obj = (ResConfig)AssetDatabase.LoadAssetAtPath(path, typeof(ResConfig));
if (obj == null)
{
obj = ScriptableObject.CreateInstance<ResConfig>();
AssetDatabase.CreateAsset(obj, path);
}
3.寫入數據,直接修改obj的數組,並保存(不保存下次啟動Unity數據會丟失)
EditorUtility.SetDirty(obj);
由於數組操作不方便,所以我們可以將數據轉化為方便各種增刪操作的Dictionary容器存儲,在保持時將其寫入到持久化的容器中
最后,發現之前的一些文章被“原創”了,請勿“原創”這篇文章

