转自:
https://blog.csdn.net/mohuak/article/details/81913532
https://blog.csdn.net/u012999985/article/details/52902065
1. UE4反射系统
什么是反射系统
在UE4里面,你无时无刻都会看到类似UFUNCTION()这样的宏。官方文档告诉你,只要在一个函数的前面加上这个宏,然后在括号里面加上BlueprintCallable就可以在编辑器里面调用了。按照他的指示,我们就能让我们的函数实现各种各样特别的功能,那这个效果就是通过UE4的反射系统来实现的。
其实,所谓反射,是程序在运行时进行自检的一种能力,自检什么呢?我认为就是检查自己的C++类,函数,成员变量,结构体等等(对应起来也就是大家在UE4能看到的UCLASS,UFUNCTON,UPROPERTY,USTRUCT后面还会提到)。
那检查这些东西做什么呢?最明显的就是支持蓝图和C++的交互功能,说的更通俗一点,就是可以更自由的控制这些结构,让他在我们想出现的地方出现,让他在我们想使用的地方使用。要知道我们在虚幻4中声明的任意一个类,都是继承于UObject类的,所以他远远不是我们所以为的那个普通的C++类。我们可以使用这个类进行网络复制,执行垃圾回收,让他和蓝图交互等等。而这一切原生的C++是并不支持的,也正是因此虚幻4才构建了一个这样的反射系统。
2. 反射实现机制和基本原理
在了解反射系统前,我们必须要知道两个UE4独特的文件类型—“.generate.h”以及“.generate.cpp”。“.generate.h”文件在每一个类的声明前面都会被包含到对应的头文件里面。(这也是官方建议我们要用编辑器来创建类的原因,他们并不是常规的C++类)而“.generate.cpp”对于一整个项目只会有一个。这两种文件可以说是反射系统的关键所在,他们是通过Unreal Build Tool(UBT) 和UnrealHeaderTool(UHT)来生成的。
2.1 UBT 和UHT
UnrealBuildTool(UBT,C#):UE4的自定义工具,来编译UE4的逐个模块并处理依赖等。我们编写的Target.cs,Build.cs都是为这个工具服务的。
UnrealHeaderTool (UHT,C++):UE4的C++代码解析生成工具,我们在代码里写的那些宏UCLASS等和#include “*.generated.h”都为UHT提供了信息来生成相应的C++反射代码。
代码编译在两个阶段中进行:1.UHT 被调用。它将解析 C++ 头中引擎相关类元数据,并生成自定义代码,以实现诸多 UObject 相关的功能。2.普通 C++ 编译器被调用,以便对结果进行编译。)
思考:UHT如何实现反射?
打个比方:UClass其实就好比一张表,一张户口本的东西,指向”真实家庭“的指针。上面记录着一些信息,好比:
张三:
男
1995年出生
李四:
女
1991年出生
那虚幻引擎是如何实现这个机制的呢?一种方法是,一开始编译的时候,把表格都填好,放到一个文件处,要找某家人的时候再取出来,但是有一个问题就是,每编译一次的时候,函数地址会发生变化,所以直接存储函数地址这种方法不行。第二种方法就是,存储”进行查找户口调查的过程”,比方说,它存储了每个家庭哪些人需要登记信息。然后当运行开始的时候,逐个让每一家人进行登记。而UHT就是在为这个过程提供帮助,它生成的.generate.h 和 .generate.cpp就是存储进行查找户口调查的过程。
2.2 .generate.h 和 .generate.cpp
“.generate.h”与“.generate.cpp”文件里面都是什么?有什么作用?
“.generate.h”里面是宏,而且包含一个非常庞大的宏,这个宏把所有和反射相关的方法(包括定义)和结构体连接到一起。
而“.generate.cpp”里面是许多的函数定义,UHT根据你在头文件里面使用的宏(UFUNCTION等)自动的生成这个文件,所以这个文件并不需要你去修改,也不允许修改。UBT属性通过扫描头文件,记录任何至少有一个反射类型的头文件的模块。如果其中任意一个头文件从上一次编译起发生了变化,那么 UHT就会被调用来利用和更新反射数据。UHT分析头文件,创建一系列反射数据,并且生成包含反射数据的C++代码。
2.3 反射类型和层次结构
官方文档所给出的基本层次结构
UField
UStruct
UClass (C++ class)
UScriptStruct (C++ struct)
UFunction (C++ function)
UEnum (C++ enumeration)
UProperty (C++ member variable or function parameter)
(Many subclasses for different types)
下图引自InsideUE4
2.4 热重载
在编辑器模式下,UE4将工程代码编译成动态链接库,这样编辑器可以动态的加载和卸载某个动态链接库。UE4为工程自动生成一个cpp文件(本工程为HelloUE4.generated.cpp),cpp文件包含了当前工程中所有需要反射的类信息,以及类成员列表和每个成员的类型信息。在动态链接库被编辑器加载的时候,自动将类信息注册到编辑器中。反之,卸载的时候,这样类信息也会被反注册。
在开发的过程中,当我们编译完成工程的时候,UE4编辑器会自动检测动态链接库的变化,然后自动热重载这些动态链接库中的类信息。
3. 反射代码实例分析
UE4引擎启动时候,是分Module(dll)来构建类型信息的。Module采用模拟一般程序的构建流程的方法,大致需要以下几个阶段:生成、收集、注册、链接。
生成阶段:借助UHT(Unreal Header Tool)工具,生成UClass代码,包括UClass构造,注册属性和函数等;
收集阶段:利用Static自动注册方式,在模块加载的时候,将所有UClass登记,集中在Array管理
注册阶段:在模块初始化的时候,将Array中的所有UClass相关的Function和Property注册;
链接阶段:就是反射功能。
生成阶段
要让一个类支持反射,你需要让这个类要继承自UObject、在类声明前添加UCLASS(或USTRUCT)标识,并且include “xxx.generated.h”头文件(而且必须是最后一个include)。
当你启动UE4编译时,UE4会首先运行UHT (UnrealHeaderTool),UHT成功运行后才会执行真正的编译。UHT是一个头文件解析和代码生成工具,它会处理所有的头文件,从中检索UCLASS、GENERATED_BODY、UPROPERTY、UFUNTION等关键字,检索到以后就为它们生成.generate.h 和 .generate.cpp。
以下是创建的一个小小的demo来对反射进行更好的理解
创建一个新的项目,然后创建一个类继承AGamoModeBase。
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "HelloGameMode.generated.h" // 核心内容,必须放在最后一行,由UBT自动生成。
UCLASS()
class HELLOWORLD_API AHelloGameMode : public AGameModeBase
{
GENERATED_BODY() // 重中之重
protected:
UPROPERTY(BlueprintReadWrite, Category = "AReflectionStudyGameMode")
float Score;
UFUNCTION(BlueprintCallable, Category = "AReflectionStudyGameMode")
void CallableFuncTest();
UFUNCTION(BlueprintNativeEvent, Category = "AReflectionStudyGameMode")
void NavtiveFuncTest();
UFUNCTION(BlueprintImplementableEvent, Category = "AReflectionStudyGameMode")
void ImplementableFuncTest();
};
首先UHT帮我自动生成了四个文件。
HelloGameMode.generated.h
UHT分析生成的文件内容如下:由于篇幅原因,只列出一部分代码。
// ...
// ...
#define helloworld_Source_helloworld_Public_HelloGameMode_h_15_RPC_WRAPPERS_NO_PURE_DECLS \
virtual void NavtiveFuncTest_Implementation(); \
\
DECLARE_FUNCTION(execNavtiveFuncTest) \
{ \
P_FINISH; \
P_NATIVE_BEGIN; \
P_THIS->NavtiveFuncTest_Implementation(); \
P_NATIVE_END; \
} \
\
DECLARE_FUNCTION(execCallableFuncTest) \
{ \
P_FINISH; \
P_NATIVE_BEGIN; \
P_THIS->CallableFuncTest(); \
P_NATIVE_END; \
}
#define helloworld_Source_helloworld_Public_HelloGameMode_h_15_EVENT_PARMS
#define helloworld_Source_helloworld_Public_HelloGameMode_h_15_CALLBACK_WRAPPERS
#define helloworld_Source_helloworld_Public_HelloGameMode_h_15_INCLASS_NO_PURE_DECLS \
private: \
static void StaticRegisterNativesAHelloGameMode(); \
friend struct Z_Construct_UClass_AHelloGameMode_Statics; \
public: \
DECLARE_CLASS(AHelloGameMode, AGameModeBase, COMPILED_IN_FLAGS(0 | CLASS_Transient), CASTCLASS_None, TEXT("/Script/helloworld"), NO_API) \
DECLARE_SERIALIZER(AHelloGameMode)
#define helloworld_Source_helloworld_Public_HelloGameMode_h_15_ENHANCED_CONSTRUCTORS \
/** Standard constructor, called after all reflected properties have been initialized */ \
NO_API AHelloGameMode(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()) : Super(ObjectInitializer) { }; \
private: \
/** Private move- and copy-constructors, should never be used */ \
NO_API AHelloGameMode(AHelloGameMode&&); \
NO_API AHelloGameMode(const AHelloGameMode&); \
public: \
DECLARE_VTABLE_PTR_HELPER_CTOR(NO_API, AHelloGameMode); \
DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER(AHelloGameMode); \
DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(AHelloGameMode)
#define helloworld_Source_helloworld_Public_HelloGameMode_h_15_PRIVATE_PROPERTY_OFFSET \
FORCEINLINE static uint32 __PPO__Score() { return STRUCT_OFFSET(AHelloGameMode, Score); }
#define helloworld_Source_helloworld_Public_HelloGameMode_h_15_GENERATED_BODY \
PRAGMA_DISABLE_DEPRECATION_WARNINGS \
public: \
helloworld_Source_helloworld_Public_HelloGameMode_h_15_PRIVATE_PROPERTY_OFFSET \ // 关于UPROPERTY部分,具体还没研究透,看代码像是获取指针地址的偏移值
helloworld_Source_helloworld_Public_HelloGameMode_h_15_RPC_WRAPPERS_NO_PURE_DECLS \ //
helloworld_Source_helloworld_Public_HelloGameMode_h_15_CALLBACK_WRAPPERS \ // 空宏
helloworld_Source_helloworld_Public_HelloGameMode_h_15_INCLASS_NO_PURE_DECLS \
helloworld_Source_helloworld_Public_HelloGameMode_h_15_ENHANCED_CONSTRUCTORS \
private: \
PRAGMA_ENABLE_DEPRECATION_WARNINGS
#undef CURRENT_FILE_ID
#define CURRENT_FILE_ID helloworld_Source_helloworld_Public_HelloGameMode_h
PRAGMA_ENABLE_DEPRECATION_WARNINGS
GENERATED_BODY
我们在HelloGameMode.cpp中发现,有一个GENERATED_BODY() 宏,该宏是重中之重,其他的UCLASS宏只是提供信息,不参与编译,而GENERATED_BODY正是把声明和元数据定义关联到一起的枢纽。继续查看宏定义。 GENERATED_BODY()的宏定义 :
#define BODY_MACRO_COMBINE_INNER(A,B,C,D) A##B##C##D
#define BODY_MACRO_COMBINE(A,B,C,D) BODY_MACRO_COMBINE_INNER(A,B,C,D)
#define GENERATED_BODY(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_GENERATED_BODY)
而在。generated.h Line63中定义了CURRENT_FILE_ID。所以GENERATED_BODY() 展开就是helloworld_Source_helloworld_Public_HelloGameMode_h_15_GENERATED_BODY,而这个宏的定义则在generated.h Line51。
DECLARE_FUNCTION
通过helloworld_Source_helloworld_Public_HelloGameMode_h_15_GENERATED_BODY向上, 以helloworld_Source_helloworld_Public_HelloGameMode_h_15_RPC_WRAPPERS_NO_PURE_DECLS为例:
#define helloworld_Source_helloworld_Public_HelloGameMode_h_15_INCLASS_NO_PURE_DECLS \
private: \
static void StaticRegisterNativesAHelloGameMode(); \ // 在HelloGameMode.gen.cpp中实现
friend struct Z_Construct_UClass_AHelloGameMode_Statics; \ // 在HelloGameMode.gen.cpp中定义
public: \
DECLARE_CLASS(AHelloGameMode, AGameModeBase, COMPILED_IN_FLAGS(0 | CLASS_Transient), CASTCLASS_None, TEXT("/Script/helloworld"), NO_API) \
DECLARE_SERIALIZER(AHelloGameMode) // 序列化,先忽略
DECLARE_CLASS是最重要的一个声明,对照着定义:DECLARE_CLASS(AHelloGameMode, AGameModeBase, COMPILED_IN_FLAGS(0 | CLASS_Transient), CASTCLASS_None, TEXT("/Script/helloworld"), NO_API)
TClass:类名
TSuperClass:基类名字
TStaticFlags:类的属性标记
TStaticCastFlags:指定了该类可以转换为哪些类,这里为0表示不能转为那些默认的类,读者可以自己查看EClassCastFlags声明来查看具体有哪些默认类转换。
TPackage:类所处于的包名,所有的对象都必须处于一个包中,而每个包都具有一个名字,可以通过该名字来查找。这里是”/Script/helloworld”,指定是Script下的helloworld,Script可以理解为用户自己的实现,不管是C++还是蓝图,都可以看作是引擎外的一种脚本,当然用这个名字也肯定有UE3时代UnrealScript的影子。Hello就是项目名字,该项目下定义的对象处于该包中。Package的概念涉及到后续Object的组织方式,目前可以简单理解为一个大的Object包含了其他子Object。
TRequiredAPI:就是用来Dll导入导出的标记,这里是NO_API1,因为是最终exe,不需要导出。
DefaultConstructor默认构造
helloworld_Source_helloworld_Public_HelloGameMode_h_15_ENHANCED_CONSTRUCTORS 部分:
#define helloworld_Source_helloworld_Public_HelloGameMode_h_15_ENHANCED_CONSTRUCTORS \
/** Standard constructor, called after all reflected properties have been initialized */ \
NO_API AHelloGameMode(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()) : Super(ObjectInitializer) { }; \ //根据注释的意思,就是标准的构造函数,在所有反射的属性初始化后调用。
private: \
/** Private move- and copy-constructors, should never be used */ \ //永远不要用这两个函数
NO_API AHelloGameMode(AHelloGameMode&&); \ //移动构造函数
NO_API AHelloGameMode(const AHelloGameMode&); \ //拷贝构造函数
public: \
DECLARE_VTABLE_PTR_HELPER_CTOR(NO_API, AHelloGameMode); \ // 热加载,先忽略
DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER(AHelloGameMode); \ // 空宏
DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(AHelloGameMode) // 定义一个构造函数
继续查看DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL的定义:
#define DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(TClass) \
static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())TClass(X); }
声明定义了一个构造函数包装器。需要这么做的原因是,在根据名字反射创建对象的时候,需要调用该类的构造函数。
可是类的构造函数并不能用函数指针指向,因此这里就用一个static函数包装一下,变成一个”平凡”的函数指针
而且所有类的签名一致,就可以在UClass里用一个函数指针里保存起来 。
HelloGameMode.gen.cpp
由于这个类代码太长,所以的话就分版块讨论。
ProcessEvent
static FName NAME_AHelloGameMode_ImplementableFuncTest = FName(TEXT("ImplementableFuncTest"));
void AHelloGameMode::ImplementableFuncTest()
{
ProcessEvent(FindFunctionChecked(NAME_AHelloGameMode_ImplementableFuncTest),NULL);
}
为啥BlueprintImplementableEvent的函数不用我们去实现呢,是因为UBT帮我们自己实现了。
而关于ProcessEvent部分, 这个方法在UObject中实现的。
刚接触UE4的时候,如果是BlueprintImplementabeEvent的函数,是不是发现不需要自己去实现,那么当时有没有觉得怪异呢,上面的代码就解释清楚了,那是UE4帮我们实现了,可以看到它调用了ProcessEvent方法,这个方法在UObject中实现的。
而且如果是BlueprintImplementabeEvent或者RPC的那些函数,我们是不需要实现其函数方法的,如果在CPP文件定义实现的话,则会报错,那是因为在.gen.cpp中已经帮我们实现了这个函数。
收集阶段
自动化注册
思考:UE4如何实现自动注册的呢?
主要是通过Static 自动注册的方式。
而在本例子是如何通过Static自动注册呢?
回顾生成阶段的IMPLEMENT_CLASS和ConstructClass中静态声明了两个变量。
#define IMPLEMENT_CLASS(TClass, TClassCrc) \
static TClassCompiledInDefer<TClass> AutoInitialize##TClass(TEXT(#TClass), sizeof(TClass), TClassCrc); \
或者
static FCompiledInDefer Z_CompiledInDefer_UClass_AHelloGameMode(Z_Construct_UClass_AHelloGameMode, &AHelloGameMode::StaticClass, TEXT("/Script/helloworld"), TEXT("AHelloGameMode"), false, nullptr, nullptr, nullptr);
这两个全局静态变量在Main函数之前就会初始化
初始化就会调用这两个变量的构造函数
构造函数会分别调用UClassCompiledInDefer函数和UObjectCompiledInDefer函数。
这两个函数会把ClassInfo放进延迟注册的数组中去。
思考:为何需要TClassCompiledInDefer和FCompiledInDefer两个静态初始化来登记?
我们也观察到了这二者是一一对应的,问题是为何需要两个静态对象来分别收集,为何不合二为一?关键在于我们首先要明白它们二者的不同之处,前者的目的主要是为后续提供一个TClass::StaticClass的Register方法(其会触发GetPrivateStaticClassBody的调用,进而创建出UClass对象),而后者的目的是在其UClass身上继续调用构造函数,初始化属性和函数等一些注册操作。我们可以简单理解为就像是C++中new对象的两个步骤,首先分配内存,继而在该内存上构造对象。我们在后续的注册章节里还会继续讨论到这个问题。
思考:为啥要采用延迟注册的方式?为什么不在收集阶段直接注册呢?
主要考虑到UE4超多类,都在static初始化阶段注册,程序会表现启动速度慢,用户双击程序,没反应。所以采用延迟注册。
启动过程分析
上面我们讲解了这个注册信息的过程,而它们的执行是伴随着当前模块的加载而执行的,我们都知道静态变量的初始化是先于Main函数执行的。下面我们简单画了一下虚幻编辑器的启动流程,这样我们就可以准确地看到整个注册反射信息的过程了。
void ProcessNewlyLoadedUObjects()
{
// ...
UClassRegisterAllCompiledInClasses();
const TArray<UClass* (*)()>& DeferredCompiledInRegistration = GetDeferredCompiledInRegistration();
const TArray<FPendingStructRegistrant>& DeferredCompiledInStructRegistration = GetDeferredCompiledInStructRegistration();
const TArray<FPendingEnumRegistrant>& DeferredCompiledInEnumRegistration = GetDeferredCompiledInEnumRegistration();
bool bNewUObjects = false;
while( GFirstPendingRegistrant || DeferredCompiledInRegistration.Num() || DeferredCompiledInStructRegistration.Num() || DeferredCompiledInEnumRegistration.Num() )
{
bNewUObjects = true;
UObjectProcessRegistrants();
UObjectLoadAllCompiledInStructs();
UObjectLoadAllCompiledInDefaultProperties();
}
// ...
}
在这个函数中,可以看到void ProcessNewlyLoadedUObjects()这个函数就是我们主要关注的函数,我们前面讲到的注册的信息,包括类、结构体以及枚举类型的反射信息都会在这里进行注册。
收集
在生成阶段中,我们生成了很多Class、Property、Function的信息。但是我们需要它们的信息收集整合到我们需要的数据结构里保存,以便下一阶段的使用。
通过UClassCompiledInDefer收集
深入UClassCompiledInDefer方法我们可以发现。
void UClassCompiledInDefer(FFieldCompiledInInfo* ClassInfo, const TCHAR* Name, SIZE_T ClassSize, uint32 Crc)
{
// We will either create a new class or update the static class pointer of the existing one
GetDeferredClassRegistration().Add(ClassInfo); //static TArray<FFieldCompiledInInfo*> DeferredClassRegistration;
}
可以看到UClassCompiledInDefer(this, InName, InClassSize, InCrc),传进去4个参数,主要是收集三个数据,为啥要传4个参数呢,是因为把自己的指针传进去,为了方便后续调用Register()方法。
实现的功能主要是在一个静态Array里添加信息记录。
这个数组会在后续注册Class的时候会被调用,然后调用Register(),实现注册功能。
/** Register all loaded classes */
void UClassRegisterAllCompiledInClasses()
{
//...
TArray<FFieldCompiledInInfo*>& DeferredClassRegistration = GetDeferredClassRegistration();
for (const FFieldCompiledInInfo* Class : DeferredClassRegistration)
{
UClass* RegisteredClass = Class->Register(); //实现注册,关键步骤。
#if WITH_HOT_RELOAD
if (GIsHotReload && Class->OldClass == nullptr)
{
AddedClasses.Add(RegisteredClass);
}
#endif
}
DeferredClassRegistration.Empty(); //注册完成,清空注册表
//...
而Register函数实际上传进来的就是StaticClass函数
接着就是StaticClass()部分如何收集的解释。
通过StaticClass()我们可以发现主要是调用了GetPrivateStaticClass(),而GetPrivateStaticClass()也主要是调用了GetPrivateStaticClassBody()函数,那么通过这个函数收集到了哪些信息呢。
以下是主要的信息:
**PackageName:**StaticPackage()
Name:类名,+1去掉U、A、F前缀,+11去掉_Deprecated前缀
ReturnClass:输出引用,也就是收集信息后产生的UClass,PrivateStaticClass
void(*RegisterNativeFunc)(): StaticRegisterNativesAHelloGameMode(), 收集原生的函数。
InSize: 类的大小
InClassConstructor:构造函数,在DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(AHelloGameMode)实现。
InSuperClassFn:&TClass::Super::StaticClass, 父类的注册函数。
// in DECLARE_CLASS
inline static UClass* StaticClass()
{
return GetPrivateStaticClass();
}
// in IMPLEMENT_CLASS
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, // StaticRegisterNativesAHelloGameMode,
sizeof(TClass),
(EClassFlags)TClass::StaticClassFlags,
TClass::StaticClassCastFlags(),
TClass::StaticConfigName(),
(UClass::ClassConstructorType)InternalConstructor<TClass>, //构造函数,在DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(AHelloGameMode) 实现
(UClass::ClassVTableHelperCtorCallerType)InternalVTableHelperCtorCaller<TClass>,
&TClass::AddReferencedObjects,
&TClass::Super::StaticClass,
&TClass::WithinClass::StaticClass
);
}
return PrivateStaticClass;
}
这里主要是通过收集到的信息构造UClass。
主要有三部分:
首先给ReturnClass分配内存,然后初始化构造。
首先确保自己的父类已经注册了,设置SuperStruct和 在全局PendingRegistrantsMaps中添加自己身类,延迟注册。(在注册阶段会对这部分进行解释)
Register the class’s native functions,注册自身原生的函数、蓝图可执行的函数,执行 StaticRegisterNativesAHelloGameMode();
// 删减部分代码
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*/
)
{
if (!bIsDynamic)
{
ReturnClass = (UClass*)GUObjectAllocator.AllocateUObject(sizeof(UClass), alignof(UClass), true); // 内存分配
// 构造函数
ReturnClass = ::new (ReturnClass)
UClass
(
EC_StaticConstructor,
Name,
InSize,
InClassFlags,
InClassCastFlags,
InConfigName,
EObjectFlags(RF_Public | RF_Standalone | RF_Transient | RF_MarkAsNative | RF_MarkAsRootSet),
InClassConstructor,
InClassVTableHelperCtorCaller,
InClassAddReferencedObjects
);
check(ReturnClass);
}
// 设置SuperStruct和 在全局PendingRegistrantsMaps中添加自己身类,后续注册UClass的时候会从Maps去除自身。
InitializePrivateStaticClass(
InSuperClassFn(),
ReturnClass,
InWithinClassFn(),
PackageName,
Name
);
// Register the class's native functions.
// 注册自身原生的函数。蓝图可执行的函数
// 执行 StaticRegisterNativesAHelloGameMode();
RegisterNativeFunc();
}
通过UObjectCompiledInDefer收集
深入UObjectCompiledInDefer方法我们可以发现:
void UObjectCompiledInDefer(UClass *(*InRegister)(), UClass *(*InStaticClass)(), const TCHAR* Name, const TCHAR* PackageName, bool bDynamic, const TCHAR* DynamicPathName, void (*InInitSearchableValues)(TMap<FName, FName>&))
{
// ...
TArray<UClass *(*)()>& DeferredCompiledInRegistration = GetDeferredCompiledInRegistration();
checkSlow(!DeferredCompiledInRegistration.Contains(InRegister));
DeferredCompiledInRegistration.Add(InRegister);
// ...
}
传进去一个InRegister函数指针到延迟注册数组中,也就是Z_Construct_UClass_AHelloGameMode函数。
然后在启动阶段会调用UObjectLoadAllCompiledInDefaultProperties函数,遍历所有的延迟注册数组,进行收集注册
static void UObjectLoadAllCompiledInDefaultProperties()
{
static FName LongEnginePackageName(TEXT("/Script/Engine"));
TArray<UClass *(*)()>& DeferredCompiledInRegistration = GetDeferredCompiledInRegistration();
const bool bHaveRegistrants = DeferredCompiledInRegistration.Num() != 0;
if( bHaveRegistrants )
{
TArray<UClass*> NewClasses;
TArray<UClass*> NewClassesInCoreUObject;
TArray<UClass*> NewClassesInEngine;
TArray<UClass* (*)()> PendingRegistrants = MoveTemp(DeferredCompiledInRegistration);
for (UClass* (*Registrant)() : PendingRegistrants)
{
UClass* Class = Registrant(); // 在这一步调用了Z_Construct_UClass_AHelloGameMode函数
//...
}
// ...
}
}
而Z_Construct_UClass_AHelloGameMode函数主要是调用了ConstructClass函数。
可以看到,Construct函数传进去两个参数,一个OuterClass,一个ClassParams。
OuterClass是我们注册后得到的输出对象。
而ClassParams是我们需要收集的数据。
以下让我们对ClassParams的数据进行一些分析,下面是一些重要的数据部分:
ClassNoRegisterFunc: StaticClass,作用在上面有具体讲述
DependencySingletonFuncArray: 所依赖的构建函数,确保自己所依赖部分都被构建成功。
FunctionLinkArray:需要反射的函数数组
PropertyArray: 属性数组
const UE4CodeGen_Private::FClassParams Z_Construct_UClass_AHelloGameMode_Statics::ClassParams = {
&AHelloGameMode::StaticClass,
DependentSingletons, ARRAY_COUNT(DependentSingletons),
0x009002A8u,
FuncInfo, ARRAY_COUNT(FuncInfo),
Z_Construct_UClass_AHelloGameMode_Statics::PropPointers, ARRAY_COUNT(Z_Construct_UClass_AHelloGameMode_Statics::PropPointers),
nullptr,
&StaticCppClassTypeInfo,
nullptr, 0,
METADATA_PARAMS(Z_Construct_UClass_AHelloGameMode_Statics::Class_MetaDataParams, ARRAY_COUNT(Z_Construct_UClass_AHelloGameMode_Statics::Class_MetaDataParams))
};
struct FClassParams
{
UClass* (*ClassNoRegisterFunc)(); // 传进来StaticClass的函数
UObject* (*const *DependencySingletonFuncArray)(); // 传进所依赖函数的函数数组
int32 NumDependencySingletons; //上述函数的个数
uint32 ClassFlags; // EClassFlags
const FClassFunctionLinkInfo* FunctionLinkArray; // 需要反射的函数,具体的后面会讲述
int32 NumFunctions; //同上
const FPropertyParamsBase* const* PropertyArray; // 后续再讲这个
int32 NumProperties; //同上
const char* ClassConfigNameUTF8;
const FCppClassTypeInfoStatic* CppClassInfo;
const FImplementedInterfaceParams* ImplementedInterfaceArray; //本例子中没用上
int32 NumImplementedInterfaces; // 0
#if WITH_METADATA
const FMetaDataPairParam* MetaDataArray;
int32 NumMetaData;
#endif
};
以下是ConstructUclass的部分内容。
根据流程图,可以看到,首先是执行的DependecySingletonFunctions,检测自己所依赖的单例是否都建立了反射关系。接着就是StaticClass函数部分,这也是我们重点关注的部分。在StaticClass,实现了构建了一个基础的UClass的反射,但是还没有完善细节,因此在后续部分给NewClass添加细节部分。
步骤:
执行DependecySingletonFunctions数组中所依赖的函数,确保自身的父类反射已经被构建和自身所在的Package反射被构建。
UObjectForceRegistration,把自己从等待注册的Map中去掉, 并且注册。
ConstructUFunction,组建Function,并且注册Function。(这部分后续再探讨
ConstructUProperties,组建Properties。(这部分后续再探讨
void ConstructUClass(UClass*& OutClass, const FClassParams& Params) // 关键点,用了引用
{
if (OutClass && (OutClass->ClassFlags & CLASS_Constructed)) // 如果已经构建过了,则返回
{
return;
}
// 执行所依赖过的函数, 需要所依赖部分已经建立反射关系。(PS: Ue4源码部分 充斥着大量的懒汉单例模式)
for (UObject* (*const *SingletonFunc)() = Params.DependencySingletonFuncArray, *(*const *SingletonFuncEnd)() = SingletonFunc + Params.NumDependencySingletons; SingletonFunc != SingletonFuncEnd; ++SingletonFunc)
{
(*SingletonFunc)();
}
// 所传进来的是StaticClass函数
UClass* NewClass = Params.ClassNoRegisterFunc();
OutClass = NewClass;
if (NewClass->ClassFlags & CLASS_Constructed) // 如果这个已经构建过了,返回
{
return;
}
// 延迟注册
// 把自己从等待注册的Map中去掉
UObjectForceRegistration(NewClass);
NewClass->ClassFlags |= (EClassFlags)(Params.ClassFlags | CLASS_Constructed);
// Make sure the reference token stream is empty since it will be reconstructed later on
// This should not apply to intrinsic classes since they emit native references before AssembleReferenceTokenStream is called.
if ((NewClass->ClassFlags & CLASS_Intrinsic) != CLASS_Intrinsic)
{
check((NewClass->ClassFlags & CLASS_TokenStreamAssembled) != CLASS_TokenStreamAssembled);
NewClass->ReferenceTokenStream.Empty();
#if ENABLE_GC_OBJECT_CHECKS
NewClass->DebugTokenMap.Empty();
#endif
}
// 创建Function的反射,利用传进来的构建函数来构建函数。
NewClass->CreateLinkAndAddChildFunctionsToMap(Params.FunctionLinkArray, Params.NumFunctions);
// 如果看过上一篇blog的话,就可以发现其实属性构建部分是没有单独的函数构建的
// 原来在这部分实现。
ConstructUProperties(NewClass, Params.PropertyArray, Params.NumProperties);
// ConstructClass还有很多内容,但是由于篇幅关系,这里就不一一放出来。
注册阶段
注册阶段主要分为两个过程,首先构建UClass的时候添加进等待注册表,等待注册,在收集完成UClass所有信息的时候,延迟注册。
注册过程分析
Ue4是如何完成注册的呢?
/**
* Convert a boot-strap registered class into a real one, add to uobject array, etc
*
* @param UClassStaticClass Now that it is known, fill in UClass::StaticClass() as the class
*/
void UObjectBase::DeferredRegister(UClass *UClassStaticClass,const TCHAR* PackageName,const TCHAR* InName)
{
check(Internal::GObjInitialized);
// Set object properties.
UPackage* Package = CreatePackage(nullptr, PackageName);
check(Package);
Package->SetPackageFlags(PKG_CompiledIn);
OuterPrivate = Package;
check(UClassStaticClass);
check(!ClassPrivate);
ClassPrivate = UClassStaticClass;
// Add to the global object table.
AddObject(FName(InName), EInternalObjectFlags::None);
// Make sure that objects disregarded for GC are part of root set.
check(!GUObjectArray.IsDisregardForGC(this) || GUObjectArray.IndexToObject(InternalIndex)->IsRootSet());
其实就是把UClass放到一个Hash表中去。
void HashObject(UObjectBase* Object)
{
auto& ThreadHash = FUObjectHashTables::Get();
Hash = GetObjectHash(Name);
ThreadHash.AddToHash(Hash, Object);
Hash = GetObjectOuterHash( Name, (PTRINT)Object->GetOuter() );
ThreadHash.HashOuter.Add( Hash, Object );
AddToOuterMap( ThreadHash, Object );
AddToClassMap( ThreadHash, Object );
}
通过上面的代码,我们可以发现通过名字生成Index和通过OuterName(也就是PackageName)来生成Index,然后添加进Map内部中。具体如何添加进HashMap中呢?请看下面的分析。
TUObjectHashTables
在这里就不得不介绍这个类TUObjectHashTables了,主要的存储的类。注册的反射的数据都以存放在这里。
首先看看这个类的定义。
class FUObjectHashTables
{
FCriticalSection CriticalSection;
public:
/** Hash sets */
TMap<int32, FHashBucket> Hash;
TMultiMap<int32, class UObjectBase*> HashOuter;
/** Map of object to their outers, used to avoid an object iterator to find such things. **/
TMap<UObjectBase*, FHashBucket> ObjectOuterMap;
TMap<UClass*, TSet<UObjectBase*> > ClassToObjectListMap;
TMap<UClass*, TSet<UClass*> > ClassToChildListMap;
static FUObjectHashTables& Get() // 饿汉单例
{
static FUObjectHashTables Singleton;
return Singleton;
}
FORCEINLINE void AddToHash(int32 InHash, UObjectBase* Object)
{
FHashBucket& Bucket = Hash.FindOrAdd(InHash);
Bucket.Add(Object);
}
}
首先这个类用了饿汉单例的设计模式,保证只有一个存储数据的实例,
从这个类中我们可以看到有5个Map,具体的映射方式也都显而易见了,看看我们注册的时候用了这几个Map是怎样处理的。
void HashObject(UObjectBase* Object)
{
auto& ThreadHash = FUObjectHashTables::Get(); //获取存储单例
Hash = GetObjectHash(Name); //把FName转化为Int Index,来映射
ThreadHash.AddToHash(Hash, Object);
Hash = GetObjectOuterHash( Name, (PTRINT)Object->GetOuter() );
ThreadHash.HashOuter.Add( Hash, Object );
AddToOuterMap( ThreadHash, Object );
AddToClassMap( ThreadHash, Object );
}
第一个Map:Hash,TMap<int32, FHashBucket> Hash,映射方式通过int32映射到一个桶。(每个桶我们其实都可以看成是一个类的集合)(为了容易理解,此处的int32亦可以理解为FName)
ThreadHash.AddToHash(Hash, Object);
FORCEINLINE void AddToHash(int32 InHash, UObjectBase* Object)
{
FHashBucket& Bucket = Hash.FindOrAdd(InHash);
Bucket.Add(Object);
}
通过代码可以看出来,通过自己的名字找到了属于自己的那个桶,并且在桶中把自己添加进去。
由此可以看出HashMap的用处其实就是通过自身名字找到属于自己的桶集合。
第二个Map:HashOuter,TMultiMap<int32, class UObjectBase*> HashOuter,映射方式通过int32映射到一个UObjectBase类。(为了容易理解,此处的int32亦可以理解为OuterFName)
ThreadHash.HashOuter.Add( Hash, Object );
通过代码可以看出,HashOuterMap的用处其实是通过OuterFName的名字来找到自己的Outer。
第三个Map:ObjectOuterMap,TMap<UObjectBase*, FHashBucket> ObjectOuterMap,映射方式是用过Outer来找到自己的Bucket。
AddToOuterMap( ThreadHash, Object );
// Assumes that ThreadHash's critical is already locked
FORCEINLINE static void AddToOuterMap(FUObjectHashTables& ThreadHash, UObjectBase* Object)
{
FHashBucket& Bucket = ThreadHash.ObjectOuterMap.FindOrAdd(Object->GetOuter());
checkSlow(!Bucket.Contains(Object)); // if it already exists, something is wrong with the external code
Bucket.Add(Object);
}
通过代码可以看出, ObjectOuterMap是通过Outer来获取到Outer的Bucket。(Outer实际上就是Package)(类似于第二个Map)
第四个Map:ClassToObjectListMap,TMap<UClass*, TSet<UObjectBase*> > ClassToObjectListMap。
{
check(Object->GetClass());
TSet<UObjectBase*>& ObjectList = ThreadHash.ClassToObjectListMap.FindOrAdd(Object->GetClass());
bool bIsAlreadyInSetPtr = false;
ObjectList.Add(Object, &bIsAlreadyInSetPtr);
check(!bIsAlreadyInSetPtr); // if it already exists, something is wrong with the external code
}
通过代码可以看出, ClassToObjectListMap是通过自身来获取到自身所包含的一个Set(类似于第一个Map)。
第五个Map:ClassToChildListMap,TMap<UClass*, TSet<UClass*> > ClassToChildListMap
UObjectBaseUtility* ObjectWithUtility = static_cast<UObjectBaseUtility*>(Object);
if ( ObjectWithUtility->IsA(UClass::StaticClass()) )
{
UClass* Class = static_cast<UClass*>(ObjectWithUtility);
UClass* SuperClass = Class->GetSuperClass();
if ( SuperClass )
{
TSet<UClass*>& ChildList = ThreadHash.ClassToChildListMap.FindOrAdd(SuperClass);
bool bIsAlreadyInSetPtr = false;
ChildList.Add(Class, &bIsAlreadyInSetPtr);
check(!bIsAlreadyInSetPtr); // if it already exists, something is wrong with the external code
}
}