1. 程式人生 > >unity3D記憶體機制

unity3D記憶體機制

最近網友通過網站搜尋Unity3D在手機及其他平臺下佔用記憶體太大. 這裡寫下關於unity3d對於記憶體的管理與優化.

Unity3D 裡有兩種動態載入機制:一個是Resources.Load,另外一個通過AssetBundle,其實兩者區別不大。 Resources.Load就是從一個預設打程序序包裡的AssetBundle里加載資源,而一般AssetBundle檔案需要你自己建立,執行時 動態載入,可以指定路徑和來源的。

其實場景裡所有靜態的物件也有這麼一個載入過程,只是Unity3D後臺替你自動完成了。

詳細說一下細節概念:
AssetBundle執行時載入:
來自檔案就用CreateFromFile(注意這種方法只能用於standalone程式)這是最快的載入方法
也可以來自Memory,用CreateFromMemory(byte[]),這個byte[]可以來自檔案讀取的緩衝,www的下載或者其他可能的方式。
其實WWW的assetBundle就是內部資料讀取完後自動建立了一個assetBundle而已
Create完以後,等於把硬碟或者網路的一個檔案讀到記憶體一個區域,這時候只是個AssetBundle記憶體映象資料塊,還沒有Assets的概念。
Assets載入:
用AssetBundle.Load(同Resources.Load) 這才會從AssetBundle的記憶體映象裡讀取並建立一個Asset物件,建立Asset物件同時也會分配相應記憶體用於存放(反序列化)
非同步讀取用AssetBundle.LoadAsync
也可以一次讀取多個用AssetBundle.LoadAll
AssetBundle的釋放:
AssetBundle.Unload(flase)是釋放AssetBundle檔案的記憶體映象,不包含Load建立的Asset記憶體物件。
AssetBundle.Unload(true)是釋放那個AssetBundle檔案記憶體映象和並銷燬所有用Load建立的Asset記憶體物件。

一個Prefab從assetBundle裡Load出來 裡面可能包括:Gameobject transform mesh texture material shader script和各種其他Assets。
你 Instaniate一個Prefab,是一個對Assets進行Clone(複製)+引用結合的過程,GameObject transform 是Clone是新生成的。其他mesh / texture / material / shader 等,這其中些是純引用的關係的,包括:Texture和TerrainData,還有引用和複製同時存在的,包括:Mesh/material /PhysicMaterial。引用的Asset物件不會被複制,只是一個簡單的指標指向已經Load的Asset物件。這種含糊的引用加克隆的混合, 大概是搞糊塗大多數人的主要原因。
專門要提一下的是一個特殊的東西:Script Asset,看起來很奇怪,Unity裡每個Script都是一個封閉的Class定義而已,並沒有寫呼叫程式碼,光Class的定義指令碼是不會工作的。其 實Unity引擎就是那個呼叫程式碼,Clone一個script asset等於new一個class例項,例項才會完成工作。把他掛到Unity主執行緒的呼叫鏈裡去,Class例項裡的OnUpdate OnStart等才會被執行。多個物體掛同一個指令碼,其實就是在多個物體上掛了那個指令碼類的多個例項而已,這樣就好理解了。在new class這個過程中,資料區是複製的,程式碼區是共享的,算是一種特殊的複製+引用關係。
你可以再Instaniate一個同樣的Prefab,還是這套mesh/texture/material/shader...,這時候會有新的GameObject等,但是不會建立新的引用物件比如Texture.
所以你Load出來的Assets其實就是個資料來源,用於生成新物件或者被引用,生成的過程可能是複製(clone)也可能是引用(指標)
當你Destroy一個例項時,只是釋放那些Clone物件,並不會釋放引用物件和Clone的資料來源物件,Destroy並不知道是否還有別的object在引用那些物件。
等到沒有任何 遊戲場景物體在用這些Assets以後,這些assets就成了沒有引用的遊離資料塊了,是UnusedAssets了,這時候就可以通過 Resources.UnloadUnusedAssets來釋放,Destroy不能完成這個任 務,AssetBundle.Unload(false)也不行,AssetBundle.Unload(true)可以但不安全,除非你很清楚沒有任何 物件在用這些Assets了。
配個圖加深理解:

