轻松掌握MemoryProfiler
- MemoryProfiler的基本概念
- 如何获取MemoryProfile
- MemoryProfiler内存分析实践
- 正常GC,合理释放对象的引用
- 无法正常GC,对象引用没有合理释放。
- 总结
MemoryProfiler的基本概念
Unity 性能分析器 (Unity Profiler)
是一种可以用来获取应用程序性能信息的工具。可以将性能分析器连接到网络中的设备或连接到已连接到计算机的设备,从而测试应用程序在目标发布平台上的运行情况。还可以在
Editor 中运行性能分析器,从而在开发应用程序时概要了解资源分配情况。
有关Unity中的MemoryProfiler工具的详细介绍,可以参考这个链接:性能分析器概述
总之:MemoryProfiler是一个可以用于在Unity的Editor模式和Runtime模式下检测应用的内存使用情况的工具,它是一个免费的Unity Package。
如何获取MemoryProfile
本文内容以Unity 2021.3.8f1c1版本为教学版本。(提供PackageManager的unity版本均适用)
1.使用Unity 2021.3.8f1c1版本打开或者新建任意一个需要进行Memory分析的项目。
2.通过编辑器顶部菜单栏打开PackageManager
3.安装MemoryProfiler包
点击PackageManager面板左上角的加号,在弹出的菜单项中选择第四个:Add package by name
在弹出的输入框内输入包名: com.unity.memoryprofiler。并点击输入框右侧的“Add”按钮,开始安装memoryprofiler
等待包安装结束后,就能在包列表中看到刚刚安装的Memory Profiler。
MemoryProfiler内存分析实践
这里我们通过两个示例来说明如何通过MemoryProfiler分析项目的内存情况, 找到内存泄漏的问题。
其中第一个示例演示正常GC后的内存情况,第二个示例演示对象引用没有正常释放导致无法GC的情况。
正常GC,合理释放对象的引用
示例脚本CorrectGC.cs内容:
1 using System;
2 using System.Collections;
3 using System.Collections.Generic;
4 using UnityEngine;
5
6 public class CorrectGC : MonoBehaviour
7 {
8 private class ManagedObjectTest
9 {
10 public void TestFunc()
11 {
12 Debug.Log("TestFunc!!!");
13 }
14 }
15
16 private Action callBack = null;
17 private ManagedObjectTest objTest = null;
18
19 private void CreateManagedObject()
20 {
21 objTest = new ManagedObjectTest();
22 callBack = objTest.TestFunc;
23 }
24
25 private void DestroyManagedObject()
26 {
27 if(objTest != null)
28 {
29 objTest = null;
30 callBack = null;
31 GC.Collect();
32 }
33 }
34
35 // Start is called before the first frame update
36 void Start()
37 {
38 CreateManagedObject();
39 }
40
41 // Update is called once per frame
42 void Update()
43 {
44 if (Input.GetKeyDown(KeyCode.R))
45 {
46 DestroyManagedObject();
47 }
48 }
49 }
从上面的代码可以看到,我们定义一个类ManagedObjectTest用来模拟实际业务中创建的对象。
在Start中通过CreateManagedObject方法创建一个ManagedObjectTest对象。
在Update中通过检测按键R的按下来释放之前创建的ManagedObjectTest对象。
首先将此脚本挂载到场景中任意一个活动的GameObject对象上。
运行此场景,然后打开MemoryProfiler工具
此时点击Memory Profiler面板中的“Capture New Snapshot”按钮,创建一个当前状态下的内存快照。
选中创建好的内存快照
切换到“Objects and Allocations”面板
我们通过类型名称来验证是否创建了我们代码中编写的ManagedObjectTest对象。
该对象的内存分析:
从上图可以看出,确实创建了一个ManagedObjectTest对象,并且从右侧的References面板可以看到这个对象一共有两个引用,其中一个是脚本中的ManagedObjectTest变量,另一个是Action类型的对象,它引用的是ManagedObjectTest对象中的TestFunc函数,所以也间接引用了此对象。
此时,我们切换到正在运行的Unity场景中,按下按键R,然后再次创建一个内存快照。
选中新创建的内存快照。继续切换到“Objects and Allocations”面板,用同样的方法查找看看ManagedObjectTest对象是否被释放掉了。
通过上图我们可以看到,对象已经被成功释放掉了。
无法正常GC,对象引用没有合理释放。
这次,我们将上面的脚本代码做一个更改:去掉DestroyManagedObject方法中的callBack=null;这行。
示例代码:
1 using System;
2 using System.Collections;
3 using System.Collections.Generic;
4 using UnityEngine;
5
6 public class CorrectGC : MonoBehaviour
7 {
8 private class ManagedObjectTest
9 {
10 public void TestFunc()
11 {
12 Debug.Log("TestFunc!!!");
13 }
14 }
15
16 private Action callBack = null;
17 private ManagedObjectTest objTest = null;
18
19 private void CreateManagedObject()
20 {
21 objTest = new ManagedObjectTest();
22 callBack = objTest.TestFunc;
23 }
24
25 private void DestroyManagedObject()
26 {
27 if(objTest != null)
28 {
29 objTest = null;
30 GC.Collect();
31 }
32 }
33
34 // Start is called before the first frame update
35 void Start()
36 {
37 CreateManagedObject();
38 }
39
40 // Update is called once per frame
41 void Update()
42 {
43 if (Input.GetKeyDown(KeyCode.R))
44 {
45 DestroyManagedObject();
46 }
47 }
48 }
这次我们在编辑器运行之后直接按下按键R,并创建一个新的内存快照。
此时我们发现,ManagedObjectTest对象并没有被正常释放掉,原因就在于我们删掉的那一行代码。从右侧的References面板中也可以看出来,ManagedObjectTest对象的引用少了一个,但是由于脚本中还有callBack对象引用着ManagedObjectTest对象的TestFunc方法,所以导致该对象没办法被GC(仅仅是引用计数少了一次,但是此时引用计数不为0,无法GC)。
总结
通过以上例子我们可以得出结论:C#中的GC会根据对象的引用计数来进行内存回收,当对象的引用计数为0的时候,对象的内存能过够被正常释放,否则不能。其中有关GC的一些细节可以参考此链接:垃圾回收的基本原理
分析上面我们举的两个例子,为什么第二个例子中ManagedObjectTest对象没办法被GC呢,因为我们CreateManagedObject方法中创建完对象之后,将对象的成员方法绑定到了脚本中的callBack对象。callBack对象的生命周期是高于ManagedObjectTest对象中的TestFunc方法的,这个赋值操作就导致我们提升了ManagedObjectTest对象的TestFunc方法的生命周期,使它可能高于了ManagedObjectTest对象本身。所以当我们除了给脚本中的objTest对象置为null之外忘记了同时给callBack置为null,就会导致ManagedObjectTest对象的TestFunc此时还是被callBack引用,这样就导致ManagedObjectTest对象无法被正常GC。现实情况中,如果我们没有仔细分析对象的生命周期,没有注意这种隐含的提升对象生命周期的代码逻辑,很有可能会在项目中产生对象的内存泄漏。