前言
反射系统
我理解的反射系统:运行时可得到对象的类型信息(包括:属性,方法)、动态创建对象等
UE采用的是将对象的类、属性、方法用关键字(UCLASS,UPROPERTY,UFUNCTIOIN)标记;
编译之前启动UHT去扫描这些关键字,生成/刷新该对象的UClass"伴随对象",UClass保存了反射相关的信息,描述了各个属性之间的内存布局,通过UObject对象实例就可以将相应的属性的值取出来,进行读/写操作。
垃圾回收功能
如果让你来实现这个垃圾回收系统,你觉得垃圾回收系统应该具备哪些功能呢?
- 实现自定义的NewObject方法(无需调用对应的DeleteObject)
- 自动回收没有被引用的垃圾对象
- 不影响其他系统的正常功能
垃圾回收伪实现
垃圾回收的算法有很多, 标记-清除算法 、 复制算法 、标记-整理算法 、分代收集算法等。我们就用标记清除算法来实现。标记-清除算法,看名字就知道有两个阶段,标记和清除:
- 标记:遍历所有对象,根据某种规则,标记其是否需要清除
- 清除:遍历所有对象,清除标记了的对象,回收内存
因此可知,要实现标记清除垃圾回收,在标记阶段我们需要做到以下两点:
- 能拿到所有对象
- 确定对象清除的规则
在自定义的NewObject方法内,把生成的对象指针放入全局数组GUObjectArray,这样我们就可以拿到所有对象了
想象一个画面:空场景内站着一个英雄。这个情况下,垃圾回收系统是不是应该围绕着英雄来判断?英雄用到的对象就保留,没用到的对象就清除。此时这个英雄就是“根对象”。因此,标记清除的规则就是,根对象用到的对象保留,其他对象清除。那根对象怎么确定?就得我们“手动”标记(AddRoot)
UE垃圾回收实现
UE的垃圾回收过程,正是照着上文的伪实现一步步执行的
UE GC核心算法
UE4垃圾回收的核心是使用了标记-清理算法,标记指的是它以所有在ROOT上的UObject列表为根,去递归遍历所有这些根UObject的引用链,所有能访问到的UObject就标记为可达的。
清理阶段会在标记完成后,收集所有这些UnReachabale UObjects,对其进行清理回收。
标记流程在一帧内完成,而清理阶段可以分为多个帧,时间切片方式完成,所以GC机制的性能点在于标记阶段,如果有太多的UObject对象,那么游戏GC时会有明显的卡顿,所以需要控制内存中UObject数量。
纳入被自动GC机制管理的条件:
- UObject对象中的UObject*成员变量必须被UPROPERTY宏定义修饰,自动被加入到引用。
- UObject对象实现了派生的AddReferencedObjects接口,手动将UObject*变量添加到引用。
- 对于Struct结构体中引用的UObject,必须继承FGCObject对象,并实现派生的AddReferencedObjects接口,手动将UObject*变量加入引用。
一、GC的准备:
1.1 初始化全局数组GUObjectArray
- 需要获取所有的对象:把生成的对象的指针放入一个全局数组GUObjectArray,每创建一个对象时,为这个对象分配一个InternalIndex,GUObjectArray存放的是结构体FUObjectItem。
FUObjectItem结果如下:
//UObjectArray.h
//对象存储的结构体,GC操作的就是这个对象
struct FUObjectItem
{
class UObjectBase* Object; //对象
int32 Flags; //EInternalObjectFlags标识
int32 ClusterRootIndex; //当前所属簇索引
int32 SerialNumber; //对象序列码(WeakObjectPtr实现用到它)
}
- 核心源码解析
(完整代码参考自 UnrealEngine/Engine/Source/Runtime/CoreUObject/Public/UObject/UObjectArray.h 和 .cpp)
- 对象索引分配 - AllocateUObjectIndex 函数
int32 FUObjectArray::AllocateUObjectIndex(UObjectBase* Object)
{
// 加锁保证线程安全
FScopeLock Lock(&GUObjectArrayLock);
// 分配新索引
const int32 Index = ObjObjects.Add(Object);
Object->InternalIndex = Index;
// 维护对象列表
ObjAvailableList.RemoveAt(Index);
ObjObjects[Index] = Object;
return Index;
}
- UObjectBase 构造函数 - 自动注册到全局数组
UObjectBase::UObjectBase(UObject* InOuter, FName InName, EObjectFlags InFlags)
{
// 分配对象索引(核心注册逻辑)
InternalIndex = GUObjectArray.AllocateUObjectIndex(this);
// 初始化其他属性
Outer = InOuter;
Name = InName;
Flags = InFlags;
// 将对象加入全局表
GUObjectArray.IndexToObject(InternalIndex)->SetFlags(InFlags);
}
1.2 获取Obj对象所引用的其他UObject对象
获取Obj对象所引用的其他UObject对象(用于标记时执行),伪代码如下:
//遍历Obj对象的所有属性,如果属性的类型是UObjectle类型,就取消不可达标签
for(TFieldIterator<FProperty> PropertyIter(Obj->GetClass());
PropertyIter; ++PropertyIter)
{
const FProperty* PropertyIns = *PropertyIter;
//该属性是否是UObject,是否是TArray<UObject>等
//然后依次取消 "不可达"标记
//然后递归遍历该属性对象所引用的对象
}
这么实现,确实可以;但是UE并没有这么做,为什么呢?因为属性内其实大部分非UObject类型(比如:int32,bool等),全部遍历效率太低。因此在生成UObject对应UClass对象的时候,就构造了一个新的概念,将所引用的其他UObject对象用一个很巧妙的整数,存入ReferenceTokenStream遍历的Tokens 数组内。
UE会根据反射的信息,将对象的引用信息保存在对应UClass内的ReferenceTokenStream内;
ReferenceTokenStream结构内只有2个数组,起作用的是Tokens (TokenDebugInfo 是调试信息),Tokens 内的数字的结构如下:
struct FGCReferenceInfo
{
union
{
struct
{
uint32 ReturnCount : 8;
uint32 Type : 5;
uint32 Offset : 19;
};
//ReturnCount + Type + Offset
//00000000 + 00000 + 0000000000000000000
uint32 Value;
};
};
1.2.1 ReferenceTokenStream 的作用
ReferenceTokenStream 是 UE 用于记录对象引用关系的二进制数据流,每个 UObject 在序列化(Serialize)时会生成该数据流。其本质是一个紧凑的二进制指令集,记录了:
- 引用的 UObject 属性偏移量
- 数组/容器中引用的 UObject
- 弱引用(Weak Reference)的特殊标记
- 引用类型(Persistent/Non-Persistent)
1.2.2 生成时机
引用信息在 对象首次加载或动态创建时 通过 UObject::Serialize 函数生成:
// Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp
void UObject::Serialize(FArchive& Ar)
{
// 序列化时生成引用信息
GetClass()->SerializeTaggedProperties(Ar, (uint8*)this, GetClass(), nullptr);
}
二、GC流程
2.1 GC启动
- 手动调用:UWorld::ForceGarbageCollection( bool bFullPurge),他会在World.tick的下一帧强行进行垃圾回收。
- 自动调用:系统会根据默认的设置(可重新配置)一定的间隔时间或者条件下,自动调用垃圾回收。
2.2 GC入口
入口阶段:
void CollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge)
{
if (GIsInitialLoad)
{
// During initial load classes may not yet have their GC token streams assembled
UE_LOG(LogGarbage, Log, TEXT("Skipping CollectGarbage() call during initial load. It's not safe."));
return;
}
// No other thread may be performing UObject operations while we're running
AcquireGCLock();
// Perform actual garbage collection
UE::GC::CollectGarbageInternal(KeepFlags, bPerformFullPurge);
if (GGCLockBehavior == FGCLockBehavior::Legacy)
{
// Release the GC lock to allow async loading and other threads to perform UObject operations under the FGCScopeGuard.
ReleaseGCLock();
}