Unity MemoryProfiler 的工作機制及可能的改進

Unity 的開源內存分析工具 MemoryProfiler 非常有用,可以提供所有由 Unity 分配的 C++ 對象的內存信息,在該工具內被稱為 NativeUnityEngineObject (Native-only Mode)。當 C# 腳本經由 il2cpp 編譯為 C++ 時,此工具可以提供額外的所有 C# 對象的信息,在該工具內被稱為 ManagedObject (Full Mode)。

本文簡單地描述了該工具的工作機制,並探討了一下基於該工具的一些可能的改進。

工作機制

這個工具中所能提供的所有的內存數據均來源於一個 Unity API:

UnityEditor.MemoryProfiler.MemorySnapshot.RequestNewSnapshot();n

通過調用這個函數,我們可以向一個運行著的 Unity 程序請求一個新的內存快照。如果是運行於編輯器內的程序,該請求同步地返回上面說到的 Native-only Mode 數據;如果是運行於 iOS 上的基於 il2cpp 的應用,該請求非同步地返回上面說到的 Full Mode 數據。

剛收到的快照存在於下面這個緊湊數據對象里:

public class PackedMemorySnapshotn{n public Connection[] connections { get; }n public PackedGCHandle[] gcHandles { get; }n public MemorySection[] managedHeapSections { get; }n public PackedNativeUnityEngineObject[] nativeObjects { get; }n public PackedNativeType[] nativeTypes { get; }n public TypeDescription[] typeDescriptions { get; }n public VirtualMachineInformation virtualMachineInformation { get; }n}n

收到這個緊湊數據對象後,MemoryProfiler 做了一些展開的工作,得到下面這個展開後的對象,內含完整的信息和交叉的引用:

public class CrawledMemorySnapshotn{n public NativeUnityEngineObject[] nativeObjects;n public GCHandle[] gcHandles;n public ManagedObject[] managedObjects;n public StaticFields[] staticFields;n //contains concatenation of nativeObjects, gchandles, managedobjects and staticfieldsn public ThingInMemory[] allObjects;n public MemorySection[] managedHeap;n public TypeDescription[] typeDescriptions;n public PackedNativeType[] nativeTypes;n public VirtualMachineInformation virtualMachineInformation;n}n

這個過程中,最重要的是:所有的內存對象被展開到 ThingInMemory[] allObjects 這個多態數組裡。有了所有的對象及它們間的引用關係,我們就可以做進一步的分類,調查和分析了。

實例類型

在上面的展開後的數據對象里,前四項值得分別說明一下:

  • NativeUnityEngineObject[] nativeObjects 這是前面提到過的所有的 C++ 對象。我們無法看到這些對象的實際內容,但是可以看到下面這些信息:

    • 這是一個典型的 C++ 對象:

    • 在上圖中,最有價值的信息有
      • instanceID - 該對象的實例 ID,Unity 保證在一次運行期間新創建的對象不會與已銷毀的對象復用 ID
      • References - 該對象引用的所有對象列表
      • Referenced by - 引用該對象的所有對象列表
  • ManagedObject[] managedObjects 這是所有的 C# 對象。

    • 由於該快照包含了對應的 managed heap 的信息,我們可以獲得任意 C# 對象的數據細節。
    • 這是一個典型的 C# 對象:

    • 在上圖中可以看到這個對象的每一個欄位的詳細內容。如果某個欄位是對另一個對象的引用,可以直接點擊跳轉過去。
  • GCHandle[] gcHandles 用於 C#/C++ 對象的交叉生命期管理

    • 「If the native code will take ownership of that object, we need to tell the garbage collector that the native code is now a root in its object graph. This works by using a special managed object called a GCHandle.」 詳見 IL2CPP Internals – Garbage collector integration – Unity Blog
    • 簡單地說,如果一個 C# 對象被一個 C++ 對象持有的話,一個 GCHandle 就會被創建出來通知 GC 這種外部引用的情形存在
    • 通過任意一個 GCHandle 的 References/Referenced by 我們可以找到位於這個外部引用兩端的 C#/C++ 對象
  • StaticFields[] staticFields 則是所有的靜態變數 (C#)

    • 這裡你可以找到程序內現存的所有靜態變數,是非常有用的功能。
    • 這是一個典型的靜態變數:

    • 分析這些靜態變數的數據可以發現,很多時候內存增長都是這種難以意識到的隱性的靜態容器的尺寸增長。

除了這些實例對象,還有 C# 堆的數據和其他一些類型信息,這裡就不多說了。

可能的改進

以下是一些針對 MemoryProfiler 的一些可以改進之處。

  • 層次化和結構化的數據展示改進
    • 由於 MemoryProfiler 通過 Treemap 的形式展現數據,在操作時很容易因為數據量太大而難以定位到單個的對象。一個很容易得出的改進是使用更結構化的方式來歸類和瀏覽不同類型的對象。實踐中可以使用自製控制項 TableView 來構造一個雙層表格,分別用於類型和對象的展示。具體的使用例子可以參考文章 Unity 遊戲的 string interning 優化
    • 這種表格的一個優勢是可以自定義欄位並顯示對應的匯總統計信息

除了展示方式的改進之外,針對類型或實例的全文搜索,快照間的對比,分配時的細節診斷增強,都是非常有價值的潛在改進。在 PerfAssist/ResourceTracker 中,可以看到我們基於開源的 Unity MemoryProfiler 做出的部分改進工作。

(完)

Gu Lu

[2017-01-25]

[注]

  • 本文首先發佈於公眾號西山居技術。
  • Permanent Link: Unity MemoryProfiler 的工作機制及可能的改進
  • 本文遵循 Creative Commons BY-NC-ND 4.0 許可協議。

推薦閱讀:

Ninja Theory與《地獄之刃》:「獨立3A」遊戲開發的探索之路
《記錄》第18期:我是楊土包子啊
《InsideUE4》基礎概念
如果要自己動手編寫3D引擎,不能錯過的書籍有哪些?
《劍與家園》七日談

TAG:Unity游戏引擎 | 内存优化 | 游戏开发 |