unity在ios平台下內存的優化?
unity開發一個遊戲,同一個場景,iphone下的內存消耗快到pc的兩倍了。。。在pc下切換場景時可以做到內存回收,但是在iphone下內存回收的很少,總體持續增長。。。gui用的是daikon forge,找不到是哪裡出了問題~
一方面是避免內存泄漏,另一方面是減少內存分配。
- 避免內存泄漏,需要細心的去進行黑盒白盒檢查,一般都是設計上的不合理造成的。同時可以善用 Destroy() 方法,強制釋放非託管內存。最好弄清楚 Unity 的資源管理機制,這方面網上教程很多,我就不做搬運工了。
- 減少內存分配,並不是說任何時候都不分配。在關卡進行時要將內存分配盡量減少,以降低 GC 的頻率。可以用 Profiler 找出是所有分配了內存的地方,再根據經驗判斷是否要進行優化。我以前粗略的整理過一些會產生 GC 的操作,可供參考:
- 生成一個新的委託,例如將方法做為參數傳入
- 對 List 進行 foreach
- 用枚舉做 Key 進行字典查找(可能是默認比較器 GetHashCode 時裝箱引起的,提供自定義的比較器應該能解決)
- 訪問 animation 等組件
- 獲取 SkinedMeshRenderer.bones 或 Mesh.uvs 之類的屬性
- yield return 0 (建議全部替換為 yield return null)
- 調用 GetComponentInChildren(建議自己實現一個無GC版本)
分享一些具體的gc alloc產生的隱患和解決方案。
1 Delegate
1.1 Delegate的賦值(=)操作
Delegate是我們的好夥伴,日常開發中,都離不開它。但是,不真正了解Delegate而胡亂使用可能會帶來巨大的性能問題
下面的代碼中,聲明了一個委託和一個委託類型的成員變數del,然後在Update的時候賦值,將Start賦值給del。
public delegate void DelegateMethod();
public DelegateMethod del; void Start () {}
void Update () {
del = Start; }這段代碼是有性能問題的。在Unity的profiler可以看到有104B的gc。
為什麼呢?因為C#中對委託的「=」操作,其實是等價於new。上面的Update中的實現,在編譯器看,其實等價於
del = new DelegateMethod (Start);
在編程時,我們要牢記這點,切忌對委託對象進行頻繁的賦值操作,避免導致不必要的性能消耗。1.2 Delgate 的 「+/-」操作。
Delgate還可以進行「+/-」操作,實現多委託。
public delegate void DelegateMethod();
public DelegateMethod single_del; public DelegateMethod multi_del; void Start () { single_del = Start; multi_del = single_del; } void Update () { multi_del += single_del;multi_del -= single_del;
}profiler的結果,每幀312B的gc
因為在Start函數中,有一次對multi_del的賦值,multi_del此時已經是一個SimpleDelgate了,但Update時,先有一個"+"操作,c#就講multi_del改為了MultiDelgate。MultiDelgate的添加和刪除元素,都會有Clone操作,如上圖中紅框所示。
並且,multi_del越長,這個Clone操作的次數就越多。比如這種情況:
public delegate void DelegateMethod();
public DelegateMethod single_del; public DelegateMethod multi_del; void Start () {single_del = Start;
multi_del = single_del; multi_del += single_del; multi_del += single_del; multi_del += single_del; } void Update () { multi_del += single_del; multi_del -= single_del; }會造成每次調用高達0.9kb的gc。
1.3 EventDispatcher的最佳實現。
因為1.1和1.2,我們推薦用List&
public class EventDispather : MonoBehaviour {
public delegate void EventHandler(); public List&if (!_handlers.Contains (handler)) {
_handlers.Add (handler); } } void RemoveHandler(EventHandler handler) { if (_handlers.Contains (handler)) { _handlers.Remove (handler); } }class Example
{ public EventDispather dispatcher; EventHandler _Example_Handler; public Example() { dispatcher = new EventDispather(); _Example_Handler = Example_Handler; //緩存函數代理的引用 } public void Update(){
// dispatcher.AddHandler (Example_Handler);//gc warning!! // dispatcher.RemoveHandler (Example_Handler);//gc warnning!!dispatcher.AddHandler (_Example_Handler);
dispatcher.RemoveHandler (_Example_Handler); } public void Example_Handler() { } } }2 String字元串
2.1 String.concat運行下面的測試代碼:
public string a_str = "1";
public string b_str = "2"; // Update is called once per frame void Update () { A (); B (); C (); } void A(){ a_str = "1" + "2"; } void B() { a_str = a_str + b_str; } void C() { a_str = a_str + b_str + a_str; }我們發現,String.Concat內部在每次調用時會創建一個新的字元串對象。所以String.Concat字數越多,gc alloc就越多(StringTest.C()&>StringTest.B()).
值得一提的是,A()函數的實現沒有gc,因為編譯器會將這種常量字元串的拼接在編譯期優化掉。)。
一種優化辦法是,使用StringBuilder或String.Format(內部實現也是stringbuilder)來減少創建新字元串對象的次數,但需要在創建StringBuilder
2.2 Int.ToString
遊戲開發中,經常會遇到將遊戲中的數值顯示到ui上的需求,比如:
public int gold = 1;
void Update () { gold++; uiGold.text = gold.ToString (); }對於數字文本,有一種優化方法是預生成遊戲中所有可能用到的所有數字文本,從而避免了運行時ToString的消耗。假設遊戲中的數字不會超過20480,加血和扣血不會超過10240.
private static string[] int_str_dict=null;
private static string[] plus_int_str_dict= null; private static string[] del_int_str_dict= null;//遊戲載入階段調用。
public static void Init() { if (int_str_dict == null) { int_str_dict = new string[20480]; plus_int_str_dict = new string[10240]; del_int_str_dict = new string[10240]; for (int i = 0; i &< int_str_dict.Length; i++) { int_str_dict [i] = i.ToString (); } for (int i = 0; i &< plus_int_str_dict.Length; i++) { plus_int_str_dict [i] = "+" + i.ToString (); } for (int i = 0; i &< del_int_str_dict.Length; i++) { del_int_str_dict [i] = "-" + i.ToString (); } } } public static string ToPlusIntString(this int value){ if (value &< plus_int_str_dict.Length value &>= 0) return plus_int_str_dict [value]; else return "+"+plus_int_str_dict.ToString (); }public static string ToDelIntString(this int value){
if (value &< del_int_str_dict.Length value &>= 0) return del_int_str_dict [value]; else return "-" + value.ToString (); }三個數組的總內存佔用為
相比運行時的那些gc,這187kb的預分配其實性價比很高,所以代碼優化為。
public int gold = 1;
void Update () { gold++; uiGold.text = gold.ToIntString (); }3 用枚舉作key的Dictionary
遊戲中經常會用枚舉,但用枚舉做字典的key就會有性能隱患。public enum EnmKey
{ a, b, c } public Dictionary&// Update is called once per frame
void Update () { int a = 0; enm_dict.TryGetValue (EnmKey.a,out a); }為什麼呢。因為在Dictionary內部實現中,會調用介面
object.Equals(object a);
來判斷兩個元素是否相等。但枚舉為值類型,object是引用類型。將值類型的數據轉換為引用類型,就會產生一次裝箱和拆箱操作(詳細細節可以百度),從而導致gc alloc。實際上:
List of Struct
枚舉為key的DictionaryStruct為key的Dictionary在查詢時,都會有裝箱帶來的消耗。解決辦法是:
確保你的struct實現了 IEquatable&< T &>
確保你的structoverride Equals() and GetHashCode()為枚舉為key的字典創建一個Custom Comparerpublic enum EnmKey:int
{ a, b, c } public class EnmKeyTypeCompare : IEqualityComparer&
public int GetHashCode (EnmKey obj)
{ return (int)obj; } } public Dictionary&
void Update () {
int a = 0; enm_dict.TryGetValue (EnmKey.a,out a); }4 其他小tips
永遠不要遍歷Dictionary,除非你能保證只需遍歷有限的幾次。元素個數較少(小於10)List的查詢速度其實不比Dictionary慢。對於可變長度的List,Dictionary,最好能預估一下容量,避免運行時擴容帶來的性能消耗。性能敏感地帶,不要使用List.Find,List.FIndIndex,List.FindAll 等介面,原因和1.1中所 說一樣,每次會有一次對象創建,從而產生gc alloc。uGUI的Image.set_fillAmout有gc alloc消耗,即使你設置的值是相同的。建議賦值之前做一次是否改變的判斷。UnityEvent.Invoke性能很差,不推薦使用。對於有回調需求的還是建議使用Delegate,如本文第一節所述。優化性能時一定要杜絕每幀都有gc alloc的實現。即使每幀50b,一秒就是3kb!(60fps的話)mono在ios的gc有點問題(和ios本身內存管理方式也有關係,windows在內存回收方面比linux和unix都有效很多), 建議使用pool等機制, 避免過多類似
大量直接
new SomeClass而是重複使用已經new的SomeClass隨便提兩句,1. 注意profiler里gcalloc,特別注意每幀都分配內存的項,想辦法優化掉,比如使用迭代器或for循環替代foreach等。另外,一些unity方法在編輯器與真機上的內存分配表現是不一樣的,建議把常見的方法(比如monobehaviour中的),找人單獨測試一遍,形成文檔,便於查詢2. 特別要注意紋理復用與atlas合併,辛辛苦苦從代碼里省出來的內存,多一張沒用的紋理就白忙活了。注意同名的紋理,資源命名合理的話,項目不應該出現名字相同的兩個資源,所以在profiler里take sample當前內存是有效的。另外,遊戲比較大的話,take sample會很慢,而且目前它也不支持各種排序,比如按名字,所以可以自己抽空寫一個簡單的profiler3. 不同的遊戲平台要區分一下不同的asset bundle載入方法,這裡有兩個東西可能比較占內存,webstream和serializedfile,每個api都有自己的問題和優點,最好全部測試並選擇合適的4. 及時卸載不用的的asset bundle,當然並不是所有的東西都適合unload(true)的,有些還需要配合resources.unloadUnusedAsserts使用,但這個方法較費,可以考慮放在切換場景狀態機的時候。另外,unload(false)意味著你可以再次載入同一個資源了,這可能會導致同一份資源載入多個到內存,需要做好管理5. 設計一個合適的pool,遊戲程序都這麼干,沒啥好說的6. 可能的話,用腳本代替部分animation的功能,會減少一部分內存7. 場景要加大資源復用,另外,static batching功能的確可以降低draw call數,但會產生巨大的combined mesh,動輒幾十兆,要測試好8. 使用壓縮紋理,移動平台壓縮完大概只佔編輯里profiler看到的1/89. 對於偏數據的數據結構,可以考慮使用struct替代class,這樣很多時候就避免了gcalloc10. 困了o(╯□╰)o
推薦閱讀:
※C#如何向C++生成的dll文件中傳遞二維數組?
※零基礎學unity3d需要培訓么?
※如何用unity做兩面互相反射的鏡子?
※如何用高效的用 shader 實現柱狀圖?
※哪裡有Unity3D遊戲開發的教程?