Unity3D佔用記憶體太大怎麼解決呢?

雖然都叫Asset,但複製的和引用的是不一樣的,這點被Unity的暗黑技術細節掩蓋了,需要自己去理解。

關於記憶體管理
按照傳統的程式設計思維,最好的方法是:自己維護所有物件,用一個Queue來儲存所有object,不用時該Destory的,該Unload的自己處理。
但這樣在C# .net框架底下有點沒必要,而且很麻煩。
穩妥起見你可以這樣管理

建立時:
先建立一個AssetBundle,無論是從www還是檔案還是memory
用AssetBundle.load載入需要的asset
載入完後立即AssetBundle.Unload(false),釋放AssetBundle檔案本身的記憶體映象,但不銷燬載入的Asset物件。(這樣你不用儲存AssetBundle的引用並且可以立即釋放一部分記憶體)
釋放時:
如果有Instantiate的物件,用Destroy進行銷燬
在合適的地方呼叫Resources.UnloadUnusedAssets,釋放已經沒有引用的Asset.
如果需要立即釋放記憶體加上GC.Collect(),否則記憶體未必會立即被釋放,有時候可能導致記憶體佔用過多而引發異常。
這樣可以保證記憶體始終被及時釋放,佔用量最少。也不需要對每個載入的物件進行引用。

當然這並不是唯一的方法,只要遵循載入和釋放的原理,任何做法都是可以的。

系統在載入新場景時,所有的記憶體物件都會被自動銷燬,包括你用AssetBundle.Load載入的物件和Instaniate克隆的。但是不包括AssetBundle檔案自身的記憶體映象,那個必須要用Unload來釋放,用.net的術語,這種資料快取是非託管的。

總結一下各種載入和初始化的用法:
AssetBundle.CreateFrom.....:建立一個AssetBundle記憶體映象,注意同一個assetBundle檔案在沒有Unload之前不能再次被使用
WWW.AssetBundle:同上,當然要先new一個再 yield return 然後才能使用
AssetBundle.Load(name): 從AssetBundle讀取一個指定名稱的Asset並生成Asset記憶體物件,如果多次Load同名物件,除第一次外都只會返回已經生成的Asset 物件,也就是說多次Load一個Asset並不會生成多個副本(singleton)。
Resources.Load(path&name):同上,只是從預設的位置載入。
Instantiate(object):Clone 一個object的完整結構,包括其所有Component和子物體(詳見官方文件),淺Copy,並不複製所有引用型別。有個特別用法,雖然很少這樣 用,其實可以用Instantiate來完整的拷貝一個引用型別的Asset,比如Texture等,要拷貝的Texture必須型別設定為 Read/Write able。

總結一下各種釋放
Destroy: 主要用於銷燬克隆物件,也可以用於場景內的靜態物體,不會自動釋放該物件的所有引用。雖然也可以用於Asset,但是概念不一樣要小心,如果用於銷燬從文 件載入的Asset物件會銷燬相應的資原始檔!但是如果銷燬的Asset是Copy的或者用指令碼動態生成的,只會銷燬記憶體物件。
AssetBundle.Unload(false):釋放AssetBundle檔案記憶體映象
AssetBundle.Unload(true):釋放AssetBundle檔案記憶體映象同時銷燬所有已經Load的Assets記憶體物件
Reources.UnloadAsset(Object):顯式的釋放已載入的Asset物件,只能解除安裝磁碟檔案載入的Asset物件
Resources.UnloadUnusedAssets:用於釋放所有沒有引用的Asset物件
GC.Collect()強制垃圾收集器立即釋放記憶體 Unity的GC功能不算好,沒把握的時候就強制呼叫一下

在3.5.2之前好像Unity不能顯式的釋放Asset

