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: 这是个输出参数,传的实参就是局部静态变量PrivateStaticClassRegisterNativeFunc: 一个回调函数,用来向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) 对象,完整流程图如下
