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&數據結構來實現常用的事件系統。下面是一份best praticle

public class EventDispather : MonoBehaviour {

public delegate void EventHandler();

public List& _handlers =new List&();

void AddHandler(EventHandler handler){

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& enm_dict = new 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的Dictionary

Struct為key的Dictionary

在查詢時,都會有裝箱帶來的消耗。

解決辦法是:

確保你的struct實現了 IEquatable&< T &>

確保你的structoverride Equals() and GetHashCode()

為枚舉為key的字典創建一個Custom Comparer

public enum EnmKey:int

{

a,

b,

c

}

public class EnmKeyTypeCompare : IEqualityComparer&

{

public bool Equals (EnmKey x, EnmKey y)

{

return x == y;

}

public int GetHashCode (EnmKey obj)

{

return (int)obj;

}

}

public Dictionary& enm_dict = new Dictionary&(new EnmKeyTypeCompare());

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會很慢,而且目前它也不支持各種排序,比如按名字,所以可以自己抽空寫一個簡單的profiler

3. 不同的遊戲平台要區分一下不同的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/8

9. 對於偏數據的數據結構,可以考慮使用struct替代class,這樣很多時候就避免了gcalloc

10. 困了o(╯□╰)o


推薦閱讀:

C#如何向C++生成的dll文件中傳遞二維數組?
零基礎學unity3d需要培訓么?
如何用unity做兩面互相反射的鏡子?
如何用高效的用 shader 實現柱狀圖?
哪裡有Unity3D遊戲開發的教程?

TAG:iOS開發 | 手機遊戲 | Unity遊戲引擎 |