Unity项目内存优化大全
发表于2016-11-10
前言
手游项目开发中,每个项目都会遇到或多或少的内存问题。本文涉及到了Unity项目:启动内存,Mono内存,System 内存这三个方面。对于为什么标题叫《内存优化大全》,主要是在自己职业生涯中,《代码大全》给我带来了很多的启发和帮助。这篇文章希望帮到需要的人。
启动内存
在刚开发Unity项目时,遇到了一个非常棘手的问题。游戏启动时内存占用非常高(90M)。我使用二分法,排查是哪里分配的内存。但是结果令我非常的不解,因为当我用二分法,一直排除到程序启动至加载一个场景,一行代码都不执行,但App启动后内存占用缺还是很高(85M)。为了排除场景有未排除的代码, 新建了空白scene。 在google上搜索了好久关于启动内存高的问题,都没有得到答案。此刻只好怀疑到资源这块,通过删除Resources下的资源,神奇的事情发生了,启动内存降低了。
直到看Unite 2006的开发者大会性能优化演讲,才看到Unity会根据Resources目录下的资源生产对应的Entity信息,在App启动时会加载所有的资源信息,资源越多,对应需要的内存越多,时间越久。这么重要的信息Unity官方尽然没有任何说明,这令我非常惊奇。
我这边的项目解决办法就是使用AssetBundle,Resources目录下放少量的资源,解决了问题。官方在Unite 2016大会上也是给出的相同的方案,希望能帮到大家。
heap 内存优化
boxing
值类型转换到应用类型时,需要在堆上申请内存
手游项目开发中,每个项目都会遇到或多或少的内存问题。本文涉及到了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; // 这里会有内存申请 |
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() { Dictionary |
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; |
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 > |
成员变量
先看代码,后面说明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内存使用和回收的深入理解,是保证项目质量的不二之法。