ue4 NewObject/StaticConstructObject_Internal/StaticAllocateObject/FObjectInitializer:对象创建和初始化
UObject是一套很复杂的体系,之前读ue3代码时曾分析过其类型系统实现,主要是与UClass间的关系
现在转到ue4,发现那一块其实差不多,于是再重点备忘一下UObject本身的创建和初始化过程
1、首先,用NewObject<>来创建一个新对象:
template< class T > T* NewObject(UObject* Outer, UClass* Class, FName Name = NAME_None, EObjectFlags Flags = RF_NoFlags, UObject* Template = nullptr, bool bCopyTransientsFromClassDefaults = false, FObjectInstancingGraph* InInstanceGraph = nullptr) { if (Name == NAME_None) { FObjectInitializer::AssertIfInConstructor(Outer, TEXT("NewObject with empty name can't be used to create default subobjects (inside of UObject derived class constructor) as it produces inconsistent object names. Use ObjectInitializer.CreateDefaultSuobject<> instead.")); } #if DO_CHECK // Class was specified explicitly, so needs to be validated CheckIsClassChildOf_Internal(T::StaticClass(), Class); #endif return static_cast<T*>(StaticConstructObject_Internal(Class, Outer, Name, Flags, EInternalObjectFlags::None, Template, bCopyTransientsFromClassDefaults, InInstanceGraph)); }
除了一些条件检测,直接调了以前老版的对应物StaticConstructObject_Internal。
这里的几个参数,有意思的是第5个参数Template,通过它可以复制对象。
2、StaticConstructObject_Internal,这里面主要做两件事:
一是用StaticAllocateObject来分配空间,
二是在此空间上初始化对象。
3、StaticAllocateObject:
首先是检查要创建新对象,还是替换一个原有对象,因为一路传进来的有个Name参数。如果找到一个已有的同名对象,那么会强制那个对象析构,然后重用它的空间,否则的话,就会直接分配空间了:
UObjectBase* FUObjectAllocator::AllocateUObject(int32 Size, int32 Alignment, bool bAllowPermanent)
除了这件主要的工作,还有不少其它的事,比如管理对象之间的连接、在异步线程中创建对象时的通知等等,暂时不深究了。
4、有了前者分配的空间,那么就可以在此处构造对象了:
Result = StaticAllocateObject(InClass, InOuter, InName, InFlags, InternalSetFlags, bCanRecycleSubobjects, &bRecycledSubobject);
(*InClass->ClassConstructor)( FObjectInitializer(Result, InTemplate, bCopyTransientsFromClassDefaults, true, InInstanceGraph) );
这里ClassConstructor是每个UClass都有的一个函数指针类成员变量,实际上所有的UClass里该指针都指向一个全局模板函数:
template<class T> void InternalConstructor( const FObjectInitializer& X ) { T::__DefaultConstructor(X); }
而每个类里的__DefaultConstructor也是用宏统一生成的,内容也是简单的转发给new:
static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())TClass(X); }
这个new的形式很不平凡,既有传给operator new的【(EInternal*)X.GetObj()】,又有传给该类实际构造函数的【FObjectInitializer& X】
后者正是InClass->ClassConstructor需要的实参,而前者也是通过宏(DECLARE_CLASS)定义在每个UObject里的一个operator new:
inline void* operator new( const size_t InSize, EInternal* InMem ) \ { \ return (void*)InMem; \ }
总结一下上面绕来绕去的东西就是:
StaticAllocateObject分配了内存空间Result,然后以此为参数(当然还有其它的参数)构造了一个FObjectInitializer X,其中X.GetObj返回的就是这个内存地址Result;
接着用【 new(Result) TClass(X) 】来构造对象,指明地址在Result上,并将X做为参数传给其构造函数。
5、关于FObjectInitializer
接上步,在构造函数返回后,其临时参数【FObjectInitializer& X】是要自动析构的,因而ue4利用此步骤,在这里面做了很多事,大部份的事都是与该体系内部状态管理有关的,暂时难以透彻理解。
但有一个的操作与应用相关,即属性初始化,这是通过InitProperties来完成的:
void FObjectInitializer::InitProperties(UObject* Obj, UClass* DefaultsClass, UObject* DefaultData, bool bCopyTransientsFromClassDefaults) { …… UClass* Class = Obj->GetClass(); ……if (!bNeedInitialize && bCanUsePostConstructLink) { // This is just a fast path for the below in the common case that we are not doing a duplicate or initializing a CDO and this is all native. // We only do it if the DefaultData object is NOT a CDO of the object that's being initialized. CDO data is already initialized in the // object's constructor. if (DefaultData) { if (Class->GetDefaultObject(false) != DefaultData) { QUICK_SCOPE_CYCLE_COUNTER(STAT_InitProperties_FromTemplate); for (UProperty* P = Class->PropertyLink; P; P = P->PropertyLinkNext) { P->CopyCompleteValue_InContainer(Obj, DefaultData); } } else { QUICK_SCOPE_CYCLE_COUNTER(STAT_InitProperties_ConfigEtcOnly); // Copy all properties that require additional initialization (e.g. CPF_Config). for (UProperty* P = Class->PostConstructLink; P; P = P->PostConstructLinkNext) { P->CopyCompleteValue_InContainer(Obj, DefaultData); } } }
……
在这里通过遍历UClass上记录的属性元数据,可以对当前实例的每个属性进行赋值。
有趣的是DefaultData这个参数,也就是最早的Template参数,一路辗转到此。当然如果Template为空的话,这里的DefaultData就是该类的CDO了。
代码里明显体现出对此两种情况的不同处理策略:
如果是指定了要复制的对象Template->DefaultData,那么要遍历类上的所有的属性,因为对于一个实际的复制目标,你不知道它哪些属性已经改变了(不是默认值 ),因此必须全盘复制
而如果只是从CDO复制的话,那么只需要处理该类里明确指定过可能有初始化状态的字段,如打上CPF_Config标记的字段,它们在启动时会去ini文件里提取相应的配置值。