Unity3D_记一次CPU卡顿的排查实践
这是一次CPU卡顿的排查实践,开发环境是 Unity5.3.4p5 + uGUI
======
为了避免打log引起的额外消耗,所以以下分析基于log关闭后的Profiler数据
Debug.logger.logEnabled = false;
问题1:ConfigUtil.Get().Single(Func)
List ConfigUtil.Get() 内含 List.AddRange()申请临时内存,相对GC Alloc和耗时较多;
T List.Single(Func)多次遍历查找同一个List,耗时较多。影响范围较广。
修行状态的红点计算NinjaPracticeController.UpdatePracticeState()较为复杂,查询修行配置有多层循环嵌套,而且内层嵌套有多次重复计算,且频繁出现于回包中。比如心跳回包会计算修行红点状态,其他一些回包触发OnSyncSthChangeCommand的也会引起修行红点计算。
修改前(60次+60次):
将查询全量配置提前到for循环之前算好。并且classEvolveCfg和classUpgradeCfg这两项配置信息并不总是需要,而是根据底下if-else的条件局部需要,所以挪到实际需要的地方才计算。
修改后(40次+3次):
Single方法的调用还可以进一步优化(配置List转换为Map,然后for循环内按Key查找)。
更新忍者状态的NinjaController.UpdateNinjaState也有类似的现象:
初次加载忍者技能、忍者课程修炼也是如此:
其他模块的List ConfigUtil.Get()调用,建议按需修改。
问题2:MaouOutline的数组重置操作
为达到更佳的描边效果,重写了ModifyMesh/ModifyVertices的实现,而MaouOutline脚本又频繁出现在各个界面。影响范围较广。
修改前:
发现主要消耗是在重设数组长度。将vertexs数组初始化时指定初始长度,性能可小幅度提升。
经实测,List<UIVertex> vertexs = new List<UIVertex>(256);
指定初始长度为256,比128或512或其他都好一丢丢(减少了SetCapacity(Array.Resize)的消耗,但同时增加了List构造函数的消耗)。
第一次修改后:
进一步修改,调整为静态全局数组,就不会每次都重置数组长度了:
第二次修改后:
MaouOutline的ModifyMesh/ModifyVertices已经没问题了,但是Unity自带的Outline的ModifyMesh仍然有重置数组长度的现象。
经排查,发现TaskItem有两个按钮Text使用了Outline而非MaouOutline。于是将所涉及prefab的Outline脚本全部替换成MaouOutline脚本即可。
Outline替换为MaouOutline后:
到此GC Alloc基本解决了(0.9M=>22K)。
但进一步又发现,剩余的耗时问题集中到了ModifyVertices(每次ModifyVertices调用都会触发8次ApplyShadow调用),此问题待解决。
问题3:GameObject.SetActive(ture/false)引起的毛刺,即使并没有实际改变true/false的状态,也会有额外的消耗。
建议封装一个静态方法:先判断GameObject.activeSelf当前状态,然后再作GameObject.SetActive(ture/false)修改状态。
对于在业务View初始化方法中只作一次GameObject状态初始化的,可以不做修改。
对于操作时会变更状态的(比如赌小游戏滚动前后的按钮状态切换、奖励展示切换),建议增加判断。
已封装为UIUtil.SetActive(GameObjectgo, bool v),可直接使用。
修改前:
修改后:
赌小游戏已修改,其他模块可按需修改。
比如排行榜的一个条目:
问题4:B.GetView=>B.OnShow=>A.OnHide
界面切换会有新界面(B)的GetView和OnShow,以及旧界面(A)的OnHide,而这里的多次SetActive(true/false)会引发多个脚本OnEnable/OnDisable。
现象:
一次SetActive(true/false)会引发上百次的OnEnable/OnDisable:
两个建议:
(1)对于Instantiate创建的VIew,无需默认设为SetActive(false),因为马上就OnShow=> SetActive(true)了。
(2)对于需要频繁显示和隐藏的常驻VIew(比如TitleBar之类的),OnShow和OnHide中除了使用SetActive(true/false)外,也可考虑position移入移出屏幕,或者设置Scale为1或0等等。
但是需要注意,应该增加忽略隐藏状态下业务逻辑的代码。
问题5:可疑的Graphic.Rebuild=>Text.OnPopulateMesh=>Font.CacheFontForText
初始化或更换Text.text的文本,会引发Text.OnPopulateMesh,有时还会引起Font.CacheFontForText=>TextUpdateGeometry(耗时)和Text.cachedTextGenerator(GC Alloc)。
Image也有类似现象。优化方法待思考。
问题6:可疑的Tx.OnUpdate
这里的大毛刺使得战斗开场那一帧比较卡,看到界面显示一个空的战斗场地,等一两秒后才有人物跳出来。
由于Profiler信息量太大,无法使用DeepProfiler查看详情。
通过添加Profiler代码定位,最后定位到Tx.OnUpdate()里的tx_object_unity_update();,然后线索就断了。
真实原因待查。
添加了Profiler辅助方法:
其中加载了许多图标,包括忍者头像、战斗上阵的人物等。如果原因是资源尺寸过大,可考虑能否裁剪成多张小尺寸的。
问题7:可疑的Text. preferredWidth
在普通副本和精英副本切换时,发现有一个可疑的Text. preferredWidth,它会触发Text.cachedTextGeneratorForLayout,从Profiler看似乎是申请了一个长度较大的List。
经走读代码发现该行代码实际是不需要的,于是直接注释掉。
另全量搜索发现其他脚本也多处出现Text. preferredWidth,还需逐一排查。
问题8:可疑的Component.GetComponentFastPath
进入某些列表界面(比如任务界面),初始化MaouGrid组件时会调用SetParent。
而SetParent会触发一些Unity自有脚本(比如MaskableGraphic)的OnEnable/OnDisable的成倍调用,耗时和GC Alloc累积起来也比较可观。
切换SetActive(true/false)也有类似现象。
展开MaskableGraphic.OnEnable/OnDisable,最后落到Component.GetComponentFastPath,此方法耗时和GCAlloc原因待查。
问题9: debug信息输出导致的额外损耗
VersionView每一帧Update()都在刷新帧率,导致过渡频繁的字符串Format和Concat
建议修改为StringBuilder,而固定字符串(版本信息字符串、网络状态字符串)可改为分别用单独的UI.Text显示,每次只刷新变化的数据(比如帧率)。
(优先级较低,暂时可忽略,且正式版本会屏蔽)
另一个可疑的debug信息(PrettifyProto.ToStr和LogUtil.WritePre),常见于一些回包中,也可以考虑处理一下:
问题10:界面加载和界面刷新时的耗时
界面加载时有较多的资源加载。比如通灵兽界面:
又比如任务界面:
领取任务奖励后,任务条目更新又有一些耗时的刷新操作:
经分析,这里每次都会先Hide(全部),然后Show(其中某几个),可考虑优化。
问题11:切换普通副本和精英副本,会有瞬间的白屏
问题12:图标加载时有额外的字符串操作消耗。
比如副本介绍界面的奖励图标加载(除了字符串操作,还有一个可疑的IdUtil.TryGetId):
或者任务条目的奖励图标查询(除了字符串操作,还有一个可疑的Behaviour.set_enabled):