UE4反射机制的通俗理解【CoreObject加载】

先大概看一下当运行项目时候的整个引擎启动流程。其中绿色的部分表示有涉及CoreUObject模块。

GuardedMain是真正的实现程序循环的地方。其中Engine开头的函数内部其实只是简单的转调一个全局的GEngineLoop的内部函数。

FEngineLoop GEngineLoop;
int32 GuardedMain( const TCHAR* CmdLine, HINSTANCE hInInstance, HINSTANCE hPrevInstance, int32 nCmdShow )
{
   // make sure GEngineLoop::Exit() is always called.
    struct EngineLoopCleanupGuard 
    { 
        ~EngineLoopCleanupGuard()
        {
            EngineExit();   //保证在函数退出后能调用    转向 GEngineLoop.Exit();
        }
    } CleanupGuard;
    //...
    int32 ErrorLevel = EnginePreInit( CmdLine );    //预初始化  转向 GEngineLoop.PreInit( CmdLine );
    //...
#if WITH_EDITOR
    if (GIsEditor)
    {
        ErrorLevel = EditorInit(GEngineLoop);   //编辑器有其初始化版本
    }
    else
#endif
    {
        ErrorLevel = EngineInit();   //Runtime下的初始化    转向 GEngineLoop.Init();
    }
    //...
    while( !GIsRequestingExit )
    {
        EngineTick();   //无限循环的Tick    转向 GEngineLoop.Tick();
    }
    #if WITH_EDITOR
    if( GIsEditor )
    {
        EditorExit();   //编辑器的退出
    }
#endif
    return ErrorLevel;
}

我们知道,UE是建立在UObject对象系统上的,所以引擎里别的模块想要启动加载起来,就得先把CoreUObject模块初始化完成。因此引擎循环的预初始化部分就得开始加载CoreUObject了。

int32 FEngineLoop::PreInit(const TCHAR* CmdLine)
{
    //...
    LoadCoreModules();  //加载CoreUObject模块
    //...
    //LoadPreInitModules();   //加载一些PreInit的模块,比如Engine,Renderer
    //...
    AppInit();  //程序初始化
    //...
    ProcessNewlyLoadedUObjects();   //处理最近加载的对象
    //...
    //LoadStartupModules();   //自己写的LoadingPhase为PreDefault的模块在这个时候加载
    //...
    GUObjectArray.CloseDisregardForGC();    //对象池启用,最开始是关闭的
    //...
    //NotifyRegistrationComplete();   //注册完成事件通知,完成Package加载
}

从这个预初始化的流程可以看出,最先加载的是CoreUObject。 其中的LoadCoreModules()内部调用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();   //注册所有编译进来的类,此刻大概有1728多个

        void InitUObject();
        FCoreDelegates::OnInit.AddStatic(InitUObject);  //先注册个回调,后续会在AppInit里被调用
        //...
    }
}
void UClassRegisterAllCompiledInClasses()
{
    TArray<FFieldCompiledInInfo*>& DeferredClassRegistration = GetDeferredClassRegistration();
    for (const FFieldCompiledInInfo* Class : DeferredClassRegistration)
    {
        //这里的Class其实是TClassCompiledInDefer<TClass>
        UClass* RegisteredClass = Class->Register();    //return TClass::StaticClass();
    }
    DeferredClassRegistration.Empty();  //前面返回的是引用,因此这里可以清空数据。
}
//...
static TArray<FFieldCompiledInInfo*>& GetDeferredClassRegistration()    //返回可变引用
{
    static TArray<FFieldCompiledInInfo*> DeferredClassRegistration; //单件模式
    return DeferredClassRegistration;
}

想看懂这里的逻辑需要回顾提醒的有(忘了的请翻阅前三篇):

GetDeferredClassRegistration()里的元素是之前收集文章里讲的静态初始化的时候添加进去的,在XXX.gen.cpp里用static TClassCompiledInDefer这种形式添加。(这里就是第一个static变量收集的信息)
TClassCompiledInDefer<TClass>::Register()内部只是简单的转调TClass::StaticClass()。
TClass::StaticClass()是在XXX.generated.h里的DECLARE_CLASS宏里定义的,内部只是简单的转到GetPrivateStaticClass(TPackage)。
GetPrivateStaticClass(TPackage)的函数是实现是在IMPLEMENT_CLASS宏里。其内部会真正调用到GetPrivateStaticClassBody。这个函数的内部会创建出UClass对象并调用Register(),在上篇已经具体讲解过了。
总结这里的逻辑就是对之前收集到的所有的XXX.gen.cpp里定义的类,都触发一次其UClass的构造,其实也只有UObject比较特殊,会在Static初始化的时候就触发构造。因此这个过程其实是类型系统里每一个类的UClass的创建过程。
这个函数会被调用多次,在后续的ProcessNewlyLoadedUObjects的里仍然会触发该调用。在FCoreUObjectModule::StartupModule()的这次调用是最先的,这个时候加载编译进来的的类都是引擎启动一开始就链接进来的。

通过对关键代码的增加Log打印(比如在GetPrivateStaticClassBody的最后打印), 朋友们可能会发现在Editor模式和Runtime模式下,各类的UClass可能会不太一样。这一方面原因是因为dll链接加载的方式顺序不一样,另一方面也是因为static变量的初始化顺序是不确定的,所以会造成进来的FFieldCompiledInInfo顺序不一样。但这其实也没太多影响,因为UE的代码里,有大量的防护性代码去加载前置所需要的类。另一方面,因为这个阶段生成的UClass,也只有SuperStruct和WithinClass之间的依赖,所以一定的顺序不定也没有关系。Static初始化的“Object”Class是最先的,Editor模式下会先加载CoreUObject模块和其他引擎模块,最后才是Hello模块(原因其实是编辑器的exe启动了然后去加载Hello.dll)。而打包后的游戏Runtime就反了过来,会先加载Hello模块,然后才是CoreUObject模块(原因其实是Hello.exe启动后内部加载其他dll)。所以static变量初始化的顺序其实大体上是越顶层的dll会越先被初始化。

思考:Struct和Enum的注册为何在这一个阶段无体现?

在此阶段,我们好像没有看见在模块里定义的结构和枚举有参与此阶段的注册。其实是因为结构在注册后生成的元数据信息保存的对象是UScriptStruct,枚举对应的是UEnum,类对应的是UClass。 虽然我们在上篇说构造出来的第一个UClass也是一个UObject,但其实除了在Native编译进来的UClass,其他的UObject的构造都得需要有其对应的UClass的辅助,因为UClass里保存了类的构造函数指针。所以如果想构造出UScriptStruct和UEnum对象,就必须先有描述这两个类元数据信息的UClass。而这两个名为“ScriptStruct”和“Enum”的UClass在上述的CoreUObject模块加载里已经完成了。所以就不需要再做啥了。因此在这个阶段,其实已经是加载了所有基本的类型,因为类型就是用UClass描述。

ScriptStruct 和 Enum的UClass在哪里加载的。加载好了之后也需要构造这俩对象吧。好像是第二个static变量收集的信息就可以把这俩对象构造出来。

通俗理解就是在蓝图里构造这个MyStruct用的其实是UScriptStruct而不是UClass,UScriptStruct是用第二个static变量就可以构造出来,这里还没调到。

描述对象类型的只有UClass,UScriptStruct和UEnum是两个保存结构和枚举元数据信息的对象,而构造对象就需要先有其UClass。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值