从源码深入理解Unreal中UObject的构造流程

前言

在c++中,实例化一个对象的流程非常简单,通过new实现分配内存+初始化内存(调用类的构造函数)即可
但在Unreal中,UObject的实例化实际上非常复杂,一方面是一些基于效率上的考虑做了很多操作,另一方面是为了与蓝图、编辑器界面、Config等功能结合添加了额外的属性初始化步骤,让整个UObject的实例化分为了很多步并且建立在大量的反射和宏上,并引入了Archetype和CDO等概念,本文将从源码来解释整个流程是怎样工作的。

案例

这个问题是我实际工作中遇到的一个奇怪的问题,也正是在解决这个问题的过程中引出了对本文的探索,所以我先把一个简化版的问题留在这里,在文章的最后我会给出答案
我有一个TestActor,它有一个组件TestComponentTestComponent上有一个Actor指针(TestOwner)需要初始化指向它,于是我在TestActor的构造函数中写了如下代码

AMyTestActor::AMyTestActor()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
	TestComponent = CreateDefaultSubobject<UTestComponent>("TestComp");
	TestComponent->TestOwner = this;
}

然后我创建了一个派生自TestActor的蓝图(BP_TestActor),并把他放到场景中。当游戏开始后,这个TestComponent的TestOwner是指向谁呢?是当前游戏场景中的TestActor吗?

NewObject

创建一个UObject通常是调用NewObject,那我们就从这里开始入手,NewObject内基本就是把参数重新做了下转发到StaticConstructObject_Internal中,其核心代码也非常简洁

UObject* StaticConstructObject_Internal(const FStaticConstructObjectParameters& Params)
{
	const UClass* InClass = Params.Class;
	UObject* InOuter = Params.Outer;
	const FName& InName = Params.Name;
	EObjectFlags InFlags = Params.SetFlags;
	UObject* InTemplate = Params.Template;

	// 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)))
		);

	// Do not recycle subobjects when performing hot-reload as they may contain old property values.
	const bool bCanRecycleSubobjects = bIsNativeFromCDO && (!(InFlags & RF_DefaultSubObject) || !FUObjectThreadContext::Get().IsInConstructor) && !IsReloadActive();

	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;
}

这里的bCanRecycleSubobjects从条件上看可能有点难懂是什么意思,实际上后面就会看到当你创建的是CDO而已经有一个旧的CDO或者你选择的ObjectName已经有了一个同名的UObject时,就会先销毁旧的UObject,这时它的Subobject是否要回收(直接用于新创建的这个UObject)就由这个值决定;从中不难看出如果能够回收(值为True)需要 这个类是一个原生类型(非蓝图类) 并且 如果创建的这个UObject是CDO需要这次创建不是在构造函数中调用并且HotReload没有开启
然后其实就像传统new一样分为两大部分,分配内存StaticAllocateObject和初始化变量调用ClassConstructor

StaticAllocateObject

第一步是查找是否有已经创建的同名对象,如果创建的是CDO的话那么名字会被强制改为CDO的默认名字

