文章目录
核心目标
UE需要实现一个跨模块(DLL)、支持热重载的反射系统。这意味着:
编译器需要知道所有UObject类的信息(类名、大小、属性、函数等)。
运行时需要按正确的顺序创建
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。它会检查:
如果ACharacter的UClass已经初始化,太好了,直接初始化AMyCharacter的UClass。
如果ACharacter的UClass还没初始化(比如ACharacter在另一个还没加载的Engine.dll里),那么AMyCharacter的初始化就会被跳过,留在链表里,等待下一次机会。
4.动态加载与循环
- 引擎继续运行,现在需要加载
Engine.dll
。 Engine.dll
被加载到内存中,它的静态变量自动初始化,将其内部的类(包括ACharacter
)的信息添加到它自己的DeferredCompiledInRegistration
数组中。- 引擎再次调用
ProcessNewlyLoadedUObjects()
。 - 这次调用会处理所有模块(包括刚加载的
Engine.dll
)的“待办事项列表”。它为ACharacter
创建UClass
,并将其加入“延迟注册链表”。 - 在尝试清空链表时,系统发现:
ACharacter
的父类(APawn
)可能也需要检查,但假设其父类已就绪- 现在
ACharacter
的UClass
可以被成功初始化。 - 更重要的是,之前被跳过的
AMyCharacter
,它的依赖(父类ACharacter
)现在已经可用了! - 于是,系统现在可以顺利地初始化
AMyCharacter
的UClass
了。
这个过程会不断重复:
加载DLL -> 调用ProcessNewlyLoadedUObjects -> 填充链表 -> 尝试清空链表(解决依赖)-> 剩余未解决的留在链表中等待下次循环。
总结与比喻
你可以把整个系统想象成一个不断扩大的拼图游戏:
静态收集:每个模块(DLL)就像一个袋子,里面装着一堆无序的拼图碎片(类信息)。每个袋子自己不知道整个图画的全貌。
动态注册:
打开袋子:每当一个DLL被加(
ProcessNewlyLoadedUObjects
),就把这个袋子里的所有碎片倒在一张大桌子(全局链表)上。拼图:系统尝试从桌上的碎片中拼出完整的图。规则是:边缘碎片(父类、依赖类)必须先拼好,才能拼中间的碎片(子类)。
等待:如果发现某块碎片(如
AMyCharacter
)缺了它依赖的边缘碎片(ACharacter
),就把它暂时放回桌上不动。新袋子:当新的DLL(如
Engine.dll
)被加载,就等于又打开了一个新袋子,倒出了新的碎片(如ACharacter)。继续拼:现在有了新的碎片,之前无法完成的拼图现在可以继续了。
重复这个过程,直到所有袋子都打开,所有拼图都完成。
这种设计的精妙之处在于:
- 解耦:每个模块只关心自己的信息收集,不知道其他模块的存在和加载顺序。
- 弹性:无论DLL以何种顺序动态加载,系统都能通过多次尝试,最终解决所有依赖关系。
- 支持热重载:重新加载一个DLL时,只需重复“打开袋子倒碎片 -> 拼图”的过程,系统会自动处理更新。