对应用程序进行性能分析时,可能会遇到一些常见问题。本页概述了如何调查一些常见性能问题的原因。
在查看启动时间的跟踪记录时,需要关注两个关键方法:UnityInitApplicationGraphics 和 UnityLoadApplication。这两个方法是项目的配置、资源和代码可能影响启动时间的主要原因。
注意:应用程序的启动时间因平台而异。在大多数平台上,启动发生在启动画面出现时。
在上面的截屏(在 iOS 设备上运行的 Unity 项目示例的 Instruments 跟踪记录)中,在特定于平台的 startUnity 方法中,请注意 UnityInitApplicationGraphics 和 UnityLoadApplication 方法。
UnityInitApplicationGraphics 执行大量内部工作,例如设置图形设备和初始化 Unity 的大量内部系统。它还通过加载资源系统中包含的所有文件的索引来初始化资源系统。
Unity 的资源系统在其数据中包含了项目 Assets 文件夹中 Resources 文件夹中的每个资源文件。其中包括 Resources 文件夹的子文件夹中的所有文件。因此,初始化资源系统所需的时间与应用程序项目的 Resources 文件夹中的文件数量呈正相关关系。
UnityLoadApplication 包含加载并初始化项目的第一个场景的方法。其中包括反序列化并实例化显示第一个场景所需的数据,例如编译着色器、上传纹理和实例化游戏对象。此外,Unity 还会执行第一个场景中所有 MonoBehaviour 的 Awake 回调。
这就意味着,如果在项目的第一个场景中的 Awake 回调中存在任何执行时间很长的代码,那么该代码可能会导致项目的初始启动时间的延长。解决此问题的方法是删除这些运行速度慢的代码,或者在应用程序生命周期的其他地方执行该代码。
对于在初始启动时间之后的性能分析跟踪记录,最需要关注的是 PlayerLoop 方法。这是 Unity 的主循环,该循环中的代码每帧运行一次。
上面的截屏说明了 PlayerLoop 中几种对性能影响最大的方法。注意:PlayerLoop 中的方法的名称可能因 Unity 版本而异。
PlayerRender 是运行 Unity 渲染系统的方法。此方法进行的操作包括剔除对象、计算动态批次以及向 GPU 提交绘图指令。任何图像效果或基于渲染的脚本回调(例如 OnWillRenderObject)也在此时运行。通常情况下,当项目进行交互时,此方法应该对 CPU 时间的消耗最大。
BaseBehaviourManager 调用三个模板化版本的 CommonUpdate。这些会调用当前场景中附加到处于活动状态的游戏对象的 MonoBehaviour 中的特定回调:
CommonUpdate<UpdateManager> 调用 Update 回调CommonUpdate<LateUpdateManager> 调用 LateUpdate 回调CommonUpdate<FixedUpdateManager> 调用 FixedUpdate
通常情况下,BaseBehaviourManager::CommonUpdate<UpdateManager> 是最适用于检查的方法族,因为它是 Unity 项目中运行的大多数脚本代码的入口点。
还有其他几种方法可用于检查:
UI::CanvasManager 将调用多个不同的回调。其中包括 Unity__ UI__(即用户界面,User Interface)让用户能够与您的应用程序进行交互。Unity 目前支持三种 UI 系统。更多信息CanvasManager 出现在性能分析器中的最常见原因。DelayedCallManager::Update 运行协程。PhysicsManager::FixedUpdate 运行 PhysX 物理系统。这主要涉及运行 PhysX 的内部代码。当前场景中物理对象(例如 Rigidbody 和 Collider)的数量会影响 PhysX 的内部代码。基于物理系统的回调也会出现在此处,例如 OnTriggerStay 和 OnCollisionStay。如果项目使用 2D 物理系统,那么会在 Physics2DManager::FixedUpdate 下显示为一组相似的调用。
在使用__ IL2CPP__种由 Unity 开发的脚本后端,可在为某些平台构建项目时替代 Mono。更多信息
See in Glossary 交叉编译的平台上调用脚本时,应查找包含 ScriptingInvocation 对象的跟踪行。这表示 Unity 的内部原生代码转换到脚本运行时以便执行脚本代码。注意:从技术上讲,Unity 通过 IL2CPP 运行 C# 代码后,该代码也会成为原生代码。但是,这种交叉编译后的代码主要通过 IL2CPP 运行时框架来执行方法,与手写的 C++ 不太一样。
在上面的截屏中,嵌套在 RuntimeInvoker_Void 行下的方法是 Unity 每帧执行一次的交叉编译的 C# 脚本的一部分。
跟踪行的名称是原始类的名称,后跟下划线和原始方法的名称。对于此跟踪记录示例,可以看到 EventSystem.Update、PlayerShooting.Update 和其他几个 Update 方法。这些是 MonoBehaviours 中常见的标准 Unity Update 回调。
展开这些方法后,可以查看其中哪些方法消耗了 CPU 时间。这包括项目中的其他脚本方法、Unity API 和 C# 库代码。
上面的跟踪记录显示,StandaloneInputModule.Process 方法每帧对整个 UI 进行一次光线投射。此方法检测是否有任何悬停的触摸事件或是否激活了任何 UI 元素。迭代所有 UI 元素并测试鼠标的位置是否在UI 元素的边界矩形内的方法属于资源密集型方法。
还可以在 CPU 跟踪记录中识别出资源加载记录。标识资源加载的主要方法是 SerializedFile::ReadObject。此方法将二进制数据流通过名为 Transfer 的方法从文件连接到 Unity 的序列化系统中。Transfer 方法位于所有资源类型(例如纹理、MonoBehaviour 和粒子系统)上。
上面的截屏是 Unity 加载场景时的跟踪记录。加载场景时,Unity 会读取并反序列化场景中的所有资源,如对 SerializedFile::ReadObject 下各种 Transfer 方法的调用所示。
如果在运行时看到性能不稳,并且性能跟踪记录显示 SerializedFile::ReadObject 使用了大量时间,则意味着资源加载降低了帧率。请注意:当 SceneManager、Resources 或 AssetBundle API 请求同步进行资源加载时,SerializedFile::ReadObject 通常会出现在主线程上。
为解决此类性能不稳的问题,可以使资源加载异步进行(将繁重的 ReadObject 调用移至工作线程),或预加载某些繁重资源。
在 Unity 克隆对象(在跟踪记录中以 CloneObject 方法表示)时也会出现 Transfer 调用。如果在 CloneObject 调用下出现了对 Transfer 的调用,则表示 Unity 不是从存储中加载资源。相反,Unity 将旧对象的数据传输到新对象。为此,Unity 会序列化旧对象,并将结果数据反序列化为新对象。