UObject* StaticAllocateObject(const UClass*	InClass,UObject* InOuter,FName InName,EObjectFlags InFlags,EInternalObjectFlags InternalSetFlags,bool bCanRecycleSubobjects,bool* bOutRecycledSubobject,UPackage* ExternalPackage)
{
	const bool bCreatingCDO = (InFlags & RF_ClassDefaultObject) != 0;
	const bool bCreatingArchetype = (InFlags & RF_ArchetypeObject) != 0;

	if (bCreatingCDO)
	{
		InName = InClass->GetDefaultObjectName();
		// never call PostLoad on class default objects
		InFlags &= ~(RF_NeedPostLoad|RF_NeedPostLoadSubobjects);
	}

	UObject* Obj = NULL;
	if(InName == NAME_None)
	{
		InName = MakeUniqueObjectName(InOuter, InClass);
	}
	else
	{
		// See if object already exists.
		Obj = StaticFindObjectFastInternal( /*Class=*/ NULL, InOuter, InName, true );
	}

根据查找到的对象,如果没有已经创建的对象的话则根据反射分配对应大小的内存,否则尝试销毁旧对象并直接使用旧对象的内存。这里会用到之前的bCanRecycleSubobjects来处理旧对象的回收,这里的源码我省去了一些,但实际上可以看到,所谓的回收子对象实际上是完全重用了旧对象的内存地址,没有做任何操作,所以这个回收其实就是回收利用了整个旧对象而不只是Subobject

	// True when the object to be allocated already exists and is a subobject.
	bool bSubObject = false;
	int32 TotalSize = InClass->GetPropertiesSize();

	if( Obj == nullptr )
	{	
		int32 Alignment	= FMath::Max( 4, InClass->GetMinAlignment() );
		Obj = (UObject *)GUObjectAllocator.AllocateUObject(TotalSize,Alignment,GIsInitialLoad);
	}
	else
	{
		// Subobjects are always created in the constructor, no need to re-create them here unless their archetype != CDO or they're blueprint generated.	
		if (!bCreatingCDO && (!bCanRecycleSubobjects || !Obj->IsDefaultSubobject()))
		{
			OldIndex = GUObjectArray.ObjectToIndex(Obj);
			OldSerialNumber = GUObjectArray.GetSerialNumber(OldIndex);
			// Check that the object hasn't been destroyed yet.
			if(!Obj->HasAnyFlags(RF_FinishDestroyed))
			{
				// Get the name before we start the destroy, as destroy renames it
				FString OldName = Obj->GetFullName();

				// Begin the asynchronous object cleanup.
				Obj->ConditionalBeginDestroy();
				// Finish destroying the object.
				Obj->ConditionalFinishDestroy();
			}
			GUObjectArray.LockInternalArray();
			TGuardValue<bool> _(GUObjectArray.bShouldRecycleObjectIndices, false);
			Obj->~UObject();
			GUObjectArray.UnlockInternalArray();
			bWasConstructedOnOldObject	= true;
		}
		else
		{
			bSubObject = true;
		}
	}

如果没有使用旧对象的subobject或者没有旧对象的话(bSubobject为false),则清零内存区域,并在上面调用UObjectBase构造函数重新进行初始化(相当于完全重新创建一个UObject)
最后返回这个分配好的地址

	if (!bSubObject)
	{
		FMemory::Memzero((void *)Obj, TotalSize);
		new ((void *)Obj) UObjectBase(const_cast<UClass*>(InClass), InFlags|RF_NeedInitialization, InternalSetFlags, InOuter, InName, OldIndex, OldSerialNumber);
	}
	else
	{
		// Propagate flags to subobjects created in the native constructor.
		Obj->SetFlags(InFlags);
		Obj->SetInternalFlags(InternalSetFlags);
	}
	
	return Obj;
}

ClassConstructor

if (!bRecycledSubobject)
{		
	(*InClass->ClassConstructor)(FObjectInitializer(Result, Params));
}

只有在bRecycledSubobject为nullptr时才会调用构造函数,这样就加速了重复对象的创建。这里的初始化写法有点奇怪,但实际上就是调用了你声明的构造函数,具体是这样做的
首先利用UClass储存的反射信息找到构造函数指针ClassConstructor,在上面调用Placement new初始化,并传入FObjectInitializer
而通过调试可以发现这个构造函数指针指向的其实是一个泛型函数

template<class T>
void InternalConstructor( const FObjectInitializer& X )
{ 
	T::__DefaultConstructor(X);
}

他会调用对应类型的**__DefaultConstructor**,这里很好猜到这个函数肯定是通过UHT生成的宏创建的函数,我们随便打开一个自己的类型的generated头文件,可以看到以下宏定义

