深入UE——UObject(八)类型系统注册-CoreUObject模块加载

引言

前文介绍了Static初始化阶段的最后一步进行的操作,创建出了第一个UClass对象。接着遵循程序启动的流程,本文就开始介绍Main函数入口进来后的流程。

还请注意以下几点:

  • UE引擎这么大,其初始化从WinMain开始必然要经过一系列繁复的过程,而本章节只关注跟CoreUObject模块里,或者说是UObject系统相关的内容和流程,其他的初始化(比如窗口创建,线程启动,模块加载等)我们暂时忽略,挖个坑留待后续讲解。
  • 同时也为了最简明的说明流程,忽略编辑器的相关函数调用流程内容,只关心Runtime下的流程(就是游戏打包后运行起来的流程。调试的过程是采用源码版引擎,创建个项目,先在Editor下CookContentForWindows,然后在VS里转为Debug配置,编译运行。这样就可以一起跟踪调试Game和Engine的内容。)
  • 流程图里的箭头连接的代码块,不代表源码里就是这么直接相邻,中间仍然可能有其他的代码,只是与主题无关,所以不表示出来。箭头方向向右表示函数的嵌套调用,越向右嵌套越深;箭头向下表示一个函数内部的顺序执行代码块,向下结束表示这个函数完成执行。

引擎整体流程

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

  • Static初始化就是指的前文说的收集过程。
  • 以Windows平台为例,WinMain是LaunchWindows.cpp里定义的程序入口。
int32 WINAPI WinMain( _In_ HINSTANCE hInInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ char*, _In_ int32 nCmdShow )
{
    //...
    ErrorLevel = GuardedMain( CmdLine, hInInstance, hPrevInstance, nCmdShow );
    //...
    FEngineLoop::AppExit(); //程序的退出
    //...
    return ErrorLevel;
}
  • 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;
}
  • FEngineLoop::PreInit是我们关心的涉及UObject启动的最开始的地方。

FEngineLoop::PreInit

我们知道,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里被调用
        //...
    }
}

UClassRegisterAllCompiledInClasses

展开后是:

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;
}

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

  1. GetDeferredClassRegistration()里的元素是之前收集文章里讲的静态初始化的时候添加进去的,在XXX.gen.cpp里用static TClassCompiledInDefer这种形式添加。
  2. TClassCompiledInDefer<TClass>::Register()内部只是简单的转调TClass::StaticClass()
  3. TClass::StaticClass()是在XXX.generated.h里的DECLARE_CLASS宏里定义的,内部只是简单的转到GetPrivateStaticClass(TPackage)
  4. GetPrivateStaticClass(TPackage)的函数是实现是在IMPLEMENT_CLASS宏里。其内部会真正调用到GetPrivateStaticClassBody。这个函数的内部会创建出UClass对象并调用Register(),在上篇已经具体讲解过了。
  5. 总结这里的逻辑就是对之前收集到的所有的XXX.gen.cpp里定义的类,都触发一次其UClass的构造,其实也只有UObject比较特殊,会在Static初始化的时候就触发构造。因此这个过程其实是类型系统里每一个类的UClass的创建过程。
  6. 这个函数会被调用多次,在后续的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会越先被初始化。

附一下CoreUObject里面的各UClass来混个眼熟,反正也不多:

//Static初始化:
Object
//CoreUObject:
GCObjectReferencer,TextBuffer,Field,Struct,ScriptStruct,Class,Package,Function,DelegateFunction,DynamicClass,PackageMap,Enum,EnumProperty,Property,Interface,LinkerPlaceholderClass,LinkerPlaceholderExportObject,LinkerPlaceholderFunction,MetaData,ObjectRedirector,ArrayProperty,ObjectPropertyBase,BoolProperty,ByteProperty,NumericProperty,ClassProperty,ObjectProperty,DelegateProperty,DoubleProperty,FloatProperty,IntProperty,Int16Property,Int64Property,Int8Property,InterfaceProperty,LazyObjectProperty,MapProperty,MulticastDelegateProperty,NameProperty,SetProperty,SoftClassProperty,SoftObjectProperty,StrProperty,StructProperty,UInt16Property,UInt32Property,UInt64Property,WeakObjectProperty,TextProperty

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

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

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

讲到这,希望大家好好领悟这一句话:

UObject对象的类型是UClass,而UClass是个UObject对象。

总结

篇幅所限,本篇其实才刚刚讲了PreInit里面的LoadCoreModules(),这一步骤的目的主要是为了把CoreUObject里面定义的类的UClass都给先构建出来。但是其实这些UClass对象内部的值还没有完成初始化设置,因此下一个步骤的AppInit()ProcessNewlyLoadedUObjects()还会继续这个注册的步程。下篇再来讲解AppInit()里的道道。

猜你喜欢

转载自blog.csdn.net/ttod/article/details/133254550