unity在ios平台下内存的优化?

unity开发一个游戏,同一个场景,iphone下的内存消耗快到pc的两倍了。。。在pc下切换场景时可以做到内存回收,但是在iphone下内存回收的很…
关注者
231
被浏览
26,105

6 个回答

分享一些具体的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<Delegate>数据结构来实现常用的事件系统。下面是一份best praticle


public class EventDispather : MonoBehaviour {

public delegate void EventHandler();

public List<EventHandler> _handlers =new List<EventHandler>();

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<EnmKey,int> enm_dict = new Dictionary<EnmKey, int>();

// 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<EnmKey>

{

public bool Equals (EnmKey x, EnmKey y)

{

return x == y;

}

public int GetHashCode (EnmKey obj)

{

return (int)obj;

}

}

public Dictionary<EnmKey,int> enm_dict = new Dictionary<EnmKey, int>(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的话)

一方面是避免内存泄漏,另一方面是减少内存分配。

  • 避免内存泄漏,需要细心的去进行黑盒白盒检查,一般都是设计上的不合理造成的。同时可以善用 Destroy() 方法,强制释放非托管内存。最好弄清楚 Unity 的资源管理机制,这方面网上教程很多,我就不做搬运工了。
  • 减少内存分配,并不是说任何时候都不分配。在关卡进行时要将内存分配尽量减少,以降低 GC 的频率。可以用 Profiler 找出是所有分配了内存的地方,再根据经验判断是否要进行优化。我以前粗略的整理过一些会产生 GC 的操作,可供参考:
    • 生成一个新的委托,例如将方法做为参数传入
    • 对 List 进行 foreach
    • 用枚举做 Key 进行字典查找(可能是默认比较器 GetHashCode 时装箱引起的,提供自定义的比较器应该能解决)
    • 访问 animation 等组件
    • 获取 SkinedMeshRenderer.bones 或 Mesh.uvs 之类的属性
    • yield return 0 (建议全部替换为 yield return null)
    • 调用 GetComponentInChildren(建议自己实现一个无GC版本)