#define FID_UnrealProjects_TestProject_Source_TestProject_Public_MyTestActor_h_14_STANDARD_CONSTRUCTORS \
	/** Standard constructor, called after all reflected properties have been initialized */ \
	NO_API AMyTestActor(const FObjectInitializer& ObjectInitializer); \
	DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(AMyTestActor) \
	DECLARE_VTABLE_PTR_HELPER_CTOR(NO_API, AMyTestActor); \
	DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER(AMyTestActor); \
private: \
	/** Private move- and copy-constructors, should never be used */ \
	NO_API AMyTestActor(AMyTestActor&&); \
	NO_API AMyTestActor(const AMyTestActor&); \
public: \
	NO_API virtual ~AMyTestActor();

这里的DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL就是定义对应构造函数的宏,这个宏的定义就是通过ObjectInitializer拿到需要初始化的对象指针(GetObj),然后在上面调用对应类型的构造函数做初始化

#define DEFINE_DEFAULT_CONSTRUCTOR_CALL(TClass) \
	static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())TClass; }

#define DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(TClass) \
	static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())TClass(X); }

FObjectInitializer

所以这就没了?搞了半天初始化还是只调用了我定义的构造函数,只不过搞了点反射和宏故弄玄虚?
其实那些额外的属性(UPROPERTY)初始化操作包括CDO拷贝和读Config等都在FObjectInitializer中,没有发现调用的痕迹是因为这部分代码藏在了FObjectInitializer的析构函数中。这里的设计真的很巧妙的,当我们自己的构造函数调用结束后这个对象的创建也就结束了,对应的ObjectInitializer会被弹出创建栈(FUObjectThreadContext::InitializerStack),然后它的析构函数就会被调用,巧妙的保证了属性初始化紧接在对象创建并构造完成后

FObjectInitializer::~FObjectInitializer()
{
	//...
	if (!bIsPostConstructInitDeferred)
	{
		PostConstructInit();
	}
}

在PostConstructInit中基本可以分为以下几个部分
初始化自己的属性(InitProperties)
通过CDO对象拷贝初始化自己的属性,InitProperties中的内容就是通过反射遍历自己的UPROPERTY然后拷贝CDO中的值

if (bShouldInitializePropsFromArchetype)
{
	UClass* BaseClass = (bIsCDO && !GIsDuplicatingClassForReinstancing) ? SuperClass : Class;
	if (BaseClass == NULL)
	{
		check(Class==UObject::StaticClass());
		BaseClass = Class;
	}

	UObject* Defaults = ObjectArchetype ? ObjectArchetype : BaseClass->GetDefaultObject(false); // we don't create the CDO here if it doesn't already exist
	InitProperties(Obj, BaseClass, Defaults, bCopyTransientsFromClassDefaults);
}

初始化Subobject的属性(InitSubobjectProperties)
对所有Subobject进行CDO拷贝属性初始化,与上一步的原理相同

bool bNeedSubobjectInstancing = InitSubobjectProperties(bAllowInstancing);

bool FObjectInitializer::InitSubobjectProperties(bool bAllowInstancing) const
{
#if USE_CIRCULAR_DEPENDENCY_LOAD_DEFERRING
	bool bNeedSubobjectInstancing = bAllowInstancing && bIsDeferredInitializer;
#else 
	bool bNeedSubobjectInstancing = false;
#endif
	// initialize any subobjects, now that the constructors have run
	for (int32 Index = 0; Index < ComponentInits.SubobjectInits.Num(); Index++)
	{
		UObject* Subobject = ComponentInits.SubobjectInits[Index].Subobject;
		UObject* Template = ComponentInits.SubobjectInits[Index].Template;
		InitProperties(Subobject, Template->GetClass(), Template, false);
		if (bAllowInstancing && !Subobject->HasAnyFlags(RF_NeedLoad))
		{
			bNeedSubobjectInstancing = true;
		}
	}

	return bNeedSubobjectInstancing;
}

