这是摘自Unity官方文档有关优化的部分,原文链接:https://docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity.html
总共分为如下系列:
- 采样分析
- 内存部分
- 协程
- Asset审查
- 理解托管堆 【推荐阅读】
5.1 上篇:原理,临时分配内存,集合和数组
5.2 下篇:闭包,装箱,数组 - 字符串和文本
- 资源目录
- 通用的优化方案
- 一些特殊的优化方案
采样分析
考虑到优化性能的时候,首先必须记住所有的优化是从采样分析开始。对应用进行采样发现问题所在是第一步,从而对采样的结果针对项目的代码和资源进行分析。
工具:
- iOS:Instrument和XCode Frame Debugger
- Android:Snapdragon Profiler
- Intel CPU/GPU的平台:VTune和Intel GPA
- PS4:Razor套装
- Xbox:Pix工具
这些工具的使用条件是IL2CPP生成的项目,因为原生C++代码能够提供更加清晰的调用关系和方法调用的时间消耗,在Mono平台下做不到这些。
启动日志的分析
当观察启动时的跟踪日志,需要特别关注两个关键方法,这两个方法反映了项目中的配置、资源和代码如何影响启动时间。
游戏启动设置不同的平台上略有差异,在大部分平台上用户可以看到一个静态的启动界面。
上图是来自于Instrument工具截取在iOS设备上运行实例工程的跟踪日志。在startUnity方法中,注意UnityInitApplicationGraphics
和UnityLoadApplication
方法。
UnityInitApplicationGraphics
这个方法会执行一系列的内部操作,设置图形显示设备和初始化Unity的内部系统。除此之外,它还会初始化资源系统,导入资源系统包含的所有文件的索引。
每个“Resources”目录中包含的资源文件都属于资源系统的数据。
【Resources”目录指的是“Assets”目录下的“Resources”以及这些“Resources”目录下的所有子文件夹。】
所以,如果Resources目录下的文件非常多,就会造成加载的时间比较长。
UnityLoadApplication
包含加载和初始化项目中的第一个场景。这个步骤包括反序列化和实例化场景中所有的数据,例如编译Shader,上传纹理和实例化GameObject。除此之外,第一个场景中的所有MonoBehaviour对象中的Awake方法都在这个方法内执行。
这些处理流程意味着,如果你的Awake回调方法中有耗时较长的代码,就会严重拖慢项目的启动速度。要不你就删除这段代码,要不你就把这段代码放到别的生命周期中执行。
运行日志的分析
在启动部分完成之后,接着就是PlayerLoop
方法了。这是Unity的主循环,每帧都会执行一次。
上图中的截图来自于Unity5.4的某个示例工程,展示了PlayerLoop中值得关注的几个方法。注意PlayerLoop中的方法在不同版本的Unity中可能不同。
PlayerRender
运行Unity的渲染系统,包括剔除不需要显示的对象,计算动态合批,给GPU提交绘制指令。图片特效或者基于渲染的脚本中的回调(如OnWillRenderObject
)都会在这个函数中执行。一般情形下,当项目是可以UI交互的时候,这个方法会是消耗CPU资源的头号顾客。
BaseBehaviourManager
调用三种模板化的CommonUpdate方法。这些方法调用当前场景中活跃的GameObject上 MonoBehaviour中特定的回调方法。
- CommonUpdate<UpdateManager> 调用Update回调
- CommonUpdate<LateUpdateManager> 调用LateUpdate回调。
- CommonUpdate<FixedUpdateManager> 调用FixedUpdate回调。
通常来讲,BaseBehaviourManager::CommonUpdate<UpdateManager>
是最值得关注的方法,因为它是Unity工程中大部分代码的入口方法。
其他值得关注的代码:
UI::CanvasManager
如果工程用到了Unity UI,这个方法会调用几个不同的回调函数。包括Unity UI的批处理过程和布局更新;这两个操作会导致CanvasManager出现在剖析日志列表中。
DelayedCallManager::Update
运行协程。更多的细节可以参见系列文章中“协程”这一章。
PhysicsManager::FixedUpdate
运行PhysX物理系统,这个函数主要运行PhysX的内部代码,受到当前场景中物理组件个数的影响,如Rigidbody和Collider组件。还有,基于物理的回调也会出现在这个函数,如OnTriggerStay和OnCollisionStay方法。
如果工程中用到2D物理,在Physics2DManager::FixedUpdate方法下面会出现类似的方法。
脚本方法的分析
当IL2CPP转换后的代码被调用的时候,可以找到ScriptingInvocation
对象。这是Unity的原生代码过渡到脚本运行环境下,执行脚本代码的关键部分。【从技术细节上来讲,当使用IL2CPP,C#的代码也会被转化成原生代码。然而编译后的代码通过IL2CPP运行框架执行方法的过程,和手写的C++代码执行过程不同】
上面的截图来自Unity5.4上的一个示例工程。RuntimeInvoker_Void
行下面包含的所有函数都是交叉编译后的C#代码,这些方法每帧执行一次。
被转化后的方法命名规则是,每个方法是“类名_原始方法名”。在这些跟踪日志里面,有EventSystem.Update
,PlayerShooting.Update
和一些其他的Update方法。这些是大多数MonoBehaviour类中的Update回调方法。
展开这些方法,很容易发现到底是哪些方法消耗了大部分的CPU时,也包括其他的脚本中的方法,如Unity的API和C# 库中的代码。
上面的跟踪日志展示了StandaloneInputModule.Process
需要给整个UI系统投射射线,检测什么触碰事件正在悬停或者激活某个UI元素。最大的开销在于遍历所有的UI元素和检测鼠标的位置是否在这些UI元素的边界范围之内。
资源加载
资源加载也会出现在CPU的日志中。资源加载的主要方法是SerializedFile::ReadObject
。这个方法通过执行Transfer方法把文件的二进制数据流和Unity的序列化系统连接起来。Transfer方法可以在所有的资源类型中找到,如纹理,MonoBehaviour和粒子系统。
上面的截图展示了场景正在加载的日志。这个过程需要Unity去读取数据和反序列化场景中所有的资源,通过调用SerializedFile::ReadObject方法下的不同资源类型的Transfer方法。
通常来讲,如果在运行时间存在性能问题,而且SerializedFile::ReadObject
方法占据了很大的性能开销的话,很有可能就是资源加载导致的帧率问题。注意,在大多数情况下,如果是 SceneManager,Resources和AssetBundle API这些方法发起同步资源加载的话,SerializedFile::ReadObject方法只能在主线程中看到。
这种性能问题可以通过常用的方法解决:使用异步加载,将ReadObject方法交给worker线程去做,或者提前加载比较大的资源。
注意,Transfer调用在克隆Object的时候也可能会出现,由CloneObject方法调用。如果CloneObject方法里面调用了Transfer方法,那么表明资源不是从存储器中加载,而是将老Object的数据传递给了新Object。Unity序列化老的Object并且根据这些数据反序列化出了新的Object。
下篇:内存分析
如果觉得文章对您有用,请点个赞呗!☺☺☺