舉兩個例子幫助理解
例子1:
一個常見的錯誤:你從某個AssetBundle裡Load了一個prefab並克隆之:obj = Instaniate(AssetBundle1.Load('MyPrefab”);
這個prefab比如是個npc
然後你不需要他的時候你用了:Destroy(obj);你以為就釋放乾淨了
其實這時候只是釋放了Clone物件,通過Load載入的所有引用、非引用Assets物件全都靜靜靜的躺在記憶體裡。
這種情況應該在Destroy以後用:AssetBundle1.Unload(true),徹底釋放乾淨。
如果這個AssetBundle1是要反覆讀取的 不方便Unload,那可以在Destroy以後用:Resources.UnloadUnusedAssets()把所有和這個npc有關的Asset都銷燬。
當然如果這個NPC也是要頻繁建立 銷燬的 那就應該讓那些Assets呆在記憶體裡以加速遊戲體驗。
由此可以解釋另一個之前有人提過的話題:為什麼第一次Instaniate 一個Prefab的時候都會卡一下,因為在你第一次Instaniate之前,相應的Asset物件還沒有被建立,要載入系統內建的 AssetBundle並建立Assets,第一次以後你雖然Destroy了,但Prefab的Assets物件都還在記憶體裡,所以就很快了。

順便提一下幾種載入方式的區別:
其實存在3種載入方式:
一是靜態引用,建一個public的變數,在Inspector裡把prefab拉上去,用的時候instantiate
二是Resource.Load,Load以後instantiate
三是AssetBundle.Load,Load以後instantiate
三種方式有細 節差異,前兩種方式,引用物件texture是在instantiate時載入,而assetBundle.Load會把perfab的全部assets 都載入,instantiate時只是生成Clone。所以前兩種方式,除非你提前載入相關引用物件,否則第一次instantiate時會包含載入引用 assets的操作,導致第一次載入的lag。

例子2:
從磁碟讀取一個1.unity3d檔案到記憶體並建立一個AssetBundle1物件
AssetBundle AssetBundle1 = AssetBundle.CreateFromFile("1.unity3d");
從AssetBundle1裡讀取並建立一個Texture Asset,把obj1的主貼圖指向它
obj1.renderer.material.mainTexture = AssetBundle1.Load("wall") as Texture;
把obj2的主貼圖也指向同一個Texture Asset
obj2.renderer.material.mainTexture =obj1.renderer.material.mainTexture;
Texture是引用物件,永遠不會有自動複製的情況出現(除非你真需要,用程式碼自己實現copy),只會是建立和新增引用
如果繼續:
AssetBundle1.Unload(true) 那obj1和obj2都變成黑的了,因為指向的Texture Asset沒了
如果:
AssetBundle1.Unload(false) 那obj1和obj2不變,只是AssetBundle1的記憶體映象釋放了
繼續:
Destroy(obj1),//obj1被釋放,但並不會釋放剛才Load的Texture
如果這時候:
Resources.UnloadUnusedAssets();
不會有任何記憶體釋放 因為Texture asset還被obj2用著
如果
Destroy(obj2)
obj2被釋放,但也不會釋放剛才Load的Texture
繼續
Resources.UnloadUnusedAssets();
這時候剛才load的Texture Asset釋放了,因為沒有任何引用了
最後CG.Collect();
強制立即釋放記憶體
由此可以引申出論壇裡另一個被提了幾次的問題,如何載入一堆大圖片輪流顯示又不爆掉
不考慮AssetBundle,直接用www讀圖片檔案的話等於是直接建立了一個Texture Asset
假設檔案儲存在一個List裡
TLlist<string> fileList;
int n=0;
IEnumerator OnClick()
{
WWW image = new www(fileList[n++]);
yield return image;
obj.mainTexture = image.texture;

n = (n>=fileList.Length-1)?0:n;
Resources.UnloadUnusedAssets();
}
這樣可以保證記憶體裡始終只有一個巨型Texture Asset資源,也不用程式碼追蹤上一個載入的Texture Asset,但是速度比較慢
或者:
IEnumerator OnClick()
{
WWW image = new www(fileList[n++]);
yield return image;
Texture tex = obj.mainTexture;
obj.mainTexture = image.texture;

n = (n>=fileList.Length-1)?0:n;
Resources.UnloadAsset(tex);
}
這樣解除安裝比較快

Hog的評論引用:

感覺這是Unity記憶體管理暗黑和混亂的地方,特別是牽扯到Texture
我最近也一直在測試這些用AssetBundle載入的asset一樣可以用Resources.UnloadUnusedAssets解除安裝,但必須先AssetBundle.Unload,才會被識別為無用的asset。比較保險的做法是
建立時:
先建立一個AssetBundle,無論是從www還是檔案還是memory
用AssetBundle.load載入需要的asset
用完後立即AssetBundle.Unload(false),關閉AssetBundle但不摧毀建立的物件和引用
銷燬時:
對Instantiate的物件進行Destroy
在合適的地方呼叫Resources.UnloadUnusedAssets,釋放已經沒有引用的Asset.
如果需要立即釋放加上GC.Collect()
這樣可以保證記憶體始終被及時釋放
只要你Unload過的AssetBundle,那些建立的物件和引用都會在LoadLevel時被自動釋放。

全面理解Unity載入和記憶體管理機制之二:進一步深入和細節
Unity幾種動態載入Prefab方式的差異:

其實存在3種載入prefab的方式:
一是靜態引用,建一個public的變數,在Inspector裡把prefab拉上去,用的時候instantiate
二是Resource.Load,Load以後instantiate
三是AssetBundle.Load,Load以後instantiate
三種方式有細節差異,前兩種方式,引用物件texture是在instantiate時載入,而assetBundle.Load會把perfab的全部 assets都載入,instantiate時只是生成Clone。所以前兩種方式,除非你提前載入相關引用物件,否則第一次instantiate時會 包含載入引用類assets的操作,導致第一次載入的lag。官方論壇有人說Resources.Load和靜態引用是會把所有資源都預先載入的,反覆測試的結果,靜態引用和Resources.Load也是OnDemand的,用到時才會載入。

幾種AssetBundle建立方式的差異:
CreateFromFile:這種方式不會把整個硬碟AssetBundle檔案都載入到 記憶體來,而是類似建立一個檔案操作控制代碼和緩衝區,需要時才實時Load,所以這種載入方式是最節省資源的,基本上AssetBundle本身不佔什麼內 存,只需要Asset物件的記憶體。可惜只能在PC/Mac Standalone程式中使用。
CreateFromMemory和www.assetBundle:這兩種方式AssetBundle檔案會整個映象於記憶體中,理論上檔案多大就需要多大的記憶體,之後Load時還要佔用額外記憶體去生成Asset物件。

什麼時候才是UnusedAssets?
看一個例子:
Object obj = Resources.Load("MyPrefab");
GameObject instance = Instantiate(obj) as GameObject;
.........
Destroy(instance);
建立隨後銷燬了一個Prefab例項,這時候 MyPrefab已經沒有被實際的物體引用了,但如果這時:
Resources.UnloadUnusedAssets();
記憶體並沒有被釋放,原因:MyPrefab還被這個變數obj所引用
這時候:
obj  = null;
Resources.UnloadUnusedAssets();
這樣才能真正釋放Assets物件
所以:UnusedAssets不但要沒有被實際物體引用,也要沒有被生命週期內的變數所引用,才可以理解為 Unused(引用計數為0)
所以所以:如果你用個全域性變數儲存你Load的Assets,又沒有顯式的設為null,那 在這個變數失效前你無論如何UnloadUnusedAssets也釋放不了那些Assets的。如果你這些Assets又不是從磁碟載入的,那除了 UnloadUnusedAssets或者載入新場景以外沒有其他方式可以解除安裝之。

一個複雜的例子,程式碼很醜陋實際也不可能這樣做,只是為了加深理解

IEnumerator OnClick()

{

Resources.UnloadUnusedAssets();//清乾淨以免影響測試效果

yield return new WaitForSeconds(3);

float wait = 0.5f;

//用www讀取一個assetBundle,裡面是一個Unity基本球體和帶一張大貼圖的材質,是一個Prefab

WWW aa = new WWW(@"file://SpherePrefab.unity3d");

yield return aa;

AssetBundle asset = aa.assetBundle;

yield return new WaitForSeconds(wait);//每步都等待0.5s以便於分析結果

Texture tt = asset.Load("BallTexture") as  Texture;//載入貼圖

yield return new WaitForSeconds(wait);

GameObject ba = asset.Load("SpherePrefab") as  GameObject;//載入Prefab

yield return new WaitForSeconds(wait);

GameObject obj1 = Instantiate(ba) as GameObject;//生成例項

yield return new WaitForSeconds(wait);

Destroy(obj1);//銷燬例項

yield return new WaitForSeconds(wait);

asset.Unload(false);//解除安裝Assetbundle

yield return new WaitForSeconds(wait);

Resources.UnloadUnusedAssets();//解除安裝無用資源

yield return new WaitForSeconds(wait);

ba = null;//將prefab引用置為空以後卸無用載資源

Resources.UnloadUnusedAssets();

yield return new WaitForSeconds(wait);

tt = null;//將texture引用置為空以後解除安裝無用資源

Resources.UnloadUnusedAssets();

}

這是測試結果的記憶體Profile曲線圖

Unity3D佔用記憶體太大怎麼解決呢?

圖片:p12.jpg

很經典的對稱造型,用多少釋放多少。

這是各階段的記憶體和其他資料變化

說明:
1        初始狀態
2        載入AssetBundle檔案後,記憶體多了檔案映象,用量上升,Total Object和Assets增加1(AssetBundle也是object)
3        載入Texture後,記憶體繼續上升,因為多了Texture Asset,Total Objects和Assets增加1
4        載入Prefab後,記憶體無明顯變化,因為最佔記憶體的Texture已經載入,Materials上升是因為多了Prefab的材質,Total Objects和Assets增加6,因為 Perfab 包含很多 Components
5        例項化Prefab以後,視訊記憶體的Texture Memory、GameObjectTotal、Objects in Scene上升,都是因為例項化了一個可視的物件
6        銷燬例項後,上一步的變化還原,很好理解
7        解除安裝AssetBundle檔案後,AssetBundle檔案映象佔用的記憶體被釋放,相應的Assets和Total Objects Count也減1
8        直接Resources.UnloadUnusedAssets,沒有任何變化,因為所有Assets引用並沒有清空
9        把Prefab引用變數設為null以後,整個Prefab除了Texture外都沒有任何引用了,所以被UnloadUnusedAssets銷燬,Assets和Total Objects Count減6
10        再把Texture的引用變數設為null,之後也被UnloadUnusedAssets銷燬,記憶體被釋放,assets和Total Objects Count減1,基本還原到初始狀態

從中也可以看出:
Texture載入以後是到記憶體,顯示的時候才進入視訊記憶體的Texture Memory。
所有的東西基礎都是Object
Load的是Asset,Instantiate的是GameObject和Object in Scene
Load的Asset要Unload,new的或者Instantiate的object可以Destroy

Unity 3D中的記憶體管理

Unity3D在記憶體佔用上一直被人詬病,特別是對於面向移動裝置的遊戲開發,動輒記憶體佔用飆上一兩百兆,導致記憶體資源耗盡,從而被系統強退造成極 差的體驗。類似這種情況並不少見,但是絕大部分都是可以避免的。雖然理論上Unity的記憶體管理系統應當為開發者分憂解難,讓大家投身到更有意義的事情中 去,但是對於Unity對記憶體的管理方式,官方文件中並沒有太多的說明,基本需要依靠自己摸索。最近在接手的專案中存在嚴重的記憶體問題,在參照文件和 Unity Answer眾多猜測和證實之後,稍微總結了下Unity中的記憶體的分配和管理的基本方式,在此共享。

雖然Unity標榜自己的記憶體使用全都是“Managed Memory”,但是事實上你必須正確地使用記憶體,以保證回收機制正確執行。如果沒有做應當做的事情,那麼場景和程式碼很有可能造成很多非必要記憶體的佔用, 這也是很多Unity開發者抱怨記憶體佔用太大的原因。接下來我會介紹Unity使用記憶體的種類,以及相應每個種類的優化和使用的技巧。遵循使用原則,可以 讓非必要資源儘快得到釋放,從而降低記憶體佔用。

Unity中的記憶體種類

實際上Unity遊戲使用的記憶體一共有三種:程式程式碼、託管堆(Managed Heap)以及本機堆(Native Heap)。

程式程式碼包括了所有的Unity引擎,使用的庫,以及你所寫的所有的遊戲程式碼。在編譯後,得到的執行檔案將會被載入到裝置中執行,並佔用一定記憶體。

這部分記憶體實際上是沒有辦法去“管理”的,它們將在記憶體中從一開始到最後一直存在。一個空的Unity預設場景,什麼程式碼都不放,在iOS裝置上佔 用記憶體應該在17MB左右,而加上一些自己的程式碼很容易就飆到20MB左右。想要減少這部分記憶體的使用,能做的就是減少使用的庫,稍後再說。

託管堆是被Mono使用的一部分記憶體。Mono專案一個開源的.net框架的一種實現,對於Unity開發,其實充當了基本類庫的角色。

託管堆用來存放類的例項(比如用new生成的列表,例項中的各種宣告的變數等)。“託管”的意思是Mono“應該”自動地改變堆的大小來適應你所需要的記憶體,

並且定時地使用垃圾回收(Garbage Collect)來釋放已經不需要的記憶體。關鍵在於,有時候你會忘記清除對已經不需要再使用的記憶體的引用,

從而導致Mono認為這塊記憶體一直有用,而無法回收。

最後,本機堆是Unity引擎進行申請和操作的地方,比如貼圖,音效,關卡資料等。Unity使用了自己的一套記憶體管理機制來使這塊記憶體具有和託管堆類似的功能。

基本理念是,如果在這個關卡里需要某個資源,那麼在需要時就載入,之後在沒有任何引用時進行解除安裝。聽起來很美好也和託管堆一樣,

但是由於Unity有一套自動載入和解除安裝資源的機制,讓兩者變得差別很大。自動載入資源可以為開發者省不少事兒,

但是同時也意味著開發者失去了手動管理所有載入資源的權力,這非常容易導致大量的記憶體佔用(貼圖什麼的你懂的),

也是Unity給人留下“吃記憶體”印象的罪魁禍首。

優化程式程式碼的記憶體佔用

這部分的優化相對簡單,因為能做的事情並不多:主要就是減少打包時的引用庫,改一改build設定即可。

對於一個新專案來說不會有太大問題,但是如果是已經存在的專案,可能改變會導致原來所需要的庫的缺失(雖說一般來說這種可能性不大),

因此有可能無法做到最優。

當使用Unity開發時,預設的Mono包含庫可以說大部分用不上,在Player Setting(Edit->Project Setting->Player或者Shift+Ctrl(Command)+B裡的Player Setting按鈕)

面板裡,將最下方的Optimization欄目中“Api Compatibility Level”選為.NET 2.0 Subset,表示你只會使用到部分的.NET 2.0 Subset,不需要Unity將全部.NET的Api包含進去。接下來的“Stripping Level”表示從build的庫中剝離的力度,每一個剝離選項都將從打包好的庫中去掉一部分內容。你需要保證你的程式碼沒有用到這部分被剝離的功能,

選為“Use micro mscorlib”的話將使用最小的庫(一般來說也沒啥問題,不行的話可以試試之前的兩個)。庫剝離可以極大地降低打包後的程式的尺寸以及程式程式碼的記憶體佔用,唯一的缺點是這個功能只支援Pro版的Unity。

這部分優化的力度需要根據程式碼所用到的.NET的功能來進行調整,有可能不能使用Subset或者最大的剝離力度。

如果超出了限度,很可能會在需要該功能時因為找不到相應的庫而crash掉(ios的話很可能在Xcode編譯時就報錯了)。

比較好地解決方案是仍然用最強的剝離,並輔以較小的第三方的類庫來完成所需功能。

一個最常見問題是最大剝離時Sysytem.Xml是不被Subset和micro支援的,如果只是為了xml,完全可以匯入一個輕量級的xml庫來解決依賴(Unity官方推薦這個)。

關於每個設定對應支援的庫的詳細列表,可以在這裡找到。關於每個剝離級別到底做了什麼,Unity的文件也有說明。

實際上,在遊戲開發中絕大多數被剝離的功能使用不上的,因此不管如何,庫剝離的優化方法都值得一試。

託管堆優化

Unity有一篇不錯的關於託管堆程式碼如何寫比較好的說明,在此基礎上我個人有一些補充。

首先需要明確,託管堆中儲存的是你在你的程式碼中申請的記憶體(不論是用js,C#還是Boo寫的)。

一般來說,無非是new或者Instantiate兩種生成object的方法(事實上Instantiate中也是呼叫了new)。

在接收到alloc請求後,託管堆在其上為要新生成的物件例項以及其例項變數分配記憶體,如果可用空間不足,則向系統申請更多空間。

當你使用完一個例項物件之後,通常來說在指令碼中就不會再有對該物件的引用了(這包括將變數設定為null或其他引用,超出了變數的作用域,

或者對Unity物件傳送Destory())。在每隔一段時間,Mono的垃圾回收機制將檢測記憶體,將沒有再被引用的記憶體釋放回收。總的來說,

你要做的就是在儘可能早的時間將不需要的引用去除掉,這樣回收機制才能正確地把不需要的記憶體清理出來。但是需要注意在記憶體清理時有可能造成遊戲的短時間卡頓,

這將會很影響遊戲體驗,因此如果有大量的記憶體回收工作要進行的話,需要儘量選擇合適的時間。

如果在你的遊戲裡,有特別多的類似例項,並需要對它們經常傳送Destroy()的話,遊戲效能上會相當難看。比如小熊推金幣中的金幣例項,按理說每枚金幣落下臺子後

都需要對其Destory(),然後新的金幣進入臺子時又需要Instantiate,這對效能是極大的浪費。一種通常的做法是在不需要時,不摧毀這個GameObject,而只是隱藏它,

並將其放入一個重用陣列中。之後需要時,再從重用陣列中找到可用的例項並顯示。這將極大地改善遊戲的效能,相應的代價是消耗部分記憶體,一般來說這是可以接受的。

關於物件重用,可以參考Unity關於記憶體方面的文件中Reusable Object Pools部分,或者Prime31有一個是用Linq來建立重用池的視訊教程(Youtube,需要FQ,上,下)。

如果不是必要,應該在遊戲進行的過程中儘量減少對GameObject的Instantiate()和Destroy()呼叫,因為對計算資源會有很大消耗。在便攜裝置上短時間大量生成和摧毀物體的

話,很容易造成瞬時卡頓。如果記憶體沒有問題的話,儘量選擇先將他們收集起來,然後在合適的時候(比如按暫停鍵或者是關卡切換),將它們批量地銷燬並 且回收記憶體。Mono的記憶體回收會在後臺自動進行,系統會選擇合適的時間進行垃圾回收。在合適的時候,也可以手動地呼叫 System.GC.Collect()來建議系統進行一次垃圾回收。

要注意的是這裡的呼叫真的僅僅只是建議,可能系統會在一段時間後在進行回收,也可能完全不理會這條請求,不過在大部分時間裡,這個呼叫還是靠譜的。

本機堆的優化

當你載入完成一個Unity的scene的時候,scene中的所有用到的asset(包括Hierarchy中所有GameObject上以及指令碼中賦值了的的材質,貼圖,動畫,聲音等素材),

都會被自動載入(這正是Unity的智慧之處)。也就是說,當關卡呈現在使用者面前的時候,所有Unity編輯器能認識的本關卡的資源都已經被預先加 入記憶體了,這樣在本關卡中,使用者將有良好的體驗,不論是更換貼圖,聲音,還是播放動畫時,都不會有額外的載入,這樣的代價是記憶體佔用將變多。Unity最 初的設計目的還是面向桌上型電腦,

幾乎無限的記憶體和虛擬記憶體使得這樣的佔用似乎不是問題,但是這樣的記憶體策略在之後移動平臺的興起和大量移動裝置遊戲的製作中出現了弊端,因為移動設 備能使用的資源始終非常有限。因此在面向移動裝置遊戲的製作時,儘量減少在Hierarchy對資源的直接引用,而是使用Resource.Load的方 法,在需要的時候從硬碟中讀取資源,

在使用後用Resource.UnloadAsset()和Resources.UnloadUnusedAssets()儘快將其解除安裝掉。總之,這裡是一個處理時間和佔用記憶體空間的trade off,

如何達到最好的效果沒有標準答案,需要自己權衡。

在關卡結束的時候,這個關卡中所使用的所有資源將會被解除安裝掉(除非被標記了DontDestroyOnLoad)的資源。注意不僅是DontDestroyOnLoad的資源本身,

其相關的所有資源在關卡切換時都不會被解除安裝。DontDestroyOnLoad一般被用來在關卡之間儲存一些玩家的狀態,比如分數,級別等偏向文 本的資訊。如果DontDestroyOnLoad了一個包含很多資源(比如大量貼圖或者聲音等大記憶體佔用的東西)的話,這部分資源在場景切換時無法卸 載,將一直佔用記憶體,

這種情況應該儘量避免。

另外一種需要注意的情況是指令碼中對資源的引用。大部分指令碼將在場景轉換時隨之失效並被回收,但是,在場景之間被保持的指令碼不在此列(通常情況是被附 著在DontDestroyOnLoad的GameObject上了)。而這些指令碼很可能含有對其他物體的Component或者資源的引用,這樣相關的 資源就都得不到釋放,

這絕對是不想要的情況。另外,static的單例(singleton)在場景切換時也不會被摧毀,同樣地,如果這種單例含有大量的對資源的引用,也會成為大問題。

因此,儘量減少程式碼的耦合和對其他指令碼的依賴是十分有必要的。如果確實無法避免這種情況,那應當手動地對這些不再使用的引用物件呼叫Destroy()

或者將其設定為null。這樣在垃圾回收的時候,這些記憶體將被認為已經無用而被回收。

需要注意的是,Unity在一個場景開始時,根據場景構成和引用關係所自動讀取的資源,只有在讀取一個新的場景或者reset當前場景時,才會得到清理。

因此這部分記憶體佔用是不可避免的。在小記憶體環境中,這部分初始記憶體的佔用十分重要,因為它決定了你的關卡是否能夠被正常載入。因此在計算資源充足

或是關卡開始之後還有機會進行載入時,儘量減少Hierarchy中的引用,變為手動用Resource.Load,將大大減少記憶體佔用。在 Resource.UnloadAsset()和Resources.UnloadUnusedAssets()時,只有那些真正沒有任何引用指向的資源 會被回收,因此請確保在資源不再使用時,將所有對該資源的引用設定為null或者Destroy。

同樣需要注意,這兩個Unload方法僅僅對Resource.Load拿到的資源有效,而不能回收任何場景開始時自動載入的資源。與此類似的還有 AssetBundle的Load和Unload方法,靈活使用這些手動自願載入和解除安裝的方法,是優化Unity記憶體佔用的不二法則。

總之這些就是關於Unity3d優化細節,具體還是檢視Unity3D的技術手冊,以便實現最大的優化。