unreal / 2024.08.29

Unreal Engine の Gameplay 框架

UObject 部分参考 InsideUE4 - UObject UObject 是 UE 中所有 C++ 对象的根基类,它是 UE 对象系统的重要组成部分。通过 UObject,UE 支持以下功能: 垃圾回收 引用更新 映象 序列化 默认属性变化自动更新 自动属性初始化 自动编辑器整合 运行时类型信息可用 网络复制 … 它有以下五个继承自 UObjectBase 的关键变量。

UObject

部分参考 InsideUE4 - UObject

UObject 是 UE 中所有 C++ 对象的根基类,它是 UE 对象系统的重要组成部分。通过 UObject,UE 支持以下功能:

  • 垃圾回收
  • 引用更新
  • 映象
  • 序列化
  • 默认属性变化自动更新
  • 自动属性初始化
  • 自动编辑器整合
  • 运行时类型信息可用
  • 网络复制

它有以下五个继承自 UObjectBase 的关键变量。

 1/**
 2 * 用于描述一个对象。比如
 3 *   - RF_ClassDefaultObject 表明它是 ClassDefaultObject;
 4 *   - RF_MarkAsRootSet 表明它不应被 GC,即使不被引用;
 5 *   - RF_NeedInitialization 表明该对象还未初始化完成状态;
 6 *   - ...
 7 * 这些描述符体现为一个 32 位无符号整数的 bit
 8 */
 9EObjectFlags ObjectFlags;
10
11/**
12 * GUObjectArray 的索引,用于实现「内存管理」与「垃圾回收」。
13 * 其中 GUObjectArray 是一个全局 UObject 数组。
14 * 每构造一个对象,都会在构造函数中调用 AddObject() 将其加入到数组中。
15 */
16int32        InternalIndex;
17
18/** 该 Object 所属的 UClass,用于实现「反射」 */
19ObjectPtr_Private::TNonAccessTrackedObjectPtr<UClass> ClassPrivate;
20
21/** 该 Object 的名字,用于实现「反射」 */
22FName        NamePrivate;
23
24/**
25 * 拥有该 Object 的 Object。
26 * 在运行时引用自己的对象可能有无穷个,但自己的 Outer 有且仅有一个。
27 * Outer 语义上就是真正拥有自己的那个对象。
28 */
29ObjectPtr_Private::TNonAccessTrackedObjectPtr<UObject> OuterPrivate;

后面四个对象都可以通过一个 public 接口 GetXXX() 获取。

UObject 的内存管理

UObject 与 UE 内存管理相关的所有逻辑是在其基类 UObjectBase 中实现的。

构造函数

首先观察 UObjectBase 的构造函数,发现执行了关键的一条函数 AddObject()

 1UObjectBase::UObjectBase(...)
 2{
 3	check(ClassPrivate);
 4	// Add to global table.
 5	AddObject(InName, InInternalFlags, InInternalIndex, InSerialNumber);
 6}	
 7
 8void UObjectBase::AddObject(FName InName, EInternalObjectFlags InSetInternalFlags,
 9                            int32 InInternalIndex, int32 InSerialNumber)
10{
11  ...
12  EInternalObjectFlags InternalFlagsToSet = InSetInternalFlags;
13  ...
14  GUObjectArray.AllocateUObjectIndex(this, InternalFlagsToSet,
15                                     InInternalIndex, InSerialNumber);
16  HashObject(this);
17}

这里主要关注其中两步,即 AllocateUObjectIndex()HashObject()

AllocateUObjectIndex()

这个函数的作用是将 Object 放到一个全局的名为 GUObjectArray 的对象中。顾名思义,该对象用于管理所有 UObject 的信息,之后要提的垃圾回收也是基于这个对象进行的。

 1void FUObjectArray::AllocateUObjectIndex(UObjectBase* Object,
 2                                         EInternalObjectFlags InitialFlags,
 3                                         int32 AlreadyAllocatedIndex,
 4                                         int32 SerialNumber)
 5{
 6  ...
 7  int32 Index = INDEX_NONE;
 8  LockInternalArray();
 9
10  // 如果已经分配过了
11  if (AlreadyAllocatedIndex >= 0)
12  {
13    Index = AlreadyAllocatedIndex;
14  }
15  // 如果开启了忽略 GC 的优化
16  else if (OpenForDisregardForGC && DisregardForGCEnabled())
17  {
18    Index = ++ObjLastNonGCIndex;
19    // 如果超过 忽略 GC 的最大 Object 数量,则需要扩容。
20    if (ObjLastNonGCIndex >= MaxObjectsNotConsideredByGC)
21    {
22      Index = ObjObjects.AddSingle();
23    }
24    MaxObjectsNotConsideredByGC = FMath::Max(MaxObjectsNotConsideredByGC,
25                                             ObjLastNonGCIndex + 1);
26  }
27  // 剩下就是有 GC 的情况
28  else
29  {
30    // 这里用「空闲列表」是因为后续回收的 Object 索引是不确定的
31    // 要用空闲列表防止产生碎片
32    if (ObjAvailableList.Num() > 0)
33    {
34      Index = ObjAvailableList.Pop();
35      const int32 AvailableCount = ObjAvailableList.Num();
36      checkSlow(AvailableCount >= 0);
37    }
38    else // 如果空间不足,就扩容
39    {
40      Index = ObjObjects.AddSingle();      
41    }
42    // 这个 check 信息量很大
43    // 可以得到 ObjFirstGCIndex > ObjLastNonGCIndex 总是成立的
44    // 这意味着 ObjectArray 划分为两部分,前面为 NonGC,后面为 NeedGC
45    // 同时 NonGC 会优先于 NeedGC 创建
46    // GC 过程不会访问 NonGC Object,它们也不能有指向 NeedGC 的引用
47    // 这样可以加快 GC 速度,因为只需遍历较少 Object。
48    check(Index >= ObjFirstGCIndex && Index > ObjLastNonGCIndex);
49  }
50  FUObjectItem* ObjectItem = IndexToObject(Index);
51  // 根据 Index 获取 ObjectArray 中的 Item,进行一些初始化工作
52  ...
53  UnlockInternalArray();
54
55  // 最后挨个通知订阅了 ObjectCreated 事件的 Listener
56  ...
57}

