在 Unreal Engine 开发中,一个常见的坑是:自定义的普通 C++ 类(非 UObject 派生类)持有了 UObject 指针,但 GC 根本不知道这件事。结果就是,你以为还“持有”着的对象,可能在某个 GC 周期后被悄悄回收,留下一个悬空指针。
UE 为这个问题准备的答案就是 FGCObject。本文将从原理出发,解释它如何工作,以及如何在你的项目中正确使用它。
一、问题背景:GC 只认识 UObject
UE 的垃圾回收器采用标记-清扫(Mark-and-Sweep) 算法。它的工作流程大致是:
关键在于:GC 只追踪 UObject 之间的引用链。当你有一个普通的 C++ 结构体或类,里面存了 UObject* 指针时,GC 对此一无所知。这个指针对 GC 而言是“不存在”的。
传统解决方案是手动调用 AddToRoot() 和 RemoveFromRoot()。但正如你在开发中可能已经体会到的,这种方式有两个致命问题:
- 时机不可控:如果程序退出时析构顺序晚于 UObject 系统清理,
RemoveFromRoot()会访问已失效的指针,直接崩溃 - 容易遗漏:每个 Add 都要对应一个 Remove,手动管理容易出错
二、FGCObject 的设计思路
FGCObject 是 UE 为“非 UObject 类需要安全持有 UObject 引用”场景提供的基类。它的核心机制可以概括为一句话:
FGCObject 把自己注册到一个全局的 UObject(UGCObjectReferencer)上,让这个 UObject 成为“中间人”,在 GC 运行时替非 UObject 类申报引用
具体来说,当你继承 FGCObject 并构造实例时:
FGCObject的构造函数会调用StaticInit(),确保全局唯一的UGCObjectReferencer存在,并将其加入 Root Set- 你的
FGCObject实例把自己注册到UGCObjectReferencer的内部列表中 UGCObjectReferencer本身是 Root Set 中的 UObject,所以 GC 每次都会扫描它的引用- GC 扫描到
UGCObjectReferencer时,会调用它的AddReferencedObjects,进而遍历所有已注册的 FGCObject,调用它们各自的AddReferencedObjects
这样,你的非 UObject 类就“借道” UGCObjectReferencer 进入了 GC 的视野。
三、正确使用方式
3.1 继承并实现纯虚函数
FGCObject 是一个抽象基类,必须实现 AddReferencedObjects:
cpp
#include "UObject/GCObject.h"
class FMyNonUObjectClass : public FGCObject
{
public:
UObject* MyObject;
FMyNonUObjectClass(UObject* InObject) : MyObject(InObject) {}
virtual void AddReferencedObjects(FReferenceCollector& Collector) override
{
Collector.AddReferencedObject(MyObject);
}
virtual FString GetReferencerName() const override
{
return TEXT("FMyNonUObjectClass");
}
};
FReferenceCollector 是 GC 提供的收集器,你通过它“申报”持有的 UObject 引用。GetReferencerName() 不是必须的,但强烈建议实现——调试 GC 问题时,这个名字会出现在引用链中,帮你快速定位是谁在持有对象。
3.2 处理容器中的 UObject
如果你的非 UObject 类持有的是 TMap、TArray 等 UE 容器,FReferenceCollector 提供了专门的重载:
cpp
virtual void AddReferencedObjects(FReferenceCollector& Collector) override
{
Collector.AddReferencedObjects(MyMap, this, nullptr);
}
第二个参数 this 是 ReferencingObject(用于调试),第三个参数在非 UObject 场景传 nullptr。这个重载会遍历容器内所有 UObject 指针并登记。
3.3 关键注意事项
不要用值语义存储 FGCObject。官方文档明确指出:FGCObject 不是 trivially relocatable 的类型,不能按值放入 TArray、TMap 等容器中。原因在于它的注册机制依赖于对象地址的稳定性。作为类的成员变量或独立对象使用是安全的。
AddReferencedObjects 是“申报”而非“管理”。你不需要在析构时手动清理引用。FGCObject 的析构函数会自动从 UGCObjectReferencer 中注销自己。下次 GC 运行时,如果你的对象已经没了,它自然就不会再出现在申报列表里,那些 UObject 如果没有其他引用,就会被正常回收。这从根本上避免了析构时机导致的崩溃问题。
四、GC 何时调用 AddReferencedObjects?
这是一个常见的误解点:AddReferencedObjects 不是定时器回调,也不是轮询。
它的调用时机完全由 GC 的标记阶段决定。每当引擎执行一次垃圾回收(可能由关卡加载、内存压力、手动触发等事件引起),在“可达性分析”阶段,GC 会从 Root Set 开始扫描。当扫描到 UGCObjectReferencer 时,就会遍历所有注册的 FGCObject,逐个调用 AddReferencedObjects。
所以准确的说法是:每次 GC 运行时,你的 AddReferencedObjects 会被调用一次。调用频率取决于 GC 跑得多勤,而不是墙上的时钟。
这也意味着:你随时可以往自己的容器里增删 UObject 指针,GC 不关心你中间做了什么。它只关心在它下次来问的时候,你申报的当前名单是否准确。申报了的就保住,没申报的就可以回收。
五、与 AddToRoot/RemoveFromRoot 的对比
| 维度 | AddToRoot/RemoveFromRoot | FGCObject |
|---|---|---|
| 管理粒度 | 单个 UObject | 类级别,所有引用统一申报 |
| 析构安全性 | 差,时机不可控易崩溃 | 好,注销与申报分离 |
| 引用状态 | 永久 Root(直到手动移除) | 动态,每次 GC 时重新评估 |
| 内存效率 | 较低,对象一直 Root 直到手动清理 | 较高,引用消失后下轮 GC 即可回收 |
| 适用场景 | 简单、临时的场景 | 任何需要长期稳定持有 UObject 引用的非 UObject 类 |
FGCObject 的核心优势在于把“申报引用”和“生命周期管理”解耦了。你不需要在析构时做任何 UObject 操作,也就不存在“析构晚于 GC 系统销毁”的问题。
六、总结
FGCObject 是 UE 为 C++ 开发者提供的一个桥梁,让非 UObject 类能够以符合 GC 规则的方式持有 UObject 引用。它的工作流程可以概括为:
- 注册:FGCObject 构造时把自己挂到全局的
UGCObjectReferencer上 - 申报:每次 GC 标记阶段,
UGCObjectReferencer会遍历所有 FGCObject,调用其AddReferencedObjects - 注销:FGCObject 析构时自动从
UGCObjectReferencer移除,下次 GC 不再申报
如果你在开发中遇到过“自定义类持有的 UObject 莫名被回收”或者“程序退出时 RemoveFromRoot 崩溃”的问题,FGCObject 就是正解。
