Garbage Collection in UE4

导语
UE4 C++ 不同于原生的C++, UE内置了GC系统。对于有些人来说,GC帮我们处理了弃用的对象,防止了内存泄漏,确实是一件令人愉快的事情。然后有的时候,如果不了解GC规则,可能会给开发带来大麻烦。

什么对象会被GC 系统处理?

GC主要思想很简单——要让 GC系统 相信对象是多余的并删除它,必须满足以下几个条件:

  • 这个对象不再被UE 的反射系统引用。

  • 指向对象的指针没有被保存在容器中。

  • 对象在其作用域内没有被强指针指向,比如shared pointers, shared references, unique pointers

  • 创建对象的代码块经结束执行(离开作用域)

  • 对象没有添加到根节点 AddToRoot()

  • 对象必须是继承自Uobject

有一个要特殊注意的例外,我们可以显式地将UObject派生的类对象标记为销毁。

常见问题

  • 场景中的 Actor 和 Actor 组件,被他们的父对象引用(比如 level 本身)。在场景中生成的actor,不需要特殊的GC 考虑
  • 当一个新的关卡或者地图加载的时候,引擎将销毁world 并且创建一个新的world, 所有旧场景中的对象将被垃圾回收,包括GM GS,以及它们引用或指针指向的所有内容(假设这是指向对象的唯一引用或指针)。
  • 并不是所有的不可用对象的指针都等于nullptr,因为如果一个对象被标记为销毁或者没有被正确初始化,那么它的指针可能不是nullptr。因此要养成用IsValid()来判断对象是否可用的习惯。
  • 任何对象类如果不是继承自UObject 将不会被GC回收,也就是我们创建的对象类最好是UE 已经存在的类,至少也应该继承自 UObject.

GC对象树

到目前为止,我们已经讨论了UE引擎的GC规则和原理,那么它在技术上是如何实现的呢?

UE引擎维护了一张所有对象的树状图,在树的最根部节点是永远不会被回收的。

每当需要GC时,引擎将从根集合开始,并通过[反射系统]查看它们引用和指向的对象(https://www.unrealengine.com/en-US/blog/unreal-property-system-reflection)或容器类。任何指向或引用的内容都将添加到“不可触及”列表。然后,它将检查新添加的对象指向或引用的对象,并将所有这些对象也添加到树中。通过这种方式沿着树移动,垃圾收集系统最终会构建一个所有不可接触对象的列表,并删除所有其他对象。

img

如果一个对象可以通过引擎的属性系统指针追溯到根集,它将不会被垃圾收集。一旦这些与根集的联系被切断,对象将被GC掉。

Slate UI

需要注意的是Slate UI 是不太适用之前的GC 规则,从4.25开始,它不使用强指针来保持用户界面中使用的对象(通常是uasset)。这意味着如果一个对象被Slate UI引用,但是不再被其他对象引用,那么GC将无情地删除它。而且,当它使用的对象被GC时,Slate UI可能会使程序崩溃。

为了处理这一问题,请确保传递给Slate UI的每个对象都已受到严格保护,不受垃圾收集的影响。如有必要,可以使用显式强指针,但通常这将通过属性系统完成。例如,可以在垃圾收集图的根集中创建一个新对象,并让它保存与Slate UI小部件相对应的其他对象数组。这些对象将指向Slate UI使用的资源对象。每个专用于自己的Slate UI小部件的对象都可能在小部件启动后被销毁。

调试

有时我们的代码中有一个问题可能是由垃圾收集引起的,但我们还不能确定。这很可能是一个对象初始化问题,也可能是一系列其他问题。

我们可以使用断言在代码执行的任何阶段测试对象是否有效:

1
2
3
4
5
6
7


checkf(IsValid(Object), TEXT(" object is invalid !"));



check(IsValid(Object));

### 正确示例

第一个很好的习惯用法是在容器类(如TArray)中使用或者用UPROPERTY()声明指针。

一旦不再有“父”对象以这种方式指向“子”对象,“子”对象才能销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 头文件中的声明

UPROPERTY()

UMyCustomClass * MyPointer;

UPROPERTY()

UTexture2D * MyTexture;

UPROPERTY()

TArray<UStaticMesh*> MyArrayOfMeshes;

我们可以自由地从多个其他对象指向同一对象。只要至少有一个活动对象使用属性系统指向它,它就不会被垃圾收集。

“临时对象”会在作用域结束执行后进行垃圾收集。

下面是一段功能代码,它临时创建一个新对象,以从场景捕获组件捕获图片并将其存储到像素颜色数组中。

此范围结束后,TextureEnderTarget 本身将被GC适当地清除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

// 部分定义在 ASceneCapture2D-derived class 中的函数

const uint16 Resolution = 512;

UTextureRenderTarget2D * TextureRenderTarget = NewObject<UTextureRenderTarget2D >();

USceneCaptureComponent2D* SCC2D = GetCaptureComponent2D();

TextureRenderTarget->InitCustomFormat(Resolution,Resolution,PF_B8G8R8A8,**false**);

SCC2D->TextureTarget = TextureRenderTarget;

SCC2D->CaptureScene();

TArray<FColor> RawPixels;

RawPixels.Reserve(Resolution * Resolution);

TextureRenderTarget->GameThread_GetRenderTargetResource()->ReadPixels(RawPixels);

RawPixels.Shrink();

错误示例

两个坏习惯:

1、是将所有对象都 AddToRoot()

2、创建不符合上述GC规则的对象,但是又没有被及时的删除

有时在函数定义的范围内使用新指针创建对象,并将其指针返回(返回临时对象了)。有时,甚至大多数时候,它可能会起作用。

还有一个个坏习惯——过度依赖nullptr:

1
2
3
**if**(ObjectPointer) {
...
}

看起来非常方便,但即使对象未初始化或标记为kill,ObjectPointer的计算结果也将为true。如果要使用对象,最好使用 IsValid(ObjectPointer)来判断。

### 总结

总而言之,GC 系统是UE引擎使得代码强健的一部分,有效避免了内存泄漏。

------------- 感谢您的阅读-------------
作者dreamingpoet
有问题请发邮箱 Dreamingoet@126.com
您的鼓励将成为创作者的动力