【译】移动端优化之垃圾回收器

发表于2016-04-25
评论1 8.6k浏览

移动端优化之垃圾回收器                                      

       原文链接:http://blog.theknightsofunity.com/mobile-optimization-garbage-collector/

       版权声明:原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权;

                                                                                 优势、技巧

什么是垃圾回收器?让我们来看看MSDN的定义.

       基于.NET框架的垃圾回收器为你的应用程序管理内存的分配和释放。每当你创建一个新的对象时,通常语言运行时从管理堆中为对象分配内存。在管理堆中会有相应长度的地址空间可用。运行时会继续为新的对象分配内存空间。然而,内存并不是无限的。最终这个垃圾回收器必须执行一个收集器来释放掉一些内存….

        如果C#或者是JavaScript是你学习第一门(仅仅是)程序语言。那很可能你没有意识到管理应用程序内存是多么的难。像C/C++那样的语言,你需要决定这些内存什么时候应该分配什么时候应该释放,。只要是犯了一个错误,你的程序就会发生内存泄露或者立即崩溃。

       当然,如果这些内存难以管理那么是时候考虑下改善它了。如今我们生活在一个快节奏的世界中,我们的产品希望尽快发布到市场中去。所以我们尝试寻找一种可以让一切变得更加简单的方法。其中一个方法就是垃圾回收器的方法可以管理释放这些内存。

       听起来非常棒,是不是?是的,垃圾回收器是一个现实生活的节俭者。但是它也有缺点。垃圾回收器垃圾回收器会时常运行,并且会让你的应用程序在收集开始时暂停一会(这里有一个垃圾回收器的模式,它允许你不暂停程序就可以启动,但是你不能调整Unity垃圾回收器的设置),当垃圾回收器应该被启动时你还需要一些控制。对于界面通常为静态的商业程序来说这不算是一个问题,但是对于游戏来说可能是一个问题,因为在游戏里面短暂的停止有可能会导致玩家失去整个游戏。所以玩家讨厌在关键时刻因为一个不是自己造成的错误而丢失游戏操作。


游戏与垃圾回收器

       上一次,我们谈到一些关于Unity分析器和垃圾回收器。这个规则很简单 —那就是我们不想让垃圾回收器来开始它的工作。为此,我们需要停止产生垃圾。当然不产生垃圾是完全不可能的。但是我们可以一次性回收产生垃圾的总量。所以垃圾回收器会10分钟运行一次而不是10秒运行一次。


使用对象池

       让垃圾产生最少的技术之一就是再利用已经存在的对象来取代创建新的对象,这种技术被成为对象池,并且我们上次已经谈论过了。在大多数情况下使用这个技术会让你受益匪浅。所以这才是你应该关注的事情。这里有一个非常好的免费的库来让你创建你自己的对象池。它称为Smooth Fundations并在你可以在Asset Store上下载到。

       不幸的是,网站上的文件已经不存在了,但是代码却真的非常易于搞懂。这里有一个对想池的示例,它介绍了创建一个带有SpriteRenderer组件的GameObject的对象池。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

public class Pools

{

    public static class SpriteRenderer

    {

        private static readonly Pool _instance = new Pool(

            () =>

            {

                var go = new GameObject();

                return go.AddComponent();

            },

            sr =>

            {

                sr.sprite = null;

                sr.color = Color.white;

            });

 

        public static Pool Instance { get { return _instance; } }

    }

}

       从第6行到第10行我们创建一个托管来定义这个池对象应该怎样被创建的,11行到15行的代码负责重置对象到初始状态(在释放之后)

       然后,这个对想池就可以使用这个方法。

1

2

3

4

5

SpriteRenderer spriteRenderer = Pools.SpriteRenderer.Instance.Borrow();

 

// do something with spriteRenderer

// and when it is no longer required:

Pools.SpriteRenderer.Instance.Release(spriteRenderer);


使用数组

       你可能要使用数组,但是请记住不要在运行的时候分配它们。如果任何一个函数需要返回一个数组的值,使用一个现有的数组并把它作为一个参数。

       常规方法

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

void Start()

