Unity项目内存优化大全

发表于2016-11-10
评论1 2.5k浏览
前言
  手游项目开发中,每个项目都会遇到或多或少的内存问题。本文涉及到了Unity项目:启动内存,Mono内存,System 内存这三个方面。对于为什么标题叫《内存优化大全》,主要是在自己职业生涯中,《代码大全》给我带来了很多的启发和帮助。这篇文章希望帮到需要的人。     

启动内存
  在刚开发Unity项目时,遇到了一个非常棘手的问题。游戏启动时内存占用非常高(90M)。我使用二分法,排查是哪里分配的内存。但是结果令我非常的不解,因为当我用二分法,一直排除到程序启动至加载一个场景,一行代码都不执行,但App启动后内存占用缺还是很高(85M)。为了排除场景有未排除的代码, 新建了空白scene。 在google上搜索了好久关于启动内存高的问题,都没有得到答案。此刻只好怀疑到资源这块,通过删除Resources下的资源,神奇的事情发生了,启动内存降低了。
  直到看Unite 2006的开发者大会性能优化演讲,才看到Unity会根据Resources目录下的资源生产对应的Entity信息,在App启动时会加载所有的资源信息,资源越多,对应需要的内存越多,时间越久。这么重要的信息Unity官方尽然没有任何说明,这令我非常惊奇。
  我这边的项目解决办法就是使用AssetBundle,Resources目录下放少量的资源,解决了问题。官方在Unite 2016大会上也是给出的相同的方案,希望能帮到大家。 

heap 内存优化
  boxing
  值类型转换到应用类型时,需要在堆上申请内存
?
1
2
int a = 2;
object b = a;     // 这里会有内存申请
  项目中常见误用。enum类型当做Dictionary key, 由于会调用GetHashCode, Equals 会导致boxing
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class EnumCompare : IEqualityComparer
{
    public bool Equals(ConfigTypeEnum x, ConfigTypeEnum y)
    {
        return (x - y) == 0;
    }
 
    public int GetHashCode(ConfigTypeEnum obj)
    {
        return (int)obj;
    }
}
void TestDictionary()
{
    Dictionaryint=""> enumDic = new Dictionaryint="">(new EnumCompare());
    //Dictionary enumDic = new Dictionary();
    enumDic.Add(ConfigTypeEnum.System, 1);
    enumDic.Add(ConfigTypeEnum.Config, 2);
    enumDic.Add(ConfigTypeEnum.Network, 3);
 
 
    int len = 100000;
    for(int i = 0; i < len; ++i)
    {
        int id = enumDic[ConfigTypeEnum.System];         // 这里如果不添加EnumCompare,会调用object 的 Equals, GetHashCode
    }
 
    Dictionary<int, int=""> intDic = new Dictionary<int, int="">();
    intDic.Add(1, 1);
    intDic.Add(2, 2);
    intDic.Add(3, 3);
 
    for(int i = 0; i < len; ++i)
    {
        int id = intDic[1];                             // 这里没什么问题
    }
}

foreach
  使用foreach会在首次调用时生成一个Enumerator, 如果在Update里大量调用,还是非常影响效率的
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
List<int> m_userIdList = new List<int>
{
    100001,
    100002,
    100003
};
 
// bad
foreach(var iter in m_userIdList)
{
    Debug.Log("user id:" + iter);
}
 
// good
int idCount = m_userIdList.Count;
for(int i = 0; i < idCount; ++i)
{
    Debug.Log("user id:" + m_userIdList[i]);
}

unity api
  Unity api 返回数组时,总会返回一个新的数组,因此一定要注意调用次数
?
1
2
3
4
5
6
7
8
9
10
11
12
13
// bad
for(int i = 0; i < Input.touches.Length; ++i)
{
    Touch touch = Input.touches[i];
}
 
// good
Touch[] touchArr = Input.touches;
int len = touchArr.Length;
for(int i = 0; i < len; ++i)
{
    Touch touch = touchArr[i];
}
string
  初始化时,使用常量string.Empty,不要使用 ""
?
1
2
3
4
5
// bad
string str = "";
 
// good
string str = string.Empty;
  字符串连接,调用字符串operator+,产生零时变量, string 构造会在堆上申请内存。如果调用量大,使用StringBuilder
?
1
2
3
4
5
6
7
8
9
string str1 = "hello,";
string str2 = "world";
 
// bad
string str = str1 + str2;
 
// good
System.Text.StringBuilder sb = new System.Text.StringBuilder();
str = sb.Append(str1).Append(str2).ToString();
匿名函数
  匿名函数的本质是生成实例对象,会在堆上申请内存
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private int Cmp(int a, int b)
{
    return a - b;
}
 
private void TestDelegate()
{
    List<int> idList = new List<int>();
 
    // bad
    idList.Sort(delegate(int a, int b)
    {
        return a - b;
    });
 
    // good
    idList.Sort(Cmp);
}int>int>
  匿名函数用起来确实简介明晰,但是这是游戏开发,如果在Update 每帧调用,这里的优化还是非常有必要的。

成员变量
  先看代码,后面说明STL的最佳实践
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Player
{
    private List<int> m_buffIdList;
    private List m_effectList;
    private const int kBuffIdMaxCount = 100;
    private const int kEffectMaxCount = 200;
 
    public void Init()
    {
        m_buffIdList = new List<int>(kBuffIdMaxCount);
        m_effectList = new List(kEffectMaxCount);
    }
 
    public void Dispose()
    {
        m_buffIdList.Clear();
        m_effectList.Clear();
    }
}
  Unity的Mono heap 内存只会增长,不会减少,这句话是需要深入理解
a、Mono heap 由于不会整理内存空间,所以当内存出现空洞,但是需要新内存时,洞的空间不够,则申请新的内存
b、如果一开始全部申请好所有所需的内存空间,Mono heap内存并不会增长

  对应到成员变量的使用,在STL 内存申请方面,如果内存容量不足,则申请2的N次方空间,所以如果能知道最大使用空间,即可很好的节省不必要的申请。
并且当调用Clear 函数时,内存并不会被回收,只是将index置为0

内存碎片
  针对内存碎片,最好的方式就是用对象池,重复使用对象,这里需要注意的是在设计重复使用的对象一定要实现好Dispose函数,和Init函数。

资源内存
  对于资源占用内存,主要是资源需要有相关的规定,以及参数,参数部分需要考虑一下几点:
a、贴图设置为read,如果write的话,内存里会有一份拷贝
b、UI贴图勾选掉minimap
c、贴图合并为2的n次方大图

在合适的实际调用函数,比如场景切换时:
?
1
2
Resources.UnLoadUnUsedAssets();
System.GC.Collect();
总结
  合理的美术资源规范,以及流程,加上对Unity内存使用和回收的深入理解,是保证项目质量的不二之法。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引