HashObject()

这个函数是利用 FName 的唯一性,求出 FName 的哈希值 Hash 后,建立 Hash 到 Object 的映射,从而实现根据名字访问对象。

 1void HashObject(UObjectBase* Object)
 2{
 3  FName Name = Object->GetFName();
 4  if (Name != NAME_None)
 5  {
 6    ...
 7    int32 Hash = 0;
 8
 9    FUObjectHashTables& ThreadHash = FUObjectHashTables::Get();
10    FHashTableLock HashLock(ThreadHash);
11
12    // 因为一个 FName 唯一确定一个 Object
13    // 所以可以直接用 FName 进行 Hash
14    Hash = GetObjectHash(Name);
15    ...
16    ThreadHash.AddToHash(Hash, Object);
17    if (PTRINT Outer = (PTRINT)Object->GetOuter())
18    {
19      // 这次 Hash 会加入 Outer 的信息
20      Hash = GetObjectOuterHash(Name, Outer);
21      ...
22      ThreadHash.HashOuter.Add(Hash, Object->GetUniqueID());
23      // ObjectOuterMap(Outer).Add(Object)
24      AddToOuterMap(ThreadHash, Object);
25    }
26    // ClassToObjectListMap(Object.Class).Add(Object)
27    AddToClassMap( ThreadHash, Object );
28  }
29}

FreeUObjectIndex()

该函数和 AllocateUObjectIndex() 对应,但是只在析构函数中调用,作用是回收 GUObjectArray 中的 item。

 1void FUObjectArray::FreeUObjectIndex(UObjectBase* Object)
 2{
 3  ...
 4	// This should only be happening on the game thread (GC runs only on game thread when it's freeing objects)
 5	check(IsInGameThread() || IsInGarbageCollectorThread());
 6
 7	// No need to call LockInternalArray(); here as it should already be locked by GC
 8	int32 Index = Object->InternalIndex;
 9	FUObjectItem* ObjectItem = IndexToObject(Index);
10  // 将 ObjectItem 重置回默认值
11	...
12	if (Index > ObjLastNonGCIndex && !GExitPurge && bShouldRecycleObjectIndices)
13	{
14		ObjAvailableList.Add(Index);
15	}
16}

UObject 的创建

因为 UObject 将其所有构造函数都设为了 private,所以我们只能通过以下两种方式构造一个 UObject:

  • NewObject<ObjectClass>()
  • CreateDefaultSubobject<ObjectClass>()

实际上内部都是将参数统一打包成一个 FStaticConstructObjectParameters 结构体,传入并调用 StaticConstructObject_Internal() 函数。

 1UObject* StaticConstructObject_Internal(const FStaticConstructObjectParameters& Params)
 2{
 3	...
 4	UObject* Result = NULL;
 5	// Subobjects are always created in the constructor,
 6  //   no need to re-create them unless their archetype != CDO
 7  //   or they're blueprint generated.
 8	// If the existing subobject is to be re-used
 9  //   it can't have BeginDestroy called on it
10  //   so we need to pass this information to StaticAllocateObject.	
11	const bool bIsNativeClass = InClass->HasAnyClassFlags(CLASS_Native | CLASS_Intrinsic);
12	const bool bIsNativeFromCDO = bIsNativeClass &&
13		  (
14			!InTemplate ||
15			(InName != NAME_None && (Params.bAssumeTemplateIsArchetype || InTemplate == UObject::GetArchetypeFromRequiredInfo(InClass, InOuter, InName, InFlags)))
16			);
17	const bool bCanRecycleSubobjects = bIsNativeFromCDO && (!(InFlags & RF_DefaultSubObject) || !FUObjectThreadContext::Get().IsInConstructor);
18
19	bool bRecycledSubobject = false;	
20	Result = StaticAllocateObject(InClass, InOuter, InName, InFlags, Params.InternalSetFlags, bCanRecycleSubobjects, &bRecycledSubobject, Params.ExternalPackage);
21
22	// Don't call the constructor on recycled subobjects, they haven't been destroyed.
23	if (!bRecycledSubobject)
24	{
25    ...
26		(*InClass->ClassConstructor)(FObjectInitializer(Result, Params));
27	}
28  ...
29	return Result;
30}

