Unity客戶端優化中最常使用的輔助優化工具是Profiler。使用Profiler,可以方便我們定位游戲程序的性能瓶頸,如定位游戲中單幀耗時過高的模塊、定位游戲中產生GC較多的模塊等等。
盡管如此,在實際優化分析過程中,即使直接使用Profiler定位到游戲瓶頸的大致模塊,也往往不能分析出更精確的瓶頸代碼。例如,在實際開發過程中,我們發現Game.Update()這一模塊特別耗時,其實也是無補於事。因為Game.Update()這一模塊下層可能涵蓋了網絡交互、戰斗、聊天等多個自定義模塊,如果這些模塊不是使用Unity自帶的MonoBehavior.Update實現幀循環,我們就不能往更深層次模塊進行分析了。
在默認的情況下,Profiler僅為Unity自帶的函數添加了性能采樣點(如MonoBehavior的函數、Resources庫函數等),但是Profiler無法直接對我們用戶的代碼進行采樣分析。
使用Profiler評估客戶端性能時,推薦使用Profiler提供的性能采樣接口,來更精確地分析定位客戶端存在的性能問題。
優點:使用Profiler提供的性能采樣接口,最大的優點是可以更深層次地分析用戶代碼的性能熱點,避免定位到大致模塊后,無法繼續往下分析,只能通過其他方式(如代碼審查)繼續優化的尷尬。
舉個例子,如圖:
使用BeginSample、EndSample配對,可以有效定位用戶自己編寫的代碼
由上圖可見,在沒有使用Profiler.BeginSample()定位的情況下,Profiler只能分析到Game.Update()模塊比較耗時,往下只能看到耗時大頭在Loading.ReadObject這一點上,但是由於邏輯無關,我們無法分析導致加載耗時的模塊具體發生在Game.Update的哪一子模塊。
而使用Profiler.BeginSample()定位后,我們可以有層次性地發現,單幀耗時過大的瓶頸耗點位於 "Game.Update() -> GameNewWork.Update() -> HandleIO"。
除此之外,我封裝了一套自己的接口,代碼在本文最后面。之所以封裝一層,原因如下:
1、提供Profiler性能采樣開關,可隨時關閉
2、提供字符串格式化功能,可在Profiler中顯示自定義的文本,方便定位問題(使用時需要謹慎,后文敘述)
關於格式化字符串
有時候光知道熱點代碼位置是不夠的,還需要知道代碼中變量數據。
例如下圖OnRecv函數,代碼只是根據cmd參數,獲取已注冊的句柄,然后調用。如果沒有格式化功能,我們只能知道代碼最終執行到這一環節,而不能准確定位該句柄代表的是哪個函數。使用格式化功能,把對應句柄的函數名打印出來,我們就可以知道熱點對應的是哪一個函數。
慎用格式化字符串
以上代碼會較高頻率地觸發垃圾回收(GC.Collect()),是不是邏輯代碼有問題?
經測試發現,CurrentState.Update片段幾乎均為每幀0B的開銷,而其他代碼也不存在內存分配現象,GC.Collect()是ProfilerSample.BeginSample(format)導致的。
需要注意格式化字符串本身會帶來內存分配開銷,使用格式化字符串采樣接口時需考慮自身對代碼帶來的影響。
使用經驗:
1、在可能的熱點函數上插入性能采樣代碼,建議編譯手機版本來分析結果。當然,在熟悉代碼的前提下,可以方便使用PC測試分析GCAlloc等問題。原因如下:
1)PC性能相對太好,一些手機上的瓶頸函數在PC上幾乎不耗時,導致無法准確分析;
2)一些代碼,特別是插件代碼,PC和手機的執行流程不同,PC分析的結果不能准確表明手機也是同樣結果。
2、在插入性能采樣代碼時,特別留意函數中是否存在多個return的現象。這些return如果沒有處理好,就有可能導致性能采樣的Begin和End不匹配,導致Profiler顯示的結果有誤。
3、對於協程函數,BeginSample、EndSample之間注意不能存在yeild return null,否則可能導致Unity客戶端卡死、手機卡死等現象。個人分析:Begin和End配對分析的是單幀結果,出現yeild return null代表該區間將會分兩幀甚至多幀完成。
封裝好的性能采樣接口代碼:
using UnityEngine;
using System;
public class ProfilerSample {
public static bool EnableProfilerSample = true;
public static bool EnableFormatStringOutput = true;// 是否允許BeginSample的代碼段名字使用格式化字符串(格式化字符串本身會帶來內存開銷)
public static void BeginSample(string name) {
#if ENABLE_PROFILER
if(EnableProfilerSample){
Profiler.BeginSample(name);
}
#endif
}
public static void BeginSample(string formatName, params object[] args) {
#if ENABLE_PROFILER
if(EnableProfilerSample) {
// 必要時很有用,但string.Format本身會產生GC Alloc,需要慎用
if (EnableFormatStringOutput)
Profiler.BeginSample(string.Format(formatName, args));
else
Profiler.BeginSample(formatName);
}
#endif
}
public static void EndSample() {
#if ENABLE_PROFILER
if(EnableProfilerSample) {
Profiler.EndSample();
}
#endif
}
}