UE4中的反射之二:運行階段
環境
引擎版本: github 4.20
簡介
在上一篇文章里主要是針對 UHT 生成的 .generated.h 來分析反射相關的代碼展開參與編譯的過程。這篇文件要通過 UHT 生成的另外一個文件 .gen.cpp 來分析運行過程中的反射信息的收集使用。使用的例子還是上篇文章中的 AMyActor
類
在上篇文章中,分析反射代碼的入口是 GENERATED_BODY
宏。本篇文章也一樣有個重點的入口,那就是 UHT 在 .gen.cpp 中生成的靜態全局變數。我們都知道在 C++ 中,全局變數的初始化是先於 main
函數進行的,所以就可以在這個全局變數的構造函數中進行反射信息的註冊,最終保存到 UClass
的實例中,供其它地方的代碼使用。
IMPLEMENT_CLASS()
在 .gen.cpp 的最後,有一行代碼 IMPLEMENT_CLASS(AMyActor, 1438139441);
這個宏有兩個參數,第一個是類名,第二個是類的 CRC 值(主要是用於熱載入的,可以先忽略),這個宏的定義如下
// Register a class at startup time.
#define IMPLEMENT_CLASS(TClass, TClassCrc)
static TClassCompiledInDefer<TClass> AutoInitialize##TClass(TEXT(#TClass), sizeof(TClass), TClassCrc);
UClass* TClass::GetPrivateStaticClass()
{
static UClass* PrivateStaticClass = NULL;
if (!PrivateStaticClass)
{
/* this could be handled with templates, but we want it external to avoid code bloat */
GetPrivateStaticClassBody(
StaticPackage(),
(TCHAR*)TEXT(#TClass) + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0),
PrivateStaticClass,
StaticRegisterNatives##TClass,
sizeof(TClass),
(EClassFlags)TClass::StaticClassFlags,
TClass::StaticClassCastFlags(),
TClass::StaticConfigName(),
(UClass::ClassConstructorType)InternalConstructor<TClass>,
(UClass::ClassVTableHelperCtorCallerType)InternalVTableHelperCtorCaller<TClass>,
&TClass::AddReferencedObjects,
&TClass::Super::StaticClass,
&TClass::WithinClass::StaticClass
);
}
return PrivateStaticClass;
}
這個宏里有兩個重點,第一個是定義了一個靜態全局變數 TClassCompiledInDefer<AMyActor> AutoInitializeAMyActor
。第二個是實現了 GetPrivateStaticClass
,這個函數在上一篇中也有提到,DECLARE_CLASS
宏的定義里聲明了 GetPrivateStaticClass
的原型,並且有一個 GetClass
靜態成員函數,調用了它。這個函數的實現就是在這裡定義了。
AMyActor::GetPrivateStaticClass
先來看看這個函數的實現(因為 AutoInitializeAMyActor
這個全局變數最終會調用到這裡),這個函數的作用就是構造一個 UClass
對象來保存 AMyActor
這個類的反射信息。這個函數里創建了一個局部靜態變數 PrivateStaticClass
,在首次調用時進行初始化,之後的調用都是返回首次調用的初始化結果,因為不管 AMyActor
有多少個實例,它們的反射信息都是一樣的。
初始化 PrivateStaticClass
是通過調用 GetPrivateStaticClassBody
進行的,這個函數的原型如下
COREUOBJECT_API void GetPrivateStaticClassBody(
const TCHAR* PackageName,
const TCHAR* Name,
UClass*& ReturnClass,
void(*RegisterNativeFunc)(),
uint32 InSize,
EClassFlags InClassFlags,
EClassCastFlags InClassCastFlags,
const TCHAR* InConfigName,
UClass::ClassConstructorType InClassConstructor,
UClass::ClassVTableHelperCtorCallerType InClassVTableHelperCtorCaller,
UClass::ClassAddReferencedObjectsType InClassAddReferencedObjects,
UClass::StaticClassFunctionType InSuperClassFn,
UClass::StaticClassFunctionType InWithinClassFn,
bool bIsDynamic = false);
幾個重要參數說明如下
PackageName
: 我們構造的UClass
對象應該放到哪個包里,調用的是 .generated.h 中DECLARE_CLASS
宏里定義的StaticPackage
函數Name
: 類名,這裡就是MyActor
,不含前綴。UE4 里的編碼規範規定了類需要一個前綴,比如A
代表Actor
,並且會將準備廢棄的類或者變數加上DEPRECATED
前綴或後綴,比如DEPRECATED_AOtherActor
,int32 Value_DEPRECATED
,所以這裡的實參需要將類名字元串加 1 去掉類的前綴,如果有廢棄的標記,還會再加上DEPRECATED_
這個字元串的長度,也就是 11,最終得到真實得類名ReturnClass
: 這是個輸出參數,傳的實參就是局部靜態變數PrivateStaticClass
RegisterNativeFunc
: 一個回調函數,用來向UClass
實例里註冊AMyActor
類里可被藍圖調用的exec
版本函數的信息,在這裡傳的實參是StaticRegisterNativesAMyActor
,這個函數定義在 UHT 生成的MyActor.gen.cpp
里,內如如下
void AMyActor::StaticRegisterNativesAMyActor()
{
UClass* Class = AMyActor::StaticClass();
static const FNameNativePtrPair Funcs[] = {
{ "MyActorBlueprintCallable", &AMyActor::execMyActorBlueprintCallable },
{ "MyActorBlueprintGetter", &AMyActor::execMyActorBlueprintGetter },
{ "MyActorBlueprintNativeEvent", &AMyActor::execMyActorBlueprintNativeEvent },
{ "MyActorBlueprintPure", &AMyActor::execMyActorBlueprintPure },
{ "MyActorBlueprintSetter", &AMyActor::execMyActorBlueprintSetter },
};
FNativeFunctionRegistrar::RegisterFunctions(Class, Funcs, ARRAY_COUNT(Funcs));
}
InSize
: 類大小,這裡實參傳的是sizeof(AMyActor)
InClassConstructor
: 類的默認構造函數,這裡實參傳的是一個模板函數,內部調用的就是AMyActor::__DefaultConstructor
,這個__DefaultConstructor
在上篇文章里也提到了,就是TestCall_Source_TestCall_MyActor_h_12_STANDARD_CONSTRUCTORS
或者TestCall_Source_TestCall_MyActor_h_12_ENHANCED_CONSTRUCTORS
這個宏里定義的,內部使用 placement new 調用了真正的構造函數,一個是帶參的,一個是不帶參的默認構造函數InClassAddReferencedObjects
: 這個是和 GC 相關的,這裡不展開說了InSuperClassFn
: 直接父類的StaticClass
函數指針
GetPrivateStaticClassBody
的內部實現很清晰,如下
- 調用
GUObjectAllocator.AllocateUObject
來給UClass
或者UDynamicClass
對象分配內存 - 調用
placement new
在前面分配出來的內存上調用UClass
或者UDynamicClass
的構造函數來初始化實例 - 調用
InitializePrivateStaticClass
就是將這個UClass
實例加入到GUObjectArray
並且設置為RootSet
,防止被 GC。(部分代碼沒看懂) - 調用
RegisterNativeFunc
,在這裡就是StaticRegisterNativesAMyActor
,向構造出來的的UClass
或者UDynamicClass
對象里註冊AMyActor
類里可被藍圖調用的exec
版本函數的信息
TClassCompiledInDefer
這個模板類的實現很簡單,但是作用很重要,在構造函數里進行類名和類大小的初始化,然後調用了 UClassCompiledInDefer
全局函數,這個函數里大部分代碼都是和熱載入相關的處理,可以先忽略,最重要的是最後一句
GetDeferredClassRegistration().Add(ClassInfo);
將 TClassCompiledInDefer<AMyActor>
這個全局變數加入到 DeferredClassRegistration
列表裡,在 TClassCompiledInDefer
這個模板類里,還有兩個虛函數可以重載,其中最重要的是 Register
這個函數,這個函數的實現是這樣的
virtual UClass* Register() const override
{
LLM_SCOPE(ELLMTag::UObject);
return TClass::StaticClass();
}
在 Register
里調用了 StaticClass
,所以從這裡就可以看出來,引擎會在啟動後的某個時機,遍歷 DeferredClassRegistration
數組,對每個對象調用 Register
函數,調用對應的 StaticClass
,再調用 GetPrivateStaticClass
,這就是這個函數被第一次調用的時候,然後構造一個 UClass
對象。所以在這個時候,我們得到了所有類對應的反射信息類對象,但是這些反射對象還沒有完全填充好所有的反射信息,只填好了可以被藍圖調用的 C++ exec
版本函數信息,其他信息需要接下來的那個全局變數負責填充
FCompiledInDefer Z_CompiledInDefer_UClass_AMyActor
上面分析完了 MyActor.gen.cpp
里的 IMPLEMENT_CLASS
宏,緊接著這行代碼下面一行,還定義了另外一個全局變數
static FCompiledInDefer Z_CompiledInDefer_UClass_AMyActor(Z_Construct_UClass_AMyActor, &AMyActor::StaticClass, TEXT("/Script/TestCall"), TEXT("AMyActor"), false, nullptr, nullptr, nullptr);
FCompiledInDefer
的實現如下
struct FCompiledInDefer
{
FCompiledInDefer(class UClass *(*InRegister)(), class UClass *(*InStaticClass)(), const TCHAR* PackageName, const TCHAR* Name, bool bDynamic, const TCHAR* DynamicPackageName = nullptr, const TCHAR* DynamicPathName = nullptr, void (*InInitSearchableValues)(TMap<FName, FName>&) = nullptr)
{
if (bDynamic)
{
GetConvertedDynamicPackageNameToTypeName().Add(FName(DynamicPackageName), FName(Name));
}
UObjectCompiledInDefer(InRegister, InStaticClass, Name, PackageName, bDynamic, DynamicPathName, InInitSearchableValues);
}
};
構造函數比較重要的是前兩個參數,第一個是一個回調函數指針,這裡實參傳的是 Z_Construct_UClass_AMyActor
,第二個是類的 StaticClass
函數指針,這裡實參傳的是 &AMyActor::StaticClass
,在構造函數里調用了另外一個全局函數 UObjectCompiledInDefer
,這個函數里做的事情如下,如果不是一個動態類的話那很簡單,直接將 InRegister
回調函數指針放入 DeferredCompiledInRegistration
數組中,供後面統一調用。如果是個動態類的話,就填充好 FDynamicClassStaticData
結構,然後放入 DynamicClassMap
數組中,在後面統一調用。
回過頭來看看 Z_Construct_UClass_AMyActor
這個回調里做的事情,其實就是調用 UE4CodeGen_Private::ConstructUClass
函數將 UHT 自動生成好的 UE4CodeGen_Private::FClassParams
結構體填充到 UClass
對象中,這個結構體中重要的有兩個
FunctionLinkArray
,這個數組裡填的是每一個被UFUNCTION
標記的函數對應的UFunction
創建函數回調和對應的函數名稱,在這裡傳的實參是
const FClassFunctionLinkInfo Z_Construct_UClass_AMyActor_Statics::FuncInfo[] = {
{ &Z_Construct_UFunction_AMyActor_MyActorBlueprintCallable, "MyActorBlueprintCallable" }, // 403128632
{ &Z_Construct_UFunction_AMyActor_MyActorBlueprintGetter, "MyActorBlueprintGetter" }, // 1207006867
{ &Z_Construct_UFunction_AMyActor_MyActorBlueprintImplementableEvent, "MyActorBlueprintImplementableEvent" }, // 2944526560
{ &Z_Construct_UFunction_AMyActor_MyActorBlueprintNativeEvent, "MyActorBlueprintNativeEvent" }, // 140128359
{ &Z_Construct_UFunction_AMyActor_MyActorBlueprintPure, "MyActorBlueprintPure" }, // 4186474183
{ &Z_Construct_UFunction_AMyActor_MyActorBlueprintSetter, "MyActorBlueprintSetter" }, // 3614515892
};
這些 Z_Construct_UFunction_AMyActor_*
函數被調用後,內部調用 UE4CodeGen_Private::ConstructUFunction
來創建一個 UFunction
對象並返回 * PropertyArray
,這個數組裡填的是所有的被 UPROPERTY
標記的屬性,每個屬性 UHT 都自動填好了類型,名稱,偏移等信息,如下
const UE4CodeGen_Private::FUnsizedIntPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_MyActorProperty = { UE4CodeGen_Private::EPropertyClass::Int, "MyActorProperty", RF_Public|RF_Transient|RF_MarkAsNative, (EPropertyFlags)0x0010000000000000, 1, nullptr, STRUCT_OFFSET(AMyActor, MyActorProperty), METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_MyActorProperty_MetaData, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_MyActorProperty_MetaData)) };
終上所述,運行時的反射信息收集簡單來說就一句話:MyActor.gen.cpp 里定義了兩個全局變數,一個負責構造 UClass
對象,另一個負責往這個構造好的對象中填充 AMyActor
類的反射信息
引擎運行時初始化流程
UE4 引擎有個最核心最底層的模塊,就是 CoreUObject
,在這個模塊的 StartupModule
中,調用了 UClassRegisterAllCompiledInClasses
全局函數,在這個函數里遍歷 DeferredClassRegistration
數組調用 Register
函數進行 UClass
的構造,然後清空 DeferredClassRegistration
數組
還有個地方會調用,就是在引擎的 FEngineLoop::PreInit
函數里,在所有的模塊都載入完成後,包括自己的遊戲模塊,依賴的插件等等,調用 ProcessNewlyLoadedUObjects
函數,在這個函數里調用 UClassRegisterAllCompiledInClasses
處理上一次調用到現在加入到 DeferredClassRegistration 中的對象。在 ProcessNewlyLoadedUObjects
還對類和結構體的反射信息進行填充,就是遍歷調用第二個全局變數註冊的回調函數,並且創建 CDO(Class Default Object) 對象,完整流程圖如下
推薦閱讀: