字符串和文本:
在Unity项目中,处理字符串和文本经常会产生性能问题。在C#中,字符串是不变的。任何对字符串的操作都会重新分配新的字符串,这个代价是非常昂贵的。如果在多重循环中重复地执行字符串连接操作,就会造成性能问题,特别是对长的字符串或者大的数据集操作的时候。
因此,把N个字符串连接起来就会分配N-1个中间的字符串,这样连续的操作就会对堆内存产生压力。
当我们需要在多重循环中或者每一帧对字符串进行操作时,记得使用StringBuilder来操作字符串。StringBuilder也还能被重用,以进一步减少内存的分配。
关于字符串的使用,详细内容也可以参考微软发布的文档:Best Practices for Using Strings in .NETdocs.microsoft.com
地域限制和顺序比较:
与字符串相关代码的一个核心问题就是无意中会使用默认的、慢的字符串API。这些API的目的是为了一些商业化的应用,能够处理出现在文本中的有关不同文化和语法规则的字符。
比如,下面的代码在一些使用美式英语的地区运行时,会返回true。在欧洲地区运行时,返回false。
提示:Unity脚本都是基于美式英语来运行的。
对于大多数的项目,这个完全没有必要。而且我们简单对每个字符进行比较,判断两个字符串是否相等,这速度大约会比使用上面的方式快10倍。当然,我们也可以调用String.Equals方法,然后设置比较类型StringComparison.Ordinal来实现:
低效率的内置字符串API:
除了上面讲的顺序比较外,也有一些C#的字符串的API效率比较低。比如:String.Format,String.StartsWith和String.EndsWith。以下是Unity给出的一些测试数据:
可见,如果是我们自己实现String.StartsWith和String.EndsWith,执行效率会高的多。实现方法也可以参考下图:
正则表达式:
在字符串匹配和操作字符串方面,正则表达式是非常消耗性能的。而且,C#类库也实现了正则表达式。所以,即使是调用IsMatch这样简单的函数,都会临时分配很多的内存。我们在开发中,出了初始化会临时分配内存外,应该不允许在其他地方临时分配较多内存。
如果一定要用正则表达式的话,注意不要使用静态的Regex.Match和Regex.Replace方法。这两个方法会动态的编译正则表达式,但不会缓存生成的对象。
下面是使用正则表达式的一个例子:
每一次以上的代码被执行,它都会生成5kb的内存垃圾。为了减少垃圾的生成,我们需要对以上代码进行重构:
在这个例子中,每一次调用myRegExp.Match只会产生320b的内存垃圾。对于简单的字符串匹配而言,它对内存的消耗依旧有点多,但与之前的例子相比,这已经是很大的改进了。
因此,如果正则表达式是不变的字符串常量,那么把它们当作第一个参数传递给Regex的构造器来预编译它们会更加有效。这些Regex对象也是可以被重用的。
XML, JSON和其他的长篇文本解析:
在loading时,解析文本通常是一项耗时的操作。有些情况下,解析文本所花的时间会超过loading和实例化Assets的时间。
这原因取决于我们用的文本解析器。C#内置的XML解析器是非常灵活的,但是,它不能对一些特殊的数据布局进行优化。
许多第三方解析器都是基于反射构建的。虽然在开发中使用反射是一个不错的选择(因为它能很好的适应数据布局的变化),但用反射是非常慢的。
Unity已经引入了一个带有内置JSONUtility API(可参考:
如果在文本解析中遇到性能问题,可以考虑以下的解决方案:
1.在Build时进行解析
当我们需要解析文本时,最好避免在游戏中进行这一步的操作。我们可以在Build的时候把文本解析成二进制文件。以加快读取时的速度。
2.数据分割和延迟加载
第二种情况情况是我们需要把能解析成小块的数据分割开来。一旦分割,数据解析就可以在不同时间进行。在理想的情况下,我们确定哪部分数据是会用到的,然后只加载用到的那部分数据。
3.线程
对于那些不需要用Unity API来操作的数据,可以在另外的线程中来解析它们。这可以提高多核CPU的利用率,是相当有用的。当然,我们在写代码时需要注意避免死锁。