Unreal Engine の Gameplay 框架


UObject

部分参考 InsideUE4 - UObject

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

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

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

/**
 * 用于描述一个对象。比如
 *   - RF_ClassDefaultObject 表明它是 ClassDefaultObject;
 *   - RF_MarkAsRootSet 表明它不应被 GC,即使不被引用;
 *   - RF_NeedInitialization 表明该对象还未初始化完成状态;
 *   - ...
 * 这些描述符体现为一个 32 位无符号整数的 bit
 */
EObjectFlags ObjectFlags;

/**
 * GUObjectArray 的索引,用于实现「内存管理」与「垃圾回收」。
 * 其中 GUObjectArray 是一个全局 UObject 数组。
 * 每构造一个对象,都会在构造函数中调用 AddObject() 将其加入到数组中。
 */
int32        InternalIndex;

/** 该 Object 所属的 UClass,用于实现「反射」 */
ObjectPtr_Private::TNonAccessTrackedObjectPtr<UClass> ClassPrivate;

/** 该 Object 的名字,用于实现「反射」 */
FName        NamePrivate;

/**
 * 拥有该 Object 的 Object。
 * 在运行时引用自己的对象可能有无穷个,但自己的 Outer 有且仅有一个。
 * Outer 语义上就是真正拥有自己的那个对象。
 */
ObjectPtr_Private::TNonAccessTrackedObjectPtr<UObject> OuterPrivate;

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

UObject 的内存管理

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

构造函数

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

