Unity有个消息系统,该系统用来实现游戏运行时脚本内部方法的调用。这是个非常简单和容易理解的概念,特别对新用户来说。只需定义一个Update方法,便可以每帧调用该方法中的内容。
 
 
一个经验丰富的开发者肯定会对此产生疑问:
1.不清楚这个方法究竟是如何被调用的。
2.不清楚当一个场景中有多个对象时,这些方法是如何调用顺序。
3.这种代码风格不是十分智能。
 
UPDATE是怎么被调用的
 
Unity并没有使用System.Reflection进行方法的定位。取而代之的是,首先给定类型的MonoBehaviour通过底层脚本进行检查,判断脚本运行过程中(无论是Mono或IL2CPP)是否有方法被定义,同时其中内容有没有被缓存。如果检查到特定的方法,便将其添加到一个合适的列表中。例如,当一个脚本中定义了Update方法后,这个脚本便被添加至一个需要每帧更新的脚本列表中。在游戏过程中,Unity只需要重复执行所有列表中的方法既可。所以,你的Update方法究竟是public还是private并不重要。
 
UPDATE方法们的执行顺序是什么
 
执行顺序由脚本执行顺序设置(Script Execution Order Settings)(菜单:Edit > Project Settings > Script Execution Order)决定。要手动设置1000个脚本的执行顺序不是什么好主意,但是要微调某些特定脚本的执行顺序还是可以的。当然,未来我们将会提供更加方便的方式来指定执行顺序,比如在代码中使用一个特性(Attribute)。
 
无法使用INTELLISENSE
 
在Unity中,我们使用某类IDE编译C#脚本,这些IDE大多无法识别这些特定方法应该在何处被调用。因此常会导致警告以及玳瑁导航困难。一些开发者用一个叫BaseMonoBehaviour或差不多名字的抽象类扩展MonoBehaviour,然后在他们的项目中的每个脚本里都扩展这个类。他们在其中写了一些有用的功能以及一堆虚的特殊方法:
 

这个结构可以使你在代码中使用MonoBehaviour时更有逻辑性,但存在一个小缺点。我打赌你已经发现了……你所有的MonoBehaviour都会储存在Unity的内部更新列表里,你所有的脚本都会在每帧里调用所有这些基本上什么也没用的方法!有人可能会问为什么会有人关心一个空方法?因为这些从C++到托管C#的调用有成本上的开销。让我们来看看成本为何。
 
调用10000个UPDATE
 
我为这篇文章在Github上创建了一个示例项目,大家可以前往以下链接下载:https://github.com/valyard/Unity-Updates它有两个场景,可以通过点击设备或在编辑器中按任意键互相切换:(1) 在第一个场景中,使用下面这样的代码创建了10000个MonoBehaviour:
 

(2) 在第二个场景中,创建了另外10000个MonoBehaviour。不过,不同的是,这个代码中并不是只调用Update,而是像下面这样,加入了一个由Manager脚本在每帧都调用一次的自定义UpdateMe方法。
 

测试项目在两台iOS设备上被编译为Mono以及IL2CPP,发布设置中都设为非开发模式。它们的运行时间记录如下:1.在第一次Update调用时设置一个Stopwatch (在Script Execution Order中配置)2.在LateUpdate时停止Stopwatch
3.将获得的计时时间均摊到几分钟上
Unity版本: 5.2.2f1iOS版本: 9.0

Mono
 

哇!好多时间!测试肯定哪里出了问题!实际上,我只是忘了把Script Call Optimization 设为Fast but no exceptions,但是现在我们能看到这种设置对性能的影响了……至于IL2CPP不必太在意。

Mono (fast but no exceptions)
 

OK,这样好多了,让我们切换到IL2CPP。

IL2CPP
 
这里我们发现两件事情:1.这个优化对于IL2CPP同样有用2.IL2CPP仍有改进空间,而且在写这篇文章的同时Scripting 与IL2CPP团队正在努力提高性能。比如,最新的Scripting分支内包含的优化可以让测试运行快35%。
我接下来就会介绍Unity在幕后做了些什么,但是现在让我们修改下Manager代码,将它提速5倍!
 
接口调用,虚调用以及数组访问

结果告诉我们,如果你想在每帧里都循环迭代拥有10000个元素的列表,那应该使用数组而不是List,因为这样生成的C++代码会更简单,而数组访问就是要快很多的。在下一个测试中,我把List<ManagedUpdateBehavior> 改为了ManagedUpdateBehavior[]。
 