这里的逻辑相对来说比较复杂。首先看最开始的三个 const bool

首先看第一个 bIsNativeClass,其内容比较好理解,就是判断当前的类是不是 CLASS_Native/CLASS_Intrinsic 二者之一。前者代表这个类是在 C++ 而不是蓝图中定义的,而后者则表示该类是由 C++ 直接声明,未经过 UHT 生成反射代码,一般是引擎内部的类才会用到。

第二个 bIsNativeFromCDO 指的是是否根据**类默认对象(Class Default Object, CDO)**创建。

第三个 bCanRecycleSubobjects 指的是是否可以重复利用子对象(Subobject)。其中 RF_DefaultSubObject 标签只会在 CreateDefaultSubobject 时被赋予。也就是说如果一个对象不是通过 CDO 创建,且不是一个创建中的 Subobject,那么就可以对原来的 Subobject 进行重复利用。

至于为什么有「重复利用」一说,还得接着看后续的 StaticAllocateObject() 函数做了什么。

StaticAllocateObject()

这个函数实际做的工作是创建一个 Object 实例或者替换一个已经存在的 Class/Outer/Name 都一样的 Object。执行替换操作时,原 Object 将会被销毁,新 Object 将会占用其内存空间。

 1UObject* StaticAllocateObject(...)
 2{
 3	...
 4	UObject* Obj = NULL;
 5	if(InName == NAME_None)
 6	{ // 如果创建时没有设置 Name,则生成一个独一无二的 Name
 7    ...
 8		InName = MakeUniqueObjectName(InOuter, InClass);
 9	}
10	else
11	{
12		// 查找是否存在 Outer Name 一样的 Object
13		Obj = StaticFindObjectFastInternal( /*Class=*/ NULL, InOuter, InName, true );
14		// 类型必须一致,否则 fatal
15		if (Obj && !Obj->GetClass()->IsChildOf(InClass)) { ... }
16	}
17
18	...
19	if( Obj == nullptr )
20	{	// 如果不存在上述 Object,就调用 AllocateUObject() 进行内存分配
21		...
22		Obj = (UObject *)GUObjectAllocator.AllocateUObject(...);
23	}
24	else
25	{ // 否则,直接替换	
26		if (!bCreatingCDO && (!bCanRecycleSubobjects || !Obj->IsDefaultSubobject()))
27		{ // 如果不是 CDO ,或者是蓝图类,那就要进行 Destory
28			if(!Obj->HasAnyFlags(RF_FinishDestroyed))
29			{
30				// 如果没完成 Destroy,则销毁原来的 Object
31				Obj->ConditionalBeginDestroy();
32				...
33				Obj->ConditionalFinishDestroy();
34			}
35			...
36			Obj->~UObject();
37			...
38		}
39		else
40		{
41			bSubObject = true;
42		}
43	}
44  ...
45	if (!bSubObject)
46	{ // 在此之前已经在这块内存上调用过析构函数,故需要重新格式化一下
47    // 并用 placement new 构造一个 Object
48		FMemory::Memzero((void *)Obj, TotalSize);
49		new ((void *)Obj) UObjectBase(...);
50	}
51	else
52	{ // 反之,只需要修改原 Object 的一些变量即可,也就是进行了重用
53		Obj->SetFlags(InFlags);
54		Obj->SetInternalFlags(InternalSetFlags);
55	}
56  ...
57	return Obj;
58}

在 placement new 的过程中,就会执行 UObject 的构造函数,进而执行一些和 GUObjectArray 的交互行为。随着 return Obj; 的执行,至此,一个新的 UObject 就被创建好了。

UObject 的销毁

自动销毁

可以将指向 UObject 的指针置空,这样后续就会被 UE 自动回收掉

1Obj = NewObject<UObject>(this, TEXT("Obj"));
2Obj = nullptr;

主动销毁

UObject::ConditionalBeginDestroy()

异步执行且对象在当前帧内持续有效 等待下次GC Obj->ConditionalBeginDestroy(); Obj = nullptr; MarkPendingKill()

标记为PendingKill,等待回收。指向此实例的指针将设置为NULL,并在下一次GC时删除。 IsPendingKill 判断是否处于 PendingKill 状态 ClearPendingKill 清除 PendingKill 状态 Obj->MarkPendingKill(); Obj = nullptr; Engine\Config \BaseEngine.ini 更改下面参数,设置销毁时间间隔

gc.TimeBetweenPurgingPendingKillObjects=60 强制垃圾回收 UWorld::ForceGarbageCollection 弃用

GEngine->ForceGarbageCollection

GEngine->ForceGarbageCollection(true);

UClass