作者: Ian
翻譯: Kelvin Lo / 海龜
說到優化,不得不說所有優化的源頭都是從發現問題開始,第一步是分析,根據項目技術和資源結構的分析報告結果來劃出項目問題的可能範圍。
注意:本文裡使用的一些追蹤程序代碼是基於Unity 5.3版本,在未來這些功能有可能會變動。
注意:這個章節討論的原生(Native)方法(Method)名稱是由 Unity 5.3 的執行檔擷取出來的,這些方法名稱可能在未來的版本有所改變。
關於分析,有許多能幫助 Unity 開發者分析項目的工具,而 Unity 本身有一套內建工具像是 CPU Profiler、Memory Profiler、或是 5.3 才有的 Memory Analyzer。
但是最好的分析報告通常來自於該平臺的特有工具,像是:
iOS:Instruments 和 Xcode Frame Debugger
Android:SnapdragonProfiler
採用 Intel CPU/GPU的平臺:VTune 和 Intel GPA
PS4:Razorsuite
Xbox:Pixtool
這些工具大部分都能夠分析IL2CPP所包出來的C++項目,這些原生程序能執行原本在Mono下無法執行的高精度計時(high-resolution method timings)和更透明的堆棧呼叫(callstacks)。
要充分利用這些工具通常需要在這些平臺上啟用 IL2CPP 然後使用轉換成 C++ 版本的項目。在原生程序代碼狀態下可以透過原生工具得到完整的呼叫堆棧還有高精度計時,這些在透過 Mono 執行的時候都無法取得。
當查看啟動時間的軌跡時,有兩個關鍵的方法來檢視,分別是從項目設定、資源和程序可能影響啟動時間的可能範圍。
請注意:啟動時間在不同的平臺上會有差距,大多都是用啟動到顯示 Unity Logo(splash screen)的時間來判定。
注意 Unity 的啟動時間在不同平臺上可能表現會不同,大部分的平臺的行為是會讓用戶會在靜態的 Unity Logo(Splash screen)等待得時間變長。
上圖為 iOS 上用 Instruments 抓的啟動時間追蹤,可以看到 iOS 平臺特有的 startUnity 方法呼叫了 UnityInitApplicationGraphics 和 UnityLoadApplication 方法。
UnityInitApplicationGraphics 執行很多內部工作,像是圖形裝置的設定工作和初始化很多Unity的內部系統。不但如此,他還負責初始化 Unity 的資源系統(Resources system)。所以它需要先加載資源系統所含的所有檔案索引。
所有目錄名為 Resources 裡面的資源文件(在 Assets 目錄下的 Resources 目錄,還有 Resources 目錄下的目錄)都會被算在資源系統裡。因此初始化資源系統所需要的時間將會隨著 Resources 目錄裡的檔案數量增加而變久。
UnityLoadApplication 的工作包含加載和初始化項目最開始的場景。這包含反串行化(deserializing)和實例化(Instantiating)展示第一個場景需要的數據,例如編譯著色器(compiling Shaders)、上傳貼圖和實例化遊戲對象(GameObject)等等所有為了顯示初始場景所需要的資料。此外,第一個場景中所有的 MonoBehaviours 都會在這個時間點執行 Awake 回呼(Callback)。
這樣的呼叫結構代表如果你在項目的第一個場景裡的某個 Awake 回呼執行了非常耗時的程序,會拖慢整個項目的啟動時間。要解決這樣問題當然是要修改拖慢的程序代碼或是移到別的地方來執行。
解析執行軌跡 啟動初始化之後主要是 PlayerLoop 的追蹤。這是 Unity 主要周期循環,裡面的程序每幀會執行一次(注一)
上圖是一個 Unity 5.4 項目的分析報告,顯示了幾個 PlayerLoop 會呼叫的最有趣的方法。請注意,PlayerLoop 裡方法的名稱可能會因不同版本的 Unity 而有所不同。
PlayerRender 是執行 Unity 渲染系統的方法。也負責剔除不顯示的對象、計算動態批次計算(Dynamic batches)和對 GPU 送出繪圖指令。所有的影像後制(Image Effects)或是基於渲染的程序回呼(例如 OnWillRenderObject)也都在此處理。一般來說,當項目跟玩家互動時 CPU 大部分時間都會花在這裡。
BaseBehaviourManager 會呼叫三種不同版本的 CommonUpdate,它們各自會呼叫場景裡被啟動(Active)的 GameObject 上的 MonoBehaviours 的特定回呼。
CommonUpdate<UpdateManager> 會呼叫 Update 回呼。
CommonUpdate<LateUpdateManager>會呼叫 LateUpdate 回呼。
CommonUpdate<FixedUpdateManager> 會呼叫 FixedUpdate(如果物理系統被觸發)
一般來說 BaseBehaviourManager::CommonUpdate<UpdateManager> 是最有意思的方法,因為它在 Unity 項目裡大多數腳本的進入點。
還有幾個有趣的方法:
如果項目有用到 Unity UI 系統,UI::CanvasManager 會呼叫幾個不同的回呼。這行為包含了Unity UI 的批次運算和排版更新,這兩個操作最常造成 CanvasManager 出現在分析報告裡。
DelayedCallManager::Update 執行共例程(Coroutines)。在底下的 Coroutines 章節會有更詳細的說明。
PhysicsManager::FixedUpdate 執行 PhysX 物理系統。涉及到執行 PhysX 的內部程序,並受到場景內有物理行為對象的數量影響(像是帶有 Rigidbody 和各種 Collider 的物件)。然而,跟物理有關的回呼也會出現在此,特別是 OnTriggerStay 和 OnCollisionStay。
假如項目用的是2D物理(Box2D),類似的結構會出現在 Physics2DManager::FixedUpdate 之下。
當使用 IL2CPP 跨平臺編譯時,可以找看看 ScriptingInvocation 這行包含的內容,這是從 Unity 內部原生程序進入到用戶腳本的分界點。(注二)
上圖是一個 Unity 5.4 項目的另一個分析報告,附掛在 RuntimeInvoker_Void 底下的所有方法都是交叉編譯過的 C# 腳本的一部分,每幀執行一次。
這些追蹤還蠻容易理解的,每一個命名規則都是「原始類別_原始方法」,例如上圖範例範例能找到 EventSystem.Update、PlayerShooting.Update 以及其他幾個 Update 方法。這些都是在MonoBehaviours 裡能找到的標準 Unity Update 回呼。
透過展開這些方法,可以更精確的定位誰在消耗 CPU 時間。這包含了項目的腳本方法、Unity API 和 C# 函式庫。
上面顯示出 StandaloneInputModule.Process 方法每幀會對整個 UI 進行一次 Raycasting,檢查是否有任何 UI 事件被點擊或滑過的動作觸發。主要消耗是在逐一檢查所有 UI 元素,並測試滑鼠的位置是否在其範圍內。(注三)
資源加載也能從 CPU 追蹤裡找到,加載資源的主要方法是SerializedFile::ReadObject,它透過名為「Transfer」的方法將檔案透過 2 進位(binary)的數據串流連接到 Unity 的串行化系統(Serialization system)。Transfer 方法可以在所有資源的加載過程中看到,例如材質、MonoBehaviours 和粒子系統。
上圖顯示出一個場景正被加載,這會讀取並反串行化(Deserialize)場景中所有資源,可以看到SerializedFile::ReadObject 之下有各種不同 Transfer 呼叫。
一般來說,如果在執行時遇到了效能問題,並追蹤看到 SerializedFile::ReadObject 用掉大量的時間,代表FPS會下降是因為資源正在加載。要注意的是絕大部分的情況下,只有當透過 SceneManager、Resources 或 AssetBundle API 請求同步(Synchronous)載入時,才會在主線程上看到 SerializedFile::ReadObject。
這種效能問題通常能簡單解決:可以採用異步方式(Asynchronous)加載資源(將比較吃重的 ReadObject 呼叫放到工作線程(Worker thread)上),或預先加載那些較大的資源。
請注意,當複製(Cloning)對象時也會產生 Transfer 呼叫(在追蹤表的CloneObject方法裡)。假如一個Transfer 呼叫出現在 CloneObject 下就代表它不是從硬碟加載而是從一個舊的對象數據轉移到一個新的對象。Unity 做法是對舊的對象進行串行化之後將產生的數據反串行化成為新的對象。
注一:這隻適用於在」Assets」底下的」Resources」目錄以及底下所有名為」Resources」的子目錄。
注二:技術上來說,執行IL2CPP之後,C#/JS腳本也會轉為原生程序(Native code)。然而,這種交叉編譯程序主要是透過IL2CPP的框架來執行,不會像手動寫程序那麼嚴謹。
注三:在Unity 5.4之前,StandaloneInputModule在沒有滑鼠的裝置上查詢滑鼠輸入存在著一些缺陷,還好後面的版本已經修復,大大降低了StandaloneInputModule的CPU消耗