好景何曾虚过
胜友是处相留
回顾一下文章 UE4中反射信息收集 中的内容:
- 属性类型信息收集的流程
- 函数信息收集的流程
- 延迟注册信息的收集
可以整理成以下两张图:
生成UClass
对象的流程:
往UClass
对象上填充信息的流程:
收集的路程已经确定,而对于注册的执行路径还是未知,也就是上图中的问号。
显然得从引擎的启动以及各模块的加载来找答案了。
引擎启动流程
UE4动态库加载与静态初始化
相信不少细心的小伙伴会发现生成文件中存在大量的全局静态变量,这些变量的初始化叫做静态初始化。关于静态初始化,关注以下几点:
- 从生命周期的角度理解,静态
static
变量不止是指static
关键词修饰的变量,这些变量全都存在 “静态初始化”,包括:- 全局变量
- 静态变量(局部,全局)
- 同文件的静态初始化根据声明顺序由上至下进行,而跨文件的静态初始化顺序的是不确定的
- 静态初始化进行的时机取决于其变量所在模块加载的时间
- 静态初始化分为编译期初始化及运行时初始化,不过这不是我们要纠结的内容,就不过多赘述了
UE4中各静态变量静态初始化进行的时机是取决于其所在模块加载的时间的,于是就存在顺序问题了。每个模块中都有大量的类,谁先谁后呢?如何处理彼此之间的依赖呢?
UE4中各模块加载的顺序的是确定的,也就是说,上层模块依赖于底层模块,则应控制底层模块先加载,而上层模块后加载。而对于同一模块中,不同文件中的静态变量,在设计的时候就不应该依赖。UE4中使用了大量的懒汉 Static 加载模式,应该就是出于此目的。
而 CoreUObject 模块就主要是包含虚幻引擎的对象系统 (UObject) 和类型系统 (UClass) ,这几篇文章的内容也基本都包含在 CoreUObject 模块中。
main 前 main 后
UE4中并没有使用 C/C++ 中通用的 main()
函数作为程序的入口,在 Windows 下使用了 WinMain()
作为了入口。看资料,应该是 WinMain()
作为 GUI 程序的入口,WinMain vs. main (C++)。
为了方便,这里还是统一称为 main 。
main 以前,会执行静态初始化的逻辑,前面讲到的反射类型信息收集就是在此阶段完成的。
main 以后,就是引擎其他的初始化以及逻辑loop了,这里初始化就包括了给各个类生成 UClass
对象,并填充 UClass
信息。
有一个非常有意思的问题,假设已经明确类的 UClass
对象是在 mian 以后生成的,那 UObject
的 UClass
对象是在什么时候呢?
第一个 UClass 对象
首先给出结论, UObject
的 UClass
对象是在 main 前生成的,引擎 ScriptCore.cpp 中定义了几个宏:
很明显看到,UObject::StaticClass()
会在 IMPLEMENT_FUNCTION
被触发调用,IMPLEMENT_VM_FUNCTION
宏的调用就会使用 IMPLEMENT_FUNCTION
宏,也就是会调用 UObject::StaticClass()
。
也就是说第一次调用 IMPLEMENT_VM_FUNCTION
宏的地方,就会生成 UObject
的 UClass
对象。
在引擎 ScriptCore.cpp 中, IMPLEMENT_VM_FUNCTION
宏第一次调用在:
DEFINE_FUNCTION(UObject::execCallMathFunction)
{...}
IMPLEMENT_VM_FUNCTION(EX_CallMath, execCallMathFunction);
根据之前的内容,明白 UObject::StaticClass()
会调用 UObject::StaticClass()
,随即生成 UObject
的 UClass
对象。
而这个过程发生显然发生在 main 之前,根据之前的分析,UClass
对象即可通过动态调用,动态生成对应的对象,也就是说UClass
可用于生成 UObject
对象。
这时候,再祭出这张图:
UClass
其实就是 UObject
。
想到了Inside UE4中的一句话,“UObject对象的类型是UClass,而UClass是个UObject对象。”
main 后的执行流程
以windows为例,源码中,可以直接找到GuardedMain()
函数,查看其调用层次。
我们仅仅关心涉及 CoreUObject 模块,或者说是 UObject 对象以及类型系统相关的内容和流程。
还是画个图更直观。。。
深褐色的就是与涉及 CoreUObject 模块的相关函数调用。
CoreUObject 模块的加载是在 FEngineLoop::PreInit()
-> FEngineLoop::PreInitPreStartupScreen()
中完成:
int32 FEngineLoop::PreInitPreStartupScreen(const TCHAR* CmdLine)
{
....
LoadCoreModules(); //加载CoreUObject模块
...
AppInit(); //程序初始化
...
}
函数 LoadCoreModules()
:
bool FEngineLoop::LoadCoreModules()
{
// Always attempt to load CoreUObject. It requires additional pre-init which is called from its module's StartupModule method.
#if WITH_COREUOBJECT
#if USE_PER_MODULE_UOBJECT_BOOTSTRAP // otherwise do it later
FModuleManager::Get().OnProcessLoadedObjectsCallback().AddStatic(ProcessNewlyLoadedUObjects);
#endif
return FModuleManager::Get().LoadModule(TEXT("CoreUObject")) != nullptr;
#else
return true;
#endif
}
这样,涉及 CoreUObject 模块的相关函数调用流程就大概清晰了。
类型信息注册流程
继续跟着 CoreUObject 模块的加载,追踪类型系统的注册流程。
生成 UClass 对象
FModuleManager::Get().LoadModule(TEXT("CoreUObject"))
会触发调用 FCoreUObjectModule::StartupModule()
:
class FCoreUObjectModule : public FDefaultModuleImpl
{
virtual void StartupModule() override
{
// Register all classes that have been loaded so far. This is required for CVars to work.
UClassRegisterAllCompiledInClasses();
void InitUObject();
FCoreDelegates::OnInit.AddStatic(InitUObject);
...
}
}
这里注意到 InitUObject()
注册了回调,后续会在 AppInit()
中调用。
继续展开 UClassRegisterAllCompiledInClasses()
void UClassRegisterAllCompiledInClasses()
{
TArray<FFieldCompiledInInfo*>& DeferredClassRegistration = GetDeferredClassRegistration();
for (const FFieldCompiledInInfo* Class : DeferredClassRegistration)
{
// TClassCompiledInDefer<TClass>::Register()
UClass* RegisteredClass = Class->Register(); //return TClass::StaticClass();
}
DeferredClassRegistration.Empty();
}
static TArray<FFieldCompiledInInfo*>& GetDeferredClassRegistration()
{
static TArray<FFieldCompiledInInfo*> DeferredClassRegistration;
return DeferredClassRegistration;
}
这里就要联想起前面的内容。
即调用 Register()
会生成每个类的 UClass
对象,到这里就确定了类的 UClass
对象的生成时机。
AppInit()
函数 PreInit()
结束调用 FEngineLoop::LoadCoreModules()
以后,AppInit()
会调用之前注册的InitObject()
。
bool FEngineLoop::AppInit()
{
...
FCoreDelegates::OnInit.Broadcast(); //在CoreUOject模块加载的时候注册了InitUObject
return true;
}
继续跟踪 InitUObject()
:
void InitUObject()
{
...
FModuleManager::Get().OnProcessLoadedObjectsCallback().AddStatic(ProcessNewlyLoadedUObjects);
...
StaticUObjectInit(); // 创建Package,设置OuterPrivate,ClassPrivate,NamePrivate等
}
可以看到 函数 ProcessNewlyLoadedUObjects()
的注册,这说明ProcessNewlyLoadedUObjects()
不仅仅会被调用一次。这里我们假设ProcessNewlyLoadedUObjects()
就是用于在已经生成的 UClass
对象上填充信息,而每加载一个新的模块,首先会静态初始化,并生成对应的 UClass
,那 ProcessNewlyLoadedUObjects()
是不是也会随着新模块的加载而被重复调用用于填充信息呢。
InitUObject()
中后续的 StaticUObjectInit()
,由于与前文关联不大,不再过多赘述,其作用可简述为:创建Package,设置OuterPrivate,ClassPrivate,NamePrivate等。
注册 UClass 对象(填充)
AppInit()
执行结束后,ProcessNewlyLoadedUObjects()
便作为回调注册到了 FMOduleManager::ProcessLoadedObjectsCallback
中:
实际上,FModuleManager::LoadModule()
中会先后执行 StartupModule()
和 ProcessNewlyLoadedUObjects()
。
通过堆栈可以发现, ProcessLoadedObjectsCallback()
会在加载模块时调用。
接着查看 ProcessLoadedObjectsCallback()
的实现,去掉了对分析无关紧要的代码:
void ProcessNewlyLoadedUObjects(FName Package, bool bCanProcessNewlyLoadedObjects)
{
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();
}
if (bNewUObjects && !GIsInitialLoad)
{
UClass::AssembleReferenceTokenStreams();
}
}
可以很明显的看到,代码里使用了之前解析反射信息收集时的一些结构和函数。
UClassRegisterAllCompiledInClasses()
用于初次生成类对应的UClass
对象DeferredCompiledInRegistration
就是解析反射信息收集时,construct 回调注册的数组,对应 classDeferredCompiledInStructRegistration
和DeferredCompiledInEnumRegistration
应该对应 struct 和 enum
主要关注 UClass
的注册和信息填充,主要实现在 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_XXX(),执行信息填充
// 根据所属Package添加到对应的数组里
if (Class->GetOutermost()->GetFName() == GLongCoreUObjectPackageName)
{ NewClassesInCoreUObject.Add(Class); }
else if (Class->GetOutermost()->GetFName() == LongEnginePackageName)
{ NewClassesInEngine.Add(Class); }
else
{ NewClasses.Add(Class); }
}
// 下面代码进行了简化,构造对应的CDO
for (UClass* Class : NewClassesInCoreUObject) { Class->GetDefaultObject(); }
for (UClass* Class : NewClassesInEngine) { Class->GetDefaultObject(); }
for (UClass* Class : NewClasses) { Class->GetDefaultObject(); }
}
}
可以看到,函数中完成了信息填充,也就是将之前生成的近乎是空的 UClass
填充对应的 UProperty
和 UFunction
。
于是可以将整个过程总结为: