第一篇介绍了在 .NET/Mono 和Unity里内存管理的基础,并且提供了一些避免不必要的堆分配的建议。第三篇会深入到对象池。所有的都主要是面向中级的C#开发者。

我们现在来看看两种发现项目中不想要的堆分配的方法。第一种-Unity profiler-实在是太简单了,但是却相当费钱,得买’pro‘版的。第二种是讲你的.NET/Mono程序集反汇编成中间语言(CIL)然后再检查。如果你从没见过反汇编的.NET代码,继续看下去,不难,而且免费还很有启发意义。

容易的方法:使用Unity profiler

Unity优秀的分析器主要被用来分析游戏中各种资源需要的性能和资源:着色器,纹理,音频,游戏对象等等。然而分析器在发掘内存上也一样有用-跟你的C#代码的行为有关-甚至是外部的 没引用UnityEngine.dll的.NET/Mono程序集!在当前Unity版本中(4.3),这个功能不是来自内存分析器,而是CPU分析器。到C#代码的时候,内存分析器只是展示Mono堆的总大小和已使用的量。

这样让你看你的C#代码是否有嫩村泄露实在太粗糙了。即使不适用任何脚本,已使用的堆大小也会持续增长和缩减。只要你使用脚本,你需要一个看哪里分配了内存的途径,然后CPU分析器刚好给你提供这个。

让我们来看看一些实例代码。假设下面的脚本绑定到了一个GameObject上。

using UnityEngine;using System.Collections.Generic;
public class MemoryAllocatingScript : MonoBehaviour{    void Update()    {
List<int> iList = new List<int>(new int[] {
072, 101, 108, 108, 111, 032, 119, 111, 114, 108, 100, 033 });
string result = "";
foreach (int i in iList.ToArray())
result += ((char)i).ToString();
Debug.Log(result);    }}

它所做的就是通过一组整数用一种绕的方法创建了一个字符串("Hello world!"),一路上造成了不必要的内存分配。多少呢?很高兴你问了,但是我很懒,就让我们看看CPU分析器吧。选中窗口顶部的”Deep Profiler“,可以跟踪到每帧的调用树。

正如你所见,堆内存在Update()函数过程中的5个不同位置被分配。这个列表的初始化,foreach循环里到数组的转换是多余的,每一个数字到字符的转换以及连接都需要分配内存。有趣的是,仅仅是调用Debug.Log()也会分配一大块内存-这点值得记下来,即使在生产环境中这段代码会被剔除。

如果你没有Unity Pro,但是恰巧有Microsoft Visual Studio,那就有替代Unity Profiler的方法来发掘调用堆栈。Telerik 告诉我他们的 JustTrace Memory profiler 有相似的功能 (see here). 然而, 我不知道它模仿Unity每帧记录调用树到了什么程度。更进一步,尽管对Unity项目的远程调试(通过UnityVS) 是可以的,我还是没有成功的把JustTrace用来分析被Unity调用的程序集。

只是稍微难一点点的方法:反汇编你的代码

CIL的背景知识

如果你已经有了一个.NET/Mono的反汇编器,开始用吧,不然我推荐ILSpy. 这个工具不仅是免费的,它还非常干净简单,但是刚好包含下面我们会用到的一个特殊功能。

你也许知道C#编译器不会将你的代码编译成机器语言,而是公共中间语言。这种语言是被原.NET团队作为一种包含两种来自高级语言特性的低级语言开发出来的。一方面,它与硬件无关,另一方面,它包含最适合被称为’面向对象’的特性,比如可以引用其他模块或者类的能力。

没有经过代码模糊处理( code obfuscator )的CIL代码是异常容易反向工程的。 许多情况下,结果几乎和原始的C#(VB)代码一样。ILSpy 可以替你做这件事,但是我们仅仅反汇编代码就可以了(ILSpy通过调用ildasm.exe来实现,.它是NET/Mono的一部分)。让我们从一个加两个整数的函数开始。

int AddTwoInts(int first, int second)
{    int result = first + second;           return result;}

如果你愿意,你可以将这段代码粘贴到MemoryAllocatingScript.cs文件里。然后确保Unity编译了它,再用ILSpy打开编译了的库Assembly-Csharp.dll。如果你选择AddTwoInts() 方法,你会看到下面的:

除了蓝色的关键字 hidebysig,我们可以忽略掉,方法签名应该看起来差不多。要了解到方法里主要发生了什么,你需要知道CIL把CPU看成一个堆栈式机器stack machine 而不是寄存器机器register machine。CIL假设CPU可以处理非常基础,非常算法的指令,例如”将两个整数相加“,而且它可以处理任何内存地址的随机访问。CIL还假设CPU不直接在RAM上进行算术操作,而是首先需要将数据装载进概念上的计算堆栈。(注意计算堆栈和你你知道的C#堆栈没有任何关系。CIL计算堆栈只是一个抽象的,并且预设很小。)在行IL_0000到IL_0005发生了:

  • 两个整型参数被推进堆栈。
  • 加法被调用然后从堆栈里弹出开始位置的两个对象,自动将记过压进堆栈。
  • 第3和4行可以忽略,因为在发行版本里会被优化掉。
  • 这个方法返回堆栈的第一个值。

找到CIL里面的内存分配

CIL代码美在它不会隐藏任何堆分配。而且,堆分配会严格按照以下三个顺序分配,在你的反汇编代码里能看到。

  • newobj <constructor>:这创建了一个由constructor指定类型的未初始化的对象。如果这个对象是值类型,它就在堆栈上被创建。如果它是一个引用类型,就在堆上。你总是能从CIL代码知道类型,所以你可以容易的知道内存分配产生的地方。
  • newarr <element type>:这条指令在堆上创建了一个新的数组。Element的类型由参数指定。
  • box <value type token>:这条特殊的指令执行装箱操作,我们已经在第一篇帖子里说过。

Let's look at a rather contrived method that performs all three types of allocations.

然我们来看一个人为的执行这三种内存分配的方法。

void SomeMethod()
{    object[] myArray = new object[1];    myArray[0] = 5;    Dictionary<int, int> myDict = new Dictionary<int, int>();myDict[4] = 6;    foreach (int key in myDict.Keys)    Console.WriteLine(key);}

有这几行代码产生的CIL代码很多,所以这里我们只看关键部分:

IL_0001: newarr [mscorlib]System.Object...IL_000a: box [mscorlib]System.Int32...IL_0010: newobj instance void class [mscorlib]System.    Collections.Generic.Dictionary'2<int32, int32>::.ctor()...IL_001f: callvirt instance class [mscorlib]System.    Collections.Generic.Dictionary`2/KeyCollection<!0, !1>    class [mscorlib]System.Collections.Generic.Dictionary`2<int32,    int32>::get_Keys()

正如我们怀疑过的,对象的数组(SomeMethod()里的第一行)导致newarr指令。整数5被赋给数组的第一个元素需要装箱。Dictionary<int, int>是被newobj指令分配的。

但是还有第四个堆分配!正如我在第一篇帖子里提到的,Dictionary<K, V>. KeyCollection被声明为一个类,不是结构。这个类的一个实例会被创建,这样foreach蓄奴换才有迭代的对象。不幸的是,分配发生在Keys属性的getter方法里。正如你在CIL代码里看到,这个方法的名字是get_Keys(),而且它的返回值是一个类。

作为一个查找内存泄露的通用方法,你可以生成一个对你的整个程序集反汇编的CIL文件,只要在ILSpy按下Ctrl+S。然后用你喜欢的文本编辑器打开这个文件,搜索上面提到的三种指令。查出其他程序集里的内存泄露是有难度。我唯一知道的办法就是仔细检查你的C#代码,确认所有的外部方法调用,并且一个个地查看它们的CIL代码。你怎么知道什么时候就完成了?很简单:你的游戏可以流畅的运行好几个小时,不因为垃圾收集造成任何的性能瓶颈。

PS:在之前的帖子里,我答应要向你们展示如何确认你们系统上的Mono版本。只要装了ILSpy,没有比这更简单的了。在ILSpy里,点击打开然后找到Unity根目录。找到Data/Mono/lib/mono/2.0然后打开mscorlib.dll。在层级视图里,找到mscorlib/-/Consts,然后那儿你能找到MonoVersion作为一个字符串常量。

Unity开发者的C#内存管理(中篇)相关推荐

  1. Unity开发者的C#内存管理

    很多游戏时常崩溃,大多数情况下都是内存泄露导致的.这系列文章详细讲解了内存泄露的原因,如何找到泄露,又如何规避. 我要在开始这个帖子之前忏悔一下.虽然一直作为一个C / C++开发者,但是很长一段时间 ...

  2. 给Unity开发者的C#内存管理(第一部分) C# Memory Management for Unity Developers (part 1 of 3)

    原文地址:http://www.gamasutra.com/blogs/WendelinReich/20131109/203841/C_Memory_Management_for_Unity_Deve ...

  3. Unity 3D中的内存管理与优化游戏运行性能的经验

    Unity3D在内存占用上一直被人诟病,特别是对于面向移动设备的游戏开发,动辄内存占用飙上一两百兆,导致内存资源耗尽,从而被系统强退造成极差的体验.类似这种情况并不少见,但是绝大部分都是可以避免的.虽 ...

  4. 【Unity】Unity 3D中的内存管理

    本文欢迎转载,但烦请保留此行出处信息:http://www.onevcat.com/2012/11/memory-in-unity3d/ Unity3D在内存占用上一直被人诟病,特别是对于面向移动设备 ...

  5. 全面理解Unity加载和内存管理

    转载自:http://www.ceeger.com/forum/read.php?tid=4394 最近一直在和这些内容纠缠,把心得和大家共享一下: Unity里有两种动态加载机制:一是Resourc ...

  6. 全面理解Unity加载和内存管理机制之二:进一步深入和细节

    Unity几种动态加载Prefab方式的差异: 其实存在3种加载prefab的方式: 一是静态引用,建一个public的变量,在Inspector里把prefab拉上去,用的时候instantiate ...

  7. Unity AssetBundle内存管理相关问题

    AssetBundle机制相关资料收集 最近网友通过网站搜索Unity3D在手机及其他平台下占用内存太大. 这里写下关于Unity3D对于内存的管理与优化. Unity3D 里有两种动态加载机制:一个 ...

  8. Unity动态加载和内存管理(三合一)

    原址:http://game.ceeger.com/forum/read.php?tid=4394#info 最近一直在和这些内容纠缠,把心得和大家共享一下: Unity里有两种动态加载机制:一是Re ...

  9. Unity内存管理的原理

    [前言] 当我们谈及Unity内存管理时,我们更多的是在说手游项目上如何更好的去管理内存,如果是在端游项目上,没有那么多讲究,内存随便用. [为什么手机上内存不够用] CPU读写速度远快于内存的速度, ...

最新文章

  1. JavaScript中的字符串操作(转)
  2. Binder相关面试总结(五):为什么Activity间传递对象需要序列化
  3. python opencv键盘监听
  4. 算法 --- 求两个集合的并集
  5. 还有什么事情AI做不了?
  6. 中招了,重写TreeMap的比较器引发的问题...
  7. mysql 分库分表 后怎么操作,MySQL要分表分库怎么进行数据切分?
  8. C#------如何获取本机IP地址
  9. 【总结】Transformer结构及其9中变体汇总!
  10. Idea自定义Maven骨架(archetype)
  11. 什么叫定向广告?定向传播有哪些好处
  12. 凸优化和非凸优化的区别
  13. python主流解析库(re beautifulsoup pyquery xpath)实战--爬取猫眼电影排行
  14. erp系统的优点和不足?云系统给企业带来的好处?
  15. 三面字节跳动被虐得“体无完肤”,15天读完这份pdf,终拿下美团研发岗offer
  16. linux下强制覆盖的密令
  17. 神经网络激活函数及其Katex公式代码模板合集
  18. 动态规划dp(带模板题の超易懂版):01背包,完全背包,分组背包,多重背包,混合背包
  19. 【重点】React.Component用法
  20. mybatis 常见错误:Mapped Statements collection does not contain value for com.*

热门文章

  1. 微信小程序连接蓝牙打印机打印图片示例
  2. 图片的质量压缩和二次采样
  3. centos启动停留在started GNOME display manager
  4. 使用链表进行奇偶分排 c语言
  5. 计算机两个用户怎么设置密码,电脑如何设置密码 电脑密码三个设置
  6. Direct2D (9) : 显示图像
  7. AI应用第一课:C语言支付宝刷脸登录
  8. 华为老员工谈华为终端的来龙去脉
  9. 怎样用计算机中的画笔,Word2010中怎样用画笔绘制表格
  10. 计算机应用基础课考试题B,大工《计算机应用基础》课程考试模拟试卷B