http://gad.qq.com/article/detail/7173927
前言
手游项目开发中,每个项目都会遇到或多或少的内存问题。本文涉及到了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内存使用和回收的深入理解,是保证项目质量的不二之法。