UE4/UE5反射系统动态注册机制解析

发布于:2025-09-08 ⋅ 阅读:(17) ⋅ 点赞:(0)


核心目标

UE需要实现一个跨模块(DLL)、支持热重载的反射系统。这意味着:

  1. 编译器需要知道所有UObject类的信息(类名、大小、属性、函数等)。

  2. 运行时需要按正确的顺序创建UClass对象,尤其是在不同DLL中的类可能存在依赖关系(例如Game.dll中的AMyCharacter继承自Engine.dll中的ACharacter)时,必须确保父类的UClass先于子类被创建


阶段一:静态收集(编译期)

这个过程发生在编译每个模块(如Engine.dll, Game.dll)的时候。

1. 宏的展开

假设我们在一个游戏项目中有一个类,定义在MyCharacter.h中:

UCLASS()
class AMyCharacter : public ACharacter
{
    GENERATED_BODY()
    // ... 其他成员 ...
};

在它的.cpp文件中,必然有对应的IMPLEMENT宏:

IMPLEMENT_CLASS(AMyCharacter, 12345) // 12345 是假设的CRC校验值

这个宏会被预处理器展开,其核心是创建一个静态全局变量

static TClassCompiledInDefer<AMyCharacter> AutoInitializeAMyCharacter(
    TEXT("AMyCharacter"), 
    sizeof(AMyCharacter), 
    12345, 
    AMyCharacter::StaticClass, 
    &ACharacter::StaticClass // 父类信息
);

这个静态变量AutoInitializeAMyCharacter会在模块加载时(在main函数之前)就完成初始化。

2. 注册到全局列表

TClassCompiledInDefer的构造函数会做一件关键的事情:将自己(即类的元信息)添加到一个全局的、模块内部的数组中

这个数组就是DeferredCompiledInRegistration。你可以把它想象成这个模块的“待办事项列表”:

// 伪代码,在每个模块内部都存在这样一个列表
TArray<FRegistrarInfo> DeferredCompiledInRegistration;

void TClassCompiledInDefer::TClassCompiledInDefer(...){
    // 将 {类名, 类大小, 类CRC, 静态函数指针} 打包为一个信息结构体
    FRegistrarInfo Info = { ... };
    
    // 将这个结构体PUSH到模块的“待办事项列表”中
    DeferredCompiledInRegistration.Add(Info);
}

同时,它也可能将自己注册到一个Map中,用于快速查找。

至此,编译期的工作就完成了。 每个模块都独立地收集好了自己内部的所有UClass信息,并存放在自己的静态数组中。这些信息是无序的、分散的

阶段二:动态注册(运行时)

这个过程发生在引擎启动或模块动态加载时。

1.启动核心模块(CoreUObject)

引擎最先加载CoreUObject.dll。这个模块的启动代码会调用一个非常重要的函数:ProcessNewlyLoadedUObjects()

2.调用 Register() 函数

ProcessNewlyLoadedUObjects()函数会遍历当前已加载的所有模块中的那个“待办事项列表”(DeferredCompiledInRegistration数组)。

对于列表中的每一个项目,它都会调用其**Register()函数。这个Register()**函数的核心工作是:

void TClassCompiledInDefer<T>::Register()
{
    // 1. 创建或获取对应的 UClass 对象
    UClass* Class = TStaticClass<AMyCharacter>::Get();
    
    // 2. 调用关键函数:将 UClass 注册到全局的 UObject 系统中
    UObjectForceRegistration(Class); 
    
    // 3. 【关键步骤】并不是直接完全初始化,而是将这类 UClass 对象添加到一个“延迟注册链表”中
    //    (您提到的链表,即 DeferredCompiledInRegistration 链表)
    AddToDeferredRegistrationList(Class);
}

注意:此时只是把UClass对象放入了另一个链表,并没有完全初始化它。因为它的父类UClass可能还没有被注册和初始化。

3.处理依赖,有序初始化

现在,所有已知的UClass都被放到了一个全局的“延迟注册链表”里。接下来,ProcessNewlyLoadedUObjects()会进入最精妙的环节:清空这个链表

它会对链表中的UClass对象进行排序和初始化。如何解决依赖?

  • 它首先会确保一个类的父类它依赖的类(例如类属性指定的UPROPERTY类型所在的类)先被完全初始化。

  • 对于AMyCharacter,系统会发现它的父类是ACharacter。它会检查:

    • 如果ACharacterUClass已经初始化,太好了,直接初始化AMyCharacterUClass

    • 如果ACharacterUClass还没初始化(比如ACharacter在另一个还没加载的Engine.dll里),那么AMyCharacter的初始化就会被跳过,留在链表里,等待下一次机会。

4.动态加载与循环

  1. 引擎继续运行,现在需要加载Engine.dll
  2. Engine.dll被加载到内存中,它的静态变量自动初始化,将其内部的类(包括ACharacter)的信息添加到它自己的DeferredCompiledInRegistration数组中。
  3. 引擎再次调用ProcessNewlyLoadedUObjects()
  4. 这次调用会处理所有模块(包括刚加载的Engine.dll)的“待办事项列表”。它为ACharacter创建UClass,并将其加入“延迟注册链表”。
  5. 在尝试清空链表时,系统发现:
  • ACharacter的父类(APawn)可能也需要检查,但假设其父类已就绪
  • 现在ACharacterUClass可以被成功初始化。
  • 更重要的是,之前被跳过的AMyCharacter,它的依赖(父类ACharacter)现在已经可用了!
  • 于是,系统现在可以顺利地初始化AMyCharacterUClass了。

这个过程会不断重复:
加载DLL -> 调用ProcessNewlyLoadedUObjects -> 填充链表 -> 尝试清空链表(解决依赖)-> 剩余未解决的留在链表中等待下次循环。

总结与比喻

你可以把整个系统想象成一个不断扩大的拼图游戏

  • 静态收集:每个模块(DLL)就像一个袋子,里面装着一堆无序的拼图碎片(类信息)。每个袋子自己不知道整个图画的全貌。

  • 动态注册

    1. 打开袋子:每当一个DLL被加(ProcessNewlyLoadedUObjects),就把这个袋子里的所有碎片倒在一张大桌子(全局链表)上。

    2. 拼图:系统尝试从桌上的碎片中拼出完整的图。规则是:边缘碎片(父类、依赖类)必须先拼好,才能拼中间的碎片(子类)

    3. 等待:如果发现某块碎片(如AMyCharacter)缺了它依赖的边缘碎片(ACharacter),就把它暂时放回桌上不动。

    4. 新袋子:当新的DLL(如Engine.dll)被加载,就等于又打开了一个新袋子,倒出了新的碎片(如ACharacter)。

    5. 继续拼:现在有了新的碎片,之前无法完成的拼图现在可以继续了。

    6. 重复这个过程,直到所有袋子都打开,所有拼图都完成。

这种设计的精妙之处在于:

  • 解耦:每个模块只关心自己的信息收集,不知道其他模块的存在和加载顺序。
  • 弹性:无论DLL以何种顺序动态加载,系统都能通过多次尝试,最终解决所有依赖关系。
  • 支持热重载:重新加载一个DLL时,只需重复“打开袋子倒碎片 -> 拼图”的过程,系统会自动处理更新。

网站公告

今日签到

点亮在社区的每一天
去签到