UObjectBase.cpp
UObjectBase::UObjectBase(...) { check(ClassPrivate); // Add to global table. AddObject(InName, InInternalFlags, InInternalIndex, InSerialNumber); } void UObjectBase::AddObject(FName InName, EInternalObjectFlags InSetInternalFlags, int32 InInternalIndex, int32 InSerialNumber) { ... EInternalObjectFlags InternalFlagsToSet = InSetInternalFlags; ... GUObjectArray.AllocateUObjectIndex(this, InternalFlagsToSet, InInternalIndex, InSerialNumber); HashObject(this); }

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

AllocateUObjectIndex()

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

UObjectArray.cpp
void FUObjectArray::AllocateUObjectIndex(UObjectBase* Object, EInternalObjectFlags InitialFlags, int32 AlreadyAllocatedIndex, int32 SerialNumber) { ... int32 Index = INDEX_NONE; LockInternalArray(); // 如果已经分配过了 if (AlreadyAllocatedIndex >= 0) { Index = AlreadyAllocatedIndex; } // 如果开启了忽略 GC 的优化 else if (OpenForDisregardForGC && DisregardForGCEnabled()) { Index = ++ObjLastNonGCIndex; // 如果超过 忽略 GC 的最大 Object 数量,则需要扩容。 if (ObjLastNonGCIndex >= MaxObjectsNotConsideredByGC) { Index = ObjObjects.AddSingle(); } MaxObjectsNotConsideredByGC = FMath::Max(MaxObjectsNotConsideredByGC, ObjLastNonGCIndex + 1); } // 剩下就是有 GC 的情况 else { // 这里用「空闲列表」是因为后续回收的 Object 索引是不确定的 // 要用空闲列表防止产生碎片 if (ObjAvailableList.Num() > 0) { Index = ObjAvailableList.Pop(); const int32 AvailableCount = ObjAvailableList.Num(); checkSlow(AvailableCount >= 0); } else // 如果空间不足,就扩容 { Index = ObjObjects.AddSingle(); } // 这个 check 信息量很大 // 可以得到 ObjFirstGCIndex > ObjLastNonGCIndex 总是成立的 // 这意味着 ObjectArray 划分为两部分,前面为 NonGC,后面为 NeedGC // 同时 NonGC 会优先于 NeedGC 创建 // GC 过程不会访问 NonGC Object,它们也不能有指向 NeedGC 的引用 // 这样可以加快 GC 速度,因为只需遍历较少 Object。 check(Index >= ObjFirstGCIndex && Index > ObjLastNonGCIndex); } FUObjectItem* ObjectItem = IndexToObject(Index); // 根据 Index 获取 ObjectArray 中的 Item,进行一些初始化工作 ... UnlockInternalArray(); // 最后挨个通知订阅了 ObjectCreated 事件的 Listener ... }

HashObject()

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

void HashObject(UObjectBase* Object)
{
  FName Name = Object->GetFName();
  if (Name != NAME_None)
  {
    ...
    int32 Hash = 0;

    FUObjectHashTables& ThreadHash = FUObjectHashTables::Get();
    FHashTableLock HashLock(ThreadHash);

    // 因为一个 FName 唯一确定一个 Object
    // 所以可以直接用 FName 进行 Hash
    Hash = GetObjectHash(Name);
    ...
    ThreadHash.AddToHash(Hash, Object);
    if (PTRINT Outer = (PTRINT)Object->GetOuter())
    {
      // 这次 Hash 会加入 Outer 的信息
      Hash = GetObjectOuterHash(Name, Outer);
      ...
      ThreadHash.HashOuter.Add(Hash, Object->GetUniqueID());
      // ObjectOuterMap(Outer).Add(Object)
      AddToOuterMap(ThreadHash, Object);
    }
    // ClassToObjectListMap(Object.Class).Add(Object)
    AddToClassMap( ThreadHash, Object );
  }
}

FreeUObjectIndex()

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

UObjectArray.cpp
void FUObjectArray::FreeUObjectIndex(UObjectBase* Object) { ... // This should only be happening on the game thread (GC runs only on game thread when it's freeing objects) check(IsInGameThread() || IsInGarbageCollectorThread()); // No need to call LockInternalArray(); here as it should already be locked by GC int32 Index = Object->InternalIndex; FUObjectItem* ObjectItem = IndexToObject(Index); // 将 ObjectItem 重置回默认值 ... if (Index > ObjLastNonGCIndex && !GExitPurge && bShouldRecycleObjectIndices) { ObjAvailableList.Add(Index); } }

UObject 的创建

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

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

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

UObject* StaticConstructObject_Internal(const FStaticConstructObjectParameters& Params)
{
  ...
  UObject* Result = NULL;
  // Subobjects are always created in the constructor,
  //   no need to re-create them unless their archetype != CDO
  //   or they're blueprint generated.
  // If the existing subobject is to be re-used
  //   it can't have BeginDestroy called on it
  //   so we need to pass this information to StaticAllocateObject.	
  const bool bIsNativeClass = InClass->HasAnyClassFlags(CLASS_Native | CLASS_Intrinsic);
  const bool bIsNativeFromCDO = bIsNativeClass &&
      (
      !InTemplate ||
      (InName != NAME_None && (Params.bAssumeTemplateIsArchetype || InTemplate == UObject::GetArchetypeFromRequiredInfo(InClass, InOuter, InName, InFlags)))
      );
  const bool bCanRecycleSubobjects = bIsNativeFromCDO && (!(InFlags & RF_DefaultSubObject) || !FUObjectThreadContext::Get().IsInConstructor);

  bool bRecycledSubobject = false;	
  Result = StaticAllocateObject(InClass, InOuter, InName, InFlags, Params.InternalSetFlags, bCanRecycleSubobjects, &bRecycledSubobject, Params.ExternalPackage);

  // Don't call the constructor on recycled subobjects, they haven't been destroyed.
  if (!bRecycledSubobject)
  {
    ...
    (*InClass->ClassConstructor)(FObjectInitializer(Result, Params));
  }
  ...
  return Result;
}

这里的逻辑相对来说比较复杂。首先看最开始的三个 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 将会占用其内存空间。

UObject* StaticAllocateObject(...)
{
  ...
  UObject* Obj = NULL;
  if(InName == NAME_None)
  { // 如果创建时没有设置 Name,则生成一个独一无二的 Name
    ...
    InName = MakeUniqueObjectName(InOuter, InClass);
  }
  else
  {
    // 查找是否存在 Outer Name 一样的 Object
    Obj = StaticFindObjectFastInternal( /*Class=*/ NULL, InOuter, InName, true );
    // 类型必须一致,否则 fatal
    if (Obj && !Obj->GetClass()->IsChildOf(InClass)) { ... }
  }

  ...
  if( Obj == nullptr )
  {	// 如果不存在上述 Object,就调用 AllocateUObject() 进行内存分配
    ...
    Obj = (UObject *)GUObjectAllocator.AllocateUObject(...);
  }
  else
  { // 否则,直接替换	
    if (!bCreatingCDO && (!bCanRecycleSubobjects || !Obj->IsDefaultSubobject()))
    { // 如果不是 CDO ,或者是蓝图类,那就要进行 Destory
      if(!Obj->HasAnyFlags(RF_FinishDestroyed))
      {
        // 如果没完成 Destroy,则销毁原来的 Object
        Obj->ConditionalBeginDestroy();
        ...
        Obj->ConditionalFinishDestroy();
      }
      ...
      Obj->~UObject();
      ...
    }
    else
    {
      bSubObject = true;
    }
  }
  ...
  if (!bSubObject)
  { // 在此之前已经在这块内存上调用过析构函数,故需要重新格式化一下
    // 并用 placement new 构造一个 Object
    FMemory::Memzero((void *)Obj, TotalSize);
    new ((void *)Obj) UObjectBase(...);
  }
  else
  { // 反之,只需要修改原 Object 的一些变量即可,也就是进行了重用
    Obj->SetFlags(InFlags);
    Obj->SetInternalFlags(InternalSetFlags);
  }
  ...
  return Obj;
}

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

UObject 的销毁

自动销毁

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

自动销毁
Obj = NewObject<UObject>(this, TEXT("Obj")); Obj = nullptr;

主动销毁

UObject::ConditionalBeginDestroy()

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

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

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

GEngine->ForceGarbageCollection

GEngine->ForceGarbageCollection(true);

UClass


  目录