启动
Unity启动时,有两个方法(函数)比较重要,它们耗时较多。
上面的截图是在iOS设备上运行一个示例程序时,通过软件Instruments得到的,它记录了Unity启动时访问的方法。在特定平台上才有的startUnity方法中,有两个方法要注意,UnityInitApplicationGraphics 和UnityLoadApplication。
UnityInitApplicationGraphics 执行了很多内部工作。例如设置一些图形设备,以及很多Unity的内部系统。另外,它还初始化了资源系统。为了实现这一点,它必须加载一个资源系统中所包含的所有文件的索引。
每一个“Resources”文件夹中的每一个资产文件(注意:这只包括项目“Assets”文件夹中的Resources文件夹,和Resources文件夹中的子文件夹。)都包括在资源系统的数据中。因此,初始化资源系统的时间基本和Resources文件夹中的文件呈线性关系。
UnityLoadApplication 包含了加载和初始化项目中的第一个场景的函数。这包括反序列换和实例化所有需要用来显示第一个场景所需的数据,例如编译着色器,上传纹理,实例化GameObjects。另外,第一个场景中的所用MonoBehaviours在此时执行它们的Awake回调函数。
这些过程意味着第一个场景中的任何一个Awake回调函数中如果有要跑很长时间的代码,那么它可能就是拖慢程序启动速度的罪魁祸首。若要加快速度,要么删掉它,要么把它移到应用程序生命周期中的其他地方。
运行时
运行时有个很重要的方法Playerloop,它是Unity的主循环。它里面的代码每帧执行一次。
上面这个截图是一个Unity5.4版本的项目,展现了几个PlayerLoop中最有意思的方法。注意它们的名字在各个版本中可能会不一样。
PlayerRender是运行Unity渲染系统的方法。包括了对象的剔除、计算动态batch、向GPU提交绘制指令。任何图像效果或者基于渲染的脚本回调函数(例如OnWillRenderObject)也在这里运行。总的来说,若项目时可交互的,那这可能是最消耗CPU的地方了。
BaseBehaviourManager 调用CommonUpdate的三个模板。它们会调用附加在激活的GameObject上的MonoBehaviours中特定的回调函数。
CommonUpdate 调用Update 回调函数
CommonUpdate 调用 LateUpdate 回调函数
CommonUpdate 调用 FixedUpdate(如果物理系统被勾选)
总的来说, BaseBehaviourManager::CommonUpdate 是最有意思的方法家族了, 它是大多数脚本代码的进入点。
另外有几个方法也很有意思:
UI::CanvasManager会调用几个不同的回调函数,如果项目使用Unity UI的话。 它包括Unity UI’s batch computation和layout 更新; the two operations that most often cause CanvasManager to appear in the profiler.
DelayedCallManager::Update运行coroutines. 在“Coroutines”一章中会有更多介绍。
PhysicsManager::FixedUpdate运行PhysX物理系统. 它主要调用运行 PhysX 的内部代码, 受当前场景中物理对象数量的影响, 例如缸体和碰撞器。 当然,基于物理的回调函数也出现在 OnTriggerStay和OnCollisionStay.
如果项目在使用2D physics, 也会在Physics2DManager::FixedUpdate下出现类似的调用。
脚本
当脚本被调用的时候,会使用一个叫 ScriptingInvocation 的对象。就是在这里,Unity的内部本地代码过度到脚本运行时。(注意:技术上,在跑过 IL2CPP 后, C#/JS脚本代码也变成了本地代码。当然,交叉编译的代码通过IL2CPP运行时框架执行方法(函数),和手写的C++代码不太相似)。
上面的截图也是用Unity5.4跑的。所用嵌套在RuntimeInvoker_void这一行下方的都是交叉编译的C#脚本的一部分。它们每帧执行一次。
这里的每一行都是原始类的名字加上下划线,再加上原始方法。通过展开这些方法,可以很清楚地看到是谁在拖慢CPU的速度。这包含了项目中的其他脚本方法(函数),Unity APIs 和 C# 库代码。
上图中的StandaloneInputModule.Process 方法是覆盖整个UI界面的光线投射,它每帧调用一次,用来检测所用的触碰事件,有没有悬停或激活某些UI组件。最基础的代价是遍历所用的UI组件,并测试鼠标的位置是否在包围矩形内。
资产加载
执行资产加载的主要方法(函数)是SerializeFile::ReadObject。这个方法连接了一个二进制流到Unity的序列化系统,这个操作是通过一个叫 Transfer 的方法(函数)完成的。这个Transfer方法可以在所有的资产类型上找到,例如纹理。
在上面的截图中,加载了一个场景。这需要Unity根据SerializeFile::ReadObject下各个不同的Transfer函数读取和反序列化场景中所有的资产。
总的来说,如果在运行中画面断断续续并且显示SerializedFile::ReadObject占用了大量的时间,那么问题出在资产加载上。需要注意的是,SerializedFile::ReadObject大多出现在主线程中,只有在同步资产读取时需要借助SceneManager,Resources 或者AssetBundle APIs。
解决这个问题的常用手段是用并行的方式读取资产,或者提前读取一些大型资产。
注意:Transfer函数也会在复制对象的时候被调用(被CloneObject方法使用)。如果Transfer在CloneObject底下出现,那么资产则不是从硬盘上读取过来。只是老对象的数据被转移到新的对象上。为了做到这一点,Unity会序列化老对象并反序列化结果数据变成新对象。