Unity MemoryProfiler 的工作机制及可能的改进
作者 | 顾露
Unity 的开源内存分析工具 MemoryProfiler 非常有用,可以提供所有由 Unity 分配的 C++ 对象的内存信息,在该工具内被称为 NativeUnityEngineObject (Native-only Mode)。当 C# 脚本经由 il2cpp 编译为 C++ 时,此工具可以提供额外的所有 C# 对象的信息,在该工具内被称为 ManagedObject (Full Mode)。
本篇文章简单地描述了Unity 的开源内存分析工具 MemoryProfiler的工作机制,并探讨了一下基于该工具的一些可能的改进,主要表现在以下这三个方面:
工作机制
这个工具中所能提供的所有的内存数据均来源于一个 Unity API:
UnityEditor.MemoryProfiler.MemorySnapshot.RequestNewSnapshot();
通过调用这个函数,我们可以向一个运行着的 Unity 程序请求一个新的内存快照。如果是运行于编辑器内的程序,该请求同步地返回上面说到的 Native-only Mode 数据;如果是运行于 iOS 上的基于 il2cpp 的应用,该请求异步地返回上面说到的 Full Mode 数据。
刚收到的快照存在于下面这个紧凑数据对象里:
public class PackedMemorySnapshot
{
public Connection[] connections { get; }
public PackedGCHandle[] gcHandles { get; }
public MemorySection[] managedHeapSections { get; }
public PackedNativeUnityEngineObject[] nativeObjects { get; }
public PackedNativeType[] nativeTypes { get; }
public TypeDescription[] typeDescriptions { get; }
public VirtualMachineInformation virtualMachineInformation { get; }
}
收到这个紧凑数据对象后,MemoryProfiler 做了一些展开的工作,得到下面这个展开后的对象,内含完整的信息和交叉的引用:
public class CrawledMemorySnapshot
{
public NativeUnityEngineObject[] nativeObjects;
public GCHandle[] gcHandles;
public ManagedObject[] managedObjects;
public StaticFields[] staticFields;
//contains concatenation of nativeObjects, gchandles, managedobjects and staticfields
public ThingInMemory[] allObjects;
public MemorySection[] managedHeap;
public TypeDescription[] typeDescriptions;
public PackedNativeType[] nativeTypes;
public VirtualMachineInformation virtualMachineInformation;
}
这个过程中,最重要的是:所有的内存对象被展开到 ThingInMemory[] allObjects 这个多态数组里。有了所有的对象及它们间的引用关系,我们就可以做进一步的分类,调查和分析了。
实例类型
在上面的展开后的数据对象里,前四项值得分别说明一下:
1. NativeUnityEngineObject[] nativeObjects 这是前面提到过的所有的 C++ 对象。我们无法看到这些对象的实际内容,但是可以看到下面这些信息:
1)这是一个典型的 C++ 对象:
2)在上图中,最有价值的信息有:
instanceID - 该对象的实例 ID,Unity 保证在一次运行期间新创建的对象不会与已销毁的对象复用 ID
References - 该对象引用的所有对象列表
Referenced by - 引用该对象的所有对象列表
2. ManagedObject[] managedObjects 这是所有的 C# 对象:
1)由于该快照包含了对应的 managed heap 的信息,我们可以获得任意 C# 对象的数据细节。
2)这是一个典型的 C# 对象:
3)在上图中可以看到这个对象的每一个字段的详细内容。如果某个字段是对另一个对象的引用,可以直接点击跳转过去。
3. GCHandle[] gcHandles 用于 C#/C++ 对象的交叉生命期管理
1)“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
2)简单地说,如果一个 C# 对象被一个 C++ 对象持有的话,一个 GCHandle 就会被创 建出来通知 GC 这种外部引用的情形存在
3)通过任意一个 GCHandle 的 References/Referenced by 我们可以找到位于这个外部 引用两端的 C#/C++ 对象
4. StaticFields[] staticFields 则是所有的静态变量 (C#)
1)这里你可以找到程序内现存的所有静态变量,是非常有用的功能。
2)这是一个典型的静态变量:
3)分析这些静态变量的数据可以发现,很多时候内存增长都是这种难以意识到的隐性的 静态容器的尺寸增长。
除了这些实例对象,还有 C# 堆的数据和其他一些类型信息,这里就不多说了。
可能的改进
以下是一些针对 MemoryProfiler 的一些可以改进之处。
层次化和结构化的数据展示改进:
由于 MemoryProfiler 通过 Treemap 的形式展现数据,在操作时很容易因为数据量太大而难以定位到单个的对象。一个很容易得出的改进是使用更结构化的方式来归类和浏览不同类型的对象。实践中可以使用自制控件 TableView 来构造一个双层表格,分别用于类型和对象的展示。具体的使用例子可以参考文章 Unity 游戏的 string interning 优化
这种表格的一个优势是可以自定义字段并显示对应的汇总统计信息
除了展示方式的改进之外,针对类型或实例的全文搜索,快照间的对比,分配时的细节诊断增强,都是非常有价值的潜在改进。在 ResourceTracker 中,可以看到我们基于开源的 Unity MemoryProfiler 做出的部分改进工作。