WECHAT

扫码添加微信

YoungFan 微信二维码
← 返回 UE5 笔记
UObject对象构造 封面
开发笔记已归档

UObject对象构造

UObject对象基本构造流程

UE5UObjectC++

前置概念

Class Default Object与构造桥接函数

UE为每个UClass创建一个类默认对象Class Default Object,并且只有一个。创建普通实例时,UE通常以该类的CDO或显示指定的Archetype作为默认数据来源,通过FProperty属性链初始化反射属性,并根据需要实例化默认子对象。

UHT为每个UClass生成一个“构造桥接函数”,并且由ClassConstructor成员保存该函数指针。

例如:

UCLASS()
class AMyActor : public AActor
{
    GENERATED_BODY()

public:
    AMyActor(const FObjectInitializer& ObjectInitializer);
};

UHT会生成一个隐藏的静态函数:

static void __DefaultConstructor(const FObjectInitializer& X)
{
    // placement new:不分配内存,在现有地址上构造对象
    new((void*)X.GetObj()) AMyActor(X);
}

这个函数的作用只有一个:

在已分配内存的前提下,把this指针套进构造函数体执行一次。基于C++的placement new语法,在已分配的内存上建立完整的C++对象生命周期,包括基类构造、构造函数体和成员初始化。

然后将静态函数赋予构造函数指针:

MyActor_UClass->ClassConstructor = &__DefaultConstructor;

CDO、Archetype、Template的区别

CDO:每个UClass的默认对象。

Template:创建对象时显示传入的模板,最终可能成为ObjectArchetype。

Archetype:对象初始化时参考的模板对象。通常时CDO,但不一定。例如:调用NewObject时指定了Template参数,Template对象将作为本次构造的ObjectArchetype。

NewObject()创建UObject对象

大致顺序为:

StaticConstructObject_Internal() -> StaticAllocateObject()
  ├─ 通过placement new在分配的内存上调用UObjectBase构造函数
  └─ UObjectBase构造函数内调用AddObject()向全局对象表注册自身(GUObjectArray.AllocateUObjectIndex())
  ↓
创建 FObjectInitializer 临时对象
  ↓
构造桥接函数ClassConstructor,并传入FObjectInitializer
  ↓
ClassConstructor内通过placement new调用目标类型C++构造函数
  ↓
C++构造函数(父类构造函数/当前类构造函数)
  ↓
FObjectInitializer 析构
  ↓
FObjectInitializer::PostConstructInit()
  ├─ 属性初始化
  ├─ Config 加载
  ├─ 子对象属性初始化
  └─ 子对象实例化

StaticAllocateObject()

如果传入的InName == NAME_None,则先生成唯一名称;如果显示提供名称,则尝试查找相同Outer + Name的已有对象。

如果不存在已有对象:

  • 调用GUObjectAllocator.AllocateUObject()分配全新内存

否则:

  • 在已有对象地址上重构:
    • 可复用的DefaultSubobject
      • 不销毁
      • 不清零
      • 不执行ClassConstructor
      • 只更新Flags等状态
    • 其他对象
      • 复用Linker、Flags、Index、SerialNumber
      • 调用ConditionalBeginDestroy()
      • 等待IsReadyForFinishDestroy
      • 调用ConditionalFinishDestroy()销毁对象。
      • 调用GUObjectArray.FreeUObjectIndex释放全局数组中的槽位
      • 调用析构函数Obj->~UObject()
      • 清理内存FMemory::Memzero((void *)Obj, TotalSize)并调用构造函数new ((void *)Obj) UObjectBase()
      • UObjectBase构造函数内调用GUObjectArray.AllocateUObjectIndex()重新向全局对象表注册自身
      • 重新设置Linker。

调用UHT生成的构造桥接函数,建立FObjectInitializer临时对象

通过placement new对已有内存上建立对象生命周期并执行:

  • 基类构造
  • 成员初始化
  • 构造函数体
  • 编译器需要的vptr设置

