【Unity优化(一)】 内存和GC优化

⏳内存和垃圾回收原理

内存分为栈内存和堆内存,栈用来存储短期的和小块的数据,堆用来存储长期的和大块的数据。

Unity自动内存管理:

1.当一个变量被创建时,会在栈或堆内存池上申请一块内存空间。(值类型存在栈上,引用类型存在堆上。)

2.只要这个变量在作用域内,可以被代码访问。分配给它的内存在使用中,则称这款内存已被分配。根据内存空间位置,被称为栈上对象或者堆上对象。

3.如果这个变量不在作用域内,在代码中被释放了。内存不再被需要了,会返回到当初申请的内存池中,这个过程叫内存释放。当变量不在作用域时,栈上内存会立刻被释放。但堆上内存不会立刻释放,只是会把这块内存标记为等待释放,内存还是已分配状态。

4.调用GC垃圾回收时,会遍历已分配的堆内存,找到标记为“等待释放”的内存块,将其释放回内存池中。

栈内存

栈内存分配快速和简单,是因为栈元素都是小且存在很短时间,分配和释放内存总是按照预期的时间和大小。

栈工作像栈数据类型一样,元素只能按照严格的顺序添加或者移除。因为这种简洁和严格,所以很快。当一个变量存储在栈上时,内存简单的在栈的“末尾”被分配,当栈上的变量不在作用域时,存储它的内存马上被返还回栈以便重用。

堆内存

堆内存会存储长期和短期的数据,而且数据类型很多,而且大小不固定。
分配内存过程:
1.先判断堆上是否有足够大的连续空闲内存。
2.若内存不足,则进行GC垃圾回收。(过程较慢)
3.GC后,若还内存不足,则需要增加堆内存空间。(过程较慢)

垃圾回收过程

1.检查堆内存所有对象,和对象的引用,看对象是否在作用域内。

2.对不在作用域的对象进行标记。

3.删除被标记的对象,将其内存还给内存池。

对象多,对象的引用多,都会增加GC的工作。

触发垃圾回收的条件

1.当准备分配堆内存,但堆内存不足时。
2.Unity定时触发。
3.手动触发。

垃圾回收引起的性能问题

低帧率,间歇性卡死,性能不稳定。

在Unity Profiler的CPU分类中,以Time ms排序,如果GC.Collect()在上方,或者GC.Collect()的Time ms较大,可以考虑优化GC垃圾回收问题。

🏆优化

优化原理

1.降低GC执行时间。 较少的分配堆内存,和对对象的引用。

2.降低GC执行频率。降低对堆内存的分配和释放。

3.再合适的时机手动调用。较困难和不常用。

🌟优化方案

铺垫了这么多,终于到了优化的方法。

(1)缓存数据

如果代码重复调用堆内存,再抛弃结果,造成不必要的垃圾。那么我们应该保存结果的引用,并进行服用。这就是缓存技术。

实现:
不在方法中创建临时对象,而是创建全局对象。在方法中使用。

//下面例子中,函数每次被调用时都会造成堆内存分配,因为有新的数组创建。

void OnTriggerEnter(Collider other)
{
    Renderer[] allRenderers = FindObjectsOfType<Renderer>();
    ExampleFunction(allRenderers);
}
//下面代码只有一次堆内存分配,因为数组创建和填充一次,然后被缓存了。
//缓存数组可以复用而不用生成更多垃圾。
private Renderer[] allRenderers;

void Start()
{
    allRenderers = FindObjectsOfType<Renderer>();
}

void OnTriggerEnter(Collider other)
{
    ExampleFunction(allRenderers);
}

(2)不在频繁调用的函数中分配堆内存

把频繁在Update或LateUpdate中要调用声明的对象,改为在Start或Awake中进行声明。不在Update中new对象。

在Update中进行操作前进行判断,如判断是否需要修改坐标。或增加计时器。 降低代码执行频率。

(3)清除集合,而不是每次都创建新的

使用临时存储变量的List时,在使用前Clear一下即可。不要每次都New它。

(4)对象池

对于频繁创建和销毁的对象,如子弹,敌人等。使用对象池机制。

使用两个List,分别存储已显示的对象,和隐藏(表示被销毁)的对象。

需要创建对象时,判断隐藏对象池中是否有隐藏的对象,若有,则直接显示并使用此对象;若没有,则创建一个对象并使用,并将此对象放入显示对象池中。

需要销毁对象时,将此对象隐藏,放入隐藏对象池中。

(5)string字符串

string字符串是引用类型,而且每次修改都会创建一个新的引用。

1.不要频繁的拼接字符串,如时间的数字变量+单位名称。将其修改为两个UI,一个显示数字,另一个显示单位。

2.可以使用StringBuilder来拼接字符串使用。

(6)Unity函数调用的优化

有些Unity函数会造成堆内存分配,可以将其缓存起来,而不是每次都用UnityAPI进行查找使用。

使用GameObject.CompareTag()不会产生垃圾。使用GameObject.tag进行判断会产生垃圾。

(7)装箱和拆箱

要小心某些API底层调用了拆箱和装箱操作。

(8)协程的优化

yield 不会产生垃圾,但后面的参数会产生垃圾。

yield return 0;//这会产生垃圾是因为发生了装箱.
//如果我们只是想要等待一帧,而不产生垃圾
//最好是使用下面的代码:
yield return null;

缓存WaitForSeconds,而不是每次都new。

WaitForSeconds delay = new WaitForSeconds(1f);

while (!isComplete)

{

    yield return delay;

}

如果因为协程产生了很多垃圾,则要考虑自己写一个同功能的代码,看看是否可以优化性能。

(9)foreach循环

在Unity5.5之前,foreach循环数组以外的集合时,每次循环都会产生垃圾。这是因为幕后的装箱操作。每次循环开始System.Object都会在堆上被创建,在循环结束时被销毁。这个问题已经在Unity5.5版本中修复了。

(10)函数的引用

函数引用,不论是匿名方法还是命名的方法,在Uniyt中都是引用类型的变量。他们会引起堆内存分配。把匿名方法转换为闭包会显著的增加内存占用和堆内存分配的大小。

函数引用和闭包具体怎么明确的分配内存,取决于不同的平台和编译设置,但是考虑到垃圾回收,我们最好少使用函数引用和闭包。

匿名函数的两种形式:delegate开头,或者lambda表达式

	Action lawer;
	lawer=delegate() 			//使用匿名方法进行委托
     {
         Console.WriteLine("I have no money!!!");
     };
     
     lawer = () =>		//使用lambda表达式进行委托
     {
         Console.WriteLine("I have no money!!!");
     };

(11)少用Linq和正则表达式

他们都会产生垃圾,因为需要装箱操作,如果需要考虑性能问题,那么最好不要使用他们。

(12)结构体struct的优化

结构体虽然是值类型,但如果内部定义了string引用类型。那么GC时,会查找整个结构体。

如果结构体数组中,结构体定义包含了string。我们可以将string类型单独存放为一个数组。结构体中只定义值类型。

(13)减少对对象的引用

删除没必要的对象引用,堆内存上对象不变的情况下,引用越少,GC越快。

如:返回值是对象
如果返回值是对象,可以优化为对象的编号。

如果对你有帮助,可以点个赞鼓励我哈。

参考资料 https://www.cnblogs.com/alan777/p/6155501.html

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

真鬼123

祝你节节高升岁岁平安越来越漂亮

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值