读取Config中的值初始化变量
只有当对象是CDO或者该类型配置了PerObjectConfig才进行这一步

	if (!Obj->HasAnyFlags(RF_NeedLoad) || bIsDeferredInitializer)
	{
		if (bIsCDO || Class->HasAnyClassFlags(CLASS_PerObjectConfig))
		{
			Obj->LoadConfig(NULL, NULL, bIsCDO ? UE::LCPF_ReadParentSections : UE::LCPF_None);
		}
	}

实例化Subobject
这里和前面一样,其实不只是Subobject,也包括自己,不过这个实例化暂时不知道具体是干啥的,它通过反射对每个属性调用FProperty::InstanceSubobjects

if (bNeedInstancing || bNeedSubobjectInstancing)
{
	InstanceSubobjects(Class, bNeedInstancing, bNeedSubobjectInstancing);
}

void FObjectInitializer::InstanceSubobjects(UClass* Class, bool bNeedInstancing, bool bNeedSubobjectInstancing) const
{
	if (bNeedInstancing)
	{
		UObject* Archetype = ObjectArchetype ? ObjectArchetype : Obj->GetArchetype();
		Class->InstanceSubobjectTemplates(Obj, Archetype, Archetype ? Archetype->GetClass() : NULL, Obj, UseInstancingGraph);
	}
	if (bNeedSubobjectInstancing)
	{
		// initialize any subobjects, now that the constructors have run
		for (int32 Index = 0; Index < ComponentInits.SubobjectInits.Num(); Index++)
		{
			UObject* Subobject = ComponentInits.SubobjectInits[Index].Subobject;
			UObject* Template = ComponentInits.SubobjectInits[Index].Template;

#if USE_CIRCULAR_DEPENDENCY_LOAD_DEFERRING
			if ( !Subobject->HasAnyFlags(RF_NeedLoad) || bIsDeferredInitializer )
#else 
			if ( !Subobject->HasAnyFlags(RF_NeedLoad) )
#endif
			{
				Subobject->GetClass()->InstanceSubobjectTemplates(Subobject, Template, Template->GetClass(), Subobject, UseInstancingGraph);
			}
		}
	}
}

总结

在Unreal中创建一个UObject可以大致分为三步,前两步和传统的c++一样分别为分配内存和构造初始化,分配内存部分会判断是否已有旧对象并对可能会对旧对象做回收操作,构造初始化本质上就是直接调用我们写的构造函数只不过多了几步通过模板和宏的转发。第三步是Unreal独有的属性初始化,会通过CDO和Config等来初始化新建对象的UPROPERTY属性。
这里其实还有很多内容,包括这些过程中的很多判断我都没有分析是为什么,以及一大堆的Flag,此外还有Archetype与CDO有什么不同?ObjectInitializer也还有很多可以研究的地方。之后会抽时间再补充一下

解决问题

是时候解决我们开头时提出的问题了,先揭晓答案吧,用Default_X表示X的CDO对象TestOwner实际上指向的是Default_BP_TestActor
在游戏开始后,BP_TestActor的构造函数先被调用,正确创建了TestComponent并将TestOwner设置为了自己。但构造函数结束后在属性初始化阶段,TestComponent作为Subobject,在InitSubobjectProperties中拷贝了自己的CDO对象(Default_BP_TestActor中的TestComponent)中的属性值,而在Default_TestComponent中TestOwner显然是指向Default_BP_TestActor。
想要避免这个问题也很简单,那就是遵循Unreal推荐的编码方式,在Actor的PostInitializeComponent中设置Component的属性值

此外,还有一些我们经常使用的功能也可以通过刚才分析的流程去解析,比如我们经常在蓝图的Detail面板中编辑一些属性值,当我们修改这些属性值时,实际上就是在修改CDO的属性值,通过CDO的属性初始化拷贝到每一个实例中;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值