unity在ios平台下内存的优化?
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版本)