这看起来好多了!
 
解救之道!
 
我们发现了从C++调用C#函数较慢,不过让我们再研究下当调用所有这些对象的Update方法时,Unity实际上做了些什么。最简单的方法就是使用Apple Instruments的Time Profiler。注意这不是Mono与IL2CPP 的对比测试 — 讨论的大多数内容对Mono iOS构建同样适用。我在iPhone6上用Time Profiler启动了测试项目,记录了几分钟的数据,然后选择了一分钟检视一次。从这行代码开始的所有东西我们都很感兴趣:void BaseBehaviourManager::CommonUpdate<BehaviourManager>()
如果你以前没有使用过Instruments,右边是按照执行时间排序的函数,以及它们调用的其他函数。最左边的列是以毫秒为单位的CPU时间,以及这些函数及其调用的函数所占的CPU时间百分比。左边第二列是函数自己的执行时间。注意,在这个实验中Unity并没有将CPU使用完,所以我们能看到在60秒间隔内有10秒的CPU时间花在了我们的Update上。显然,我们关心的是那些执行时间最长的函数。用疯狂的Photoshop技术,将一些区域做了颜色区分,以便你能明白到底发生了什么。
 

UpdateBehavior.Update()
 

在中间你能看到我们的Update方法,以及IL2CPP是如何调用它的 ——UpdateBehavior_Update_m18。但是Unity在那之前还做了很多其他事。循环迭代所有的Behaviour
 

Unity循环迭代所有的Behaviour并执行更新。特殊的迭代类SafeIterator确保了即使移除了列表中的下一项,整个循环也不会中断。仅仅是循环迭代所有已注册的Behaviour就用了9979ms中的1517ms。检测调用是否有效
 

下一步,Unity做了一堆检测,确保调用的方法是属于某个已激活已初始化且Start方法已调用过的GameObject的。你肯定不希望在Update里销毁一个GameObject时让游戏崩溃,对吧?这些检测花去了整个9979ms中的另外2188ms。准备调用方法
 

Unity创建了一个ScriptingInvocationNoArgs实例 (代表了一个从原生到托管的调用)以及ScriptingArguments,然后命令IL2CPP虚拟机调用方法(scripting_method_invoke函数)。这一步消耗了整个9979ms中的2061ms。调用方法
 

scripting_method_invoke函数检测传入的参数是否有效(900ms),然后调用IL2CPP 虚拟机的Runtime::Invoke方法 (1520ms)。开始时,Runtime::Invoke检测方法是否存在 (1018ms)。而后,它调用一个生成的RuntimeInvoker函数获取方法签名(283ms)。接着再依次调用我们的Update函数,根据Time Profiler,这一步花了42ms。一个漂亮的彩色表格。
 

托管的更新
 
现在让那个我们在Manager测试上使用下Time Profiler。你在屏幕截图上可以看到,还是同样的一些方法(有些方法因为执行时间少于1ms,甚至都没出现),但是大部分的执行时间实际上都花在了UpdateMe函数上(或者说花在了IL2CPP调用ManagedUpdateBehavior_UpdateMe_m14上)。另外,IL2CPP还插入了一个null检测,确保我们循环迭代的数组不会为null。下面这个图片使用了相同的颜色。
 

所以,你现在怎么看,我们应该忽略那小小的方法调用吗?
 
有关测试的几句话

老实说,这个测试并不是完全公平的。Unity为了防止你的游戏出错或崩溃,做了很多了不起的事:这个GameObject是否已激活?它是否在Update循环中被销毁了?对象上是否存在Update方法?怎么处理在这个Update循环中创建的MonoBehaviour?——我的Manager脚本没有处理这其中任何一项,仅仅是循环迭代了一堆的对象,调用它们的Update而已。在真实世界中,Manager脚本可能会更加复杂,执行得更慢。但是,我是个开发者——我知道我的代码要做什么,我架构我的Manger类时,知道可能的行为是什么,什么不会出现在我的游戏中。而不幸的是,Unity并不知道这些。
 
你应该怎么做?

当然这完全视你的项目而定,但实战中碰到一个游戏在单一场景中使用大量需要在每帧都执行一些代码的GameObject的情况并不少见。通常这看起来都是些不起眼的小代码,似乎不会影响到任何东西,但当其数量非常巨大时,调用几千个Update方法的开销将变得显著。这个时候再去修改游戏架构,重构这些对象为Manager样式,可能已经为时已晚。你现在有数据了,在你开始下一个项目时考虑下吧。

来源:Unity官方社区