介绍

Unity 使用 C# 时会出现内存泄露问题,如何排查 C# 导致的内存泄露是一件很麻烦的事情。

C# 中的各种变量引用、delegate 委托、静态变量等等会通过各种直接或间接的方式引用到资源,会导致垃圾回收机制无法正确回收资源。

环境

  • macOS 10.14.6
  • Unity 2018.4.25f1
  • Unity Memory Profiler 0.2.6-preview-1
  • Mumu 1.9.23(20200730)
  • Scripting Runtime Version: .Net 4.x Equivalent
  • Api Compatibility Level: .Net 4.x

Unity 内置 Profiler

不能使用 Unity 内置的 Profiler,虽然可以同时抓取编辑器与真机的数据,但是无法比较数据。也就意味着难以在大量数据中找到差异,快速定位问题。

必须使用真机测试才能发现问题,不能使用编辑器自身,因为分析工具会影响编辑器。如果使用 Unity 2020.2 中的独立于编辑器运行的 Profiler 应该可以。

Unity Memory Profiler

Unity 官方单独开发了一个专用的内存分析工具,现在处于预览状态。

此工具虽然支持快照功能,但是对于资源之间的引用显示并不是很合理,导致难以追踪不同对象间是如何引用在一起的,实际使用时基本只是当作辅助工具确认使用。

安装

可以在 Package Manager 中直接安装:

复制结果

因为需要将 UI 中的结果以文本形式复制到剪贴板中,方便后续查找,因此尝试输出 UI 选中行以下行的信息到 Console 中。

修改 OnGUI_CellMouseDown 方法代码

com.unity.memoryprofiler@0.2.6-preview.1/Editor/UI/DatabaseSpreadsheet.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
protected override void OnGUI_CellMouseDown(Database.CellPosition pos)
{
    //UnityEngine.Debug.Log("MouseDown at (" + Event.current.mousePosition.x + ", " + Event.current.mousePosition.y + " row:" + row + " col:" + col);

    var column = m_TableDisplay.GetColumnByIndex((int)pos.col);
    var metaColumn = m_TableDisplay.GetMetaData().GetColumnByIndex((int)pos.col);
    if (column != null)
    {
        var result = new System.Text.StringBuilder();
        for (int i = 0; i < 200 && i < column.GetRowCount() - pos.row; i++)
        {
            var str = column.GetRowValueString(pos.row + i, m_FormattingOptions.GetFormatter(metaColumn.FormatName));
            result.AppendLine(str);
        }
        UnityEngine.Debug.Log(result);
    }
}

Unity Memory Profiler 修改版

有很多修改版的 Unity Memory Profiler

这个版本是基于较老的版本修改的,UI 显示与现在的 0.2.6 完全不同;其次最后提交时间是 2019/06,已经过去一年半了,没有生命力。


这个版本也很旧,提升了差异项与提升序列化速度。


开发了UnityProfiler和MemoryCrawler两款工具,分别替代Profiler以及MemoryProfiler进行相同领域的性能调试,它们均使用纯C++实现,因为经过与C#、Python语言的测试对比后发现C++有绝对的计算优势,可以非常明显提升性能数据分析效率和稳定性。

Heap Explorer

找到一个非常合适的工具,GitHub 上的只支持 Unity 2019.3,BitBucket 上版本支持 Unity 2017.4 - 2019.2,

源码版本导入后有编译报错:

'AbstractTreeView.CanMultiSelect(TreeViewItem)': no suitable method found to override

DLL 版本没有编译报错,但是打开窗口时提示

The MemoryProfiling API does not work with .NET 4.x Scripting runtime, which your project makes use of.

实测 DLL 版本可用,可以直接无视上面的警告。

DLL 版本下载地址

其他链接

分析过程

模拟器

使用 Mumu 模拟器启动程序后,需要转发端口,然后在 Memory Profiler 中连接 127.0.0.1 adb forward tcp:55000 localabstract:Unity-com.jjyou.ydxj.traceless 55000

手机

需要打开开发者模式并允许 USB 调试

Unity

Unity | Window | Analysis | Profiler

点击 Editor 按钮,在弹出的下拉菜单中选择 127.0.0.1,就可以连接到手机或模拟器上。

分析方法

引用

不同项目会有不同类型的引用,要提前分类整理好,方便后续处理:

  • Unity 引用:通过 GUID 引用另一个资源,例如场景中保存其他 Prefab,Prefab 中引用其他 Prefab
  • C# 文本引用:通过表格数据中保存的 UI 路径字符串,在 UI 管理器动态加载
  • C# 静态引用:通过静态类对资源产生的引用,Unity 内置 Profiler 与 Memory Profiler 都无法查找,只是显示为 ManagedStaticReferences(),Heap Explorer 可以显示这种引用
  • Lua 文本引用:通过在 UI Prefab 中的字段中设置另一个 UI 的路径,在运行时通过 UI 管理器加载
  • Lua 文本引用:在 Lua 代码中指定要打开的界面路径,使用 C# 接口打开 UI
  • Lua 静态引用:通过 C# 接口加载完成资源后存放在 Lua 变量中

不光要查引用,还要查引用为什么没有在正确的时间被断开!这涉及到仔细分析代码,了解对象的生命周期,确定资源释放的时机。

分析问题

使用比较快照功能按类型分组、按大小排序,先处理纹理,一般来说纹理都是内存泄露的大头,按照 28 原则优先处理。

结合上面提到的各种引用方式,分析资源是如何被引用的,又是为何为断开引用释放。