UE中代码:

(*InClass->ClassConstructor)(FObjectInitializer(…))

接下来就是执行对象的C++构造函数体内的代码,比如:调用CreateDefaultSubobject()创建组件对象。

注意:如果上一步StaticAllocateObject()返回bRecycledSubobject为true,表示当前正在创建的是子对象,则不会执行构造桥接函数。

FObjectInitializer析构函数

FObjectInitializer为C++构造函数执行期间提供当前对象、Archetype、默认子对象覆盖等上下文。

当前面的C++构造函数体执行完毕后,传入的FObjectInitializer对象就要退出作用域并执行析构函数了。

析构函数~FObjectInitializer负责在C++构造函数后完成UObject的创建。

如果当前对象的类型不是UObject基类本身,且没有显示提供ObjectArchetype,则尝试调用Class->GetDefaultObject()获取CDO或创建CDO作为ObjectArchetype。

创建Class Default Object

Class->GetDefaultObject()内最终调用到UClass::CreateDefaultObject()执行创建CDO的具体过程:

  • 同样调用StaticAllocateObject()分配内存,但会附带标记RF_ClassDefaultObject和RF_ArchetypeObject,表示当前创建的是CDO或模板对象。
  • 同样通过调用构造桥接函数(*ClassConstructor)(FObjectInitializer())建立对象生命周期。
  • 调用CDO对象的PostCDOContruct()。

CDO也通过FObjectInitializer完成收尾初始化,但它通常以父类CDO作为继承属性来源;普通实例则通常以当前类CDO或指定的Template作为默认数据来源。

接下来析构函数会调用PostConstructInit()进行对象的初始化工作。

FObjectInitializer::PostConstructInit()

PostConstructInit()函数负责完成UE对象系统层面的初始化:

  • 根据ObjectArchetype或CDO初始化属性。
  • 按需要加载Config。即Obj->LoadConfig()。
  • 实例化子对象InstanceSubobjects()。
  • 调用对象和类型相关的生命周期函数或回调函数。例如:Obj->PostInitProperties()、Class->PostInitInstance()。

NewObject与对象加载的区别

从Package加载对象时,构造流程之后还会由序列化数据覆盖部分默认属性,并调用PostLoad();因此加载对象的最终状态不能只通过构造函数和CDO推导。

UWorld中生成Actor

在UWorld创建一个Actor的底层也是要调用NewObject(),只不过在UObject基础上增加了组件注册、BeginPlay()等Actor生命周期。 引擎源码概念流程如下:

AActor* UWorld::SpawnActor(
    UClass* Class, 
    FTransform const* UserTransformPtr, 
    const FActorSpawnParameters& SpawnParameters 
)
{
    // Engine/Private/LevelActor.cpp
    // 以下为简化流程,并非可直接编译的完整源码

    //调用NewObject创建对象
    AActor* const Actor = NewObject<AActor>(...);

    // ─────────────────────────────
    // ⑤ 注册到世界(但还没 BeginPlay)
    // ─────────────────────────────
    LevelToSpawnIn->TryAddActorToList( Actor, /*bAddUnique*/false);

    // ─────────────────────────────
    // ⑥ Actor 特有初始化
    // ─────────────────────────────
    Actor->PostSpawnInitialize()
    {
        Actor->RegisterAllComponents()//注册组件

        Actor->PostActorCreated()//调用生命周期函数

        if(不是Deferred Spawn)
        {
            Actor->FinishSpawning()//Spawn完毕
            {
                Actor->ExecuteConstruction(FinalRootComponentTransform,...)//设置Transform

                Actor->PostActorConstruction()//主要是初始化组件

                World->OnActorFinishedSpawning(this)//通知UWorld
            }
        }
    }
    return Actor;
}

Discussion

留言讨论

无需注册账号
正在加载留言区…

发表留言时,评论服务会记录必要的 IP 与 User-Agent 信息,用于安全审核和防止滥用。