{

    int[] numbers = GetNumbers();

 

    for (int i = 0; i < numbers.Length; ++i)

    {

        Debug.Log("Got number: " + numbers[i]);

    }

}

 

int[] GetNumbers()

{

    var arr = new int[3]; // will allocate an array each time called

    arr[0] = 1;

    arr[1] = 2;

    arr[2] = 3;

    return arr;

}


       GC-友好的方法

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

// allocated only once

private static readonly int[] intArray = new int[32];

 

void Start()

{

    int size = GetNumbers(ref intArray);

 

    for (int i = 0; i < size; ++i)

    {

        Debug.Log("Got number: " + intArray[i]);

    }

}

 

int GetNumbers(ref int[] arr)

{

    arr[0] = 1;

    arr[1] = 2;

    arr[2] = 3;

    return 3;

}

       正如你可能已经注意到的,使用gc-友好方法,代码需要更多的工作量,首先,你需要知道它的上限(在这个例子中是32).你还需要确定算法中的限制没有让数组越界。否则你的游戏将会崩溃,然后如果你想遍历整个数组,你需要使用值的大小而不是使用数组的大小。

       注意,在这里我使用了ref 关键字, 使用ref 意味着 整个值应该通过引用的方式传递,但是由于数组总是以引用方式传递,所以在这个例子中没有需要使用。唯一一个原因就是它已经使用了,为了增加GetNumBers()方法调用的可读性。如果你看到一个 ref ,你就会明白一些通过参数将值传递出来。


使用Lists

       直接使用数组可能不是最好的选择。取而代之的是,你可以使用List. List实际上是一个被一些非常有用的处理代码包装起来的数组。保证了你安全,你没必要去担心数组的大小过大(和其他的事情也一样)。

       但是你还是需要小心。如果你超过了这个List的数组大小,它会分配一个新的更大点的数组。因为我们想让分配的数量最小化。总是使用构造函数来创建Lists会占用容量。

       1

var list = new List(32);

       怎样遍历整个Lists? 你可能知道也可能不知道。但是Unity自带的Mono编译器(当前作者使用Unity版本是5.2.3)使用foreach循环整个集合的时候分配内存会有一个BUG。它不是很多,仅仅24 bytes但是多次执行后会很快消耗掉你的内存并触发垃圾收集器。解决方法是使用for 循环 来循环遍历。

       这里有一个示例代码,记住这次我们没有需要一个记录大小的变量

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

// allocated only once

private static readonly List intList = new List(32);

 

void Start()

{

    GetNumbers(ref intList);

 

    for (int i = 0; i < intList.Count; ++i)

    {

        Debug.Log("Got number: " + intList[i]);

    }

}

 

void GetNumbers(ref List list)

{

    list.Clear();

    list.Add(1);

    list.Add(2);

    list.Add(3);

}


合并 strings

       这里有一些事情你不能简单的绕过,其中之一就是string的合并。当合并完成后。它总是会分配一个新的string.你可以通过使用StringBuilder来取代添加(+)运算符从而可以对这个过程进行一些优化。

       添加(+)运算符的方法:

1

2

3

4

int points = 5;

int total = 10;

string str = "You have " + points + " out of " + total + "!";

Debug.Log(str);

使用StringBuilder的方法:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

int points = 5;

int total = 10;

 

using (var disposable = StringBuilderPool.Instance.BorrowDisposable())

{

    StringBuilder sb = disposable.Value;

    sb.Append("You have ");

    sb.Append(points);

    sb.Append(" out of ");

    sb.Append(total);

    sb.Append("!");

 

    Debug.Log(sb.ToString());

}

       你是否注意到?我使用了一个指令从pool中获取一个Disposable对象?这是使用Pools的另一种方式。这个方式非常安全。因为你不想让Borrowed对象回收。


这就完了?

       当然不是,还需要注意的一件事就是,请时刻记住在Unity编辑器中运行你的代码会比程序运行目标设备上产生更多的垃圾。当你试图优化垃圾的时候,你可能会发现自己处于这样一种情况,那就是它在目标设备上没有产生垃圾时。这是因为Unity会做一些很好的优化。而你应该经常在已经建立的游戏中运行分析器来了解什么是应该优化的什么是不应该优化的

 

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