[原创]UE —UObject类智能指针

上文谈到C++ 标准智能指针,是为了让程序员从指针的 new 和 delete 中抽身,更专注逻辑本身的开发。UE4 针对非 UObject 对象也实现了一套类似的智能指针,至于具体实现原理,可查看知乎文章:UE4的智能指针 TSharedPtr

强如 Java 和 C# 等更“智能化”的高端语言已经实现了垃圾回收机制,恐怖如斯;UE虽身披 C++ 却心向 C# ,自己也捣鼓了一个低端版本的垃圾回收,针对UObject类型对象的智能指针,主要有:TWeakObjectPtr,TStrongObjectPtr

WeakObjectPtr

我们知道继承了 UObject,并且被标记了 UPROERTY 的对象 Obj ,会被 GC 追踪(会被GC追踪的对象还有其它情况,本文暂不讨论);因此我们在使用 Obj 之前一般都会有如下代码安全开道:

if (IsValid(Obj))
{
    //Use Obj do something
}

GC 的删除过程是分帧处理,但是会提前标记 PendingKill,所以 IsValid(Obj) 依然返回 false,确保我们的代码不会出问题。但是,如果我们的代码是在 GC 清理完全以后再调用呢?上面这段代码就可能会导致 Crash ,为什么呢?

//Object.h
FORCEINLINE bool IsValid(const UObject *Test)
{
    return Test && !Test->IsPendingKill();
}

//UObjectBaseUtility.h
FORCEINLINE bool IsPendingKill() const
{
    return GUObjectArray.IndexToObject(InternalIndex)->IsPendingKill();
}

Obj 指向的 UObject 对象被初始化出来后,会被插入到数组 GUObjectArray,位置就是 InternalIndex 。GC后,GUObjectArray 数组内 InternalIndex 的位置被放入新的 UObject 对象,因此 IsValid ( Obj ) 返回的结果可能是 true;因此IsValid 判断 Obj 指针有效,而实际上 Obj 指针指向的内存地址里的内容解引用会失败,导致Crash

因此,我们希望有一种新型的智能指针Smart_Ptr用来包装 裸指针Obj,它必须满足以下两点要求:

  • Smart_Ptr 不能影响 Obj 的 GC
  • Obj 所指向的对象被 GC 删除以后,IsValid(Smart_Ptr) 会百分百返回 :false

UE很巧妙的撸了个 WeakObjectPtr ,实现原理很简单,它的核心就两个参数:

//WeakObjectPtr.h
int32	ObjectIndex;
int32	ObjectSerialNumber;

//WeakObjectPtr.cpp
ObjectIndex = GUObjectArray.ObjectToIndex((UObjectBase*)Object);
ObjectSerialNumber = GUObjectArray.AllocateSerialNumber(ObjectIndex);

ObjectIndex 就是当前WeakObjectPtr 指向的对象在数组GUObjectArray内的索引,而ObjectSerialNumber 则是该对象当初在创建的时候的唯一序列码 SerialNumber (赋值和自增都是原子操作,线程安全);因为它不直接引用 Obj 指针 ,因此不会影响 GC,然后 ObjectSerialNumber 会跟 GUObjectArray 内 ObjectIndex 位置对象的 SerialNumber 对比,不一样就表示根本不是同一个对象,使用如下:

//Assignment
TWeakObjectPtr<UMyObject> WeakObj = Obj;

//use
if(WeakObj .Get())
{
    UMyObject* Obj = Cast<UMyObject>(WeakObj);
    //Use Obj do something
}

因此,对于一些会隔帧执行的代码(定时器回调、 委托等),如果有用到 UObject 对象,强烈建议使用 WeakObjectPtr,能减少很多隐藏的Crash,让你的代码更乐百氏( Robust )

StrongObjectPtr

相对 WeakObjectPtr 来说, StrongObjectPtr 用的并不多。它提供的是一种绝对的把控,被他包装的 UObject 类型对象(不标记 UPROPERTY ),不会被 GC 自动清理,只有 StrongObjectPtr 析构以后,它包装的 UObject 才会被下一次 GC 清理

因此实现这种指针封装,需要满足以下两点:

  • 持有该对象,并且保证它不被 GC
  • 告诉GC,我用完了你可以清理它

我们查看 StrongObjectPtr 的实现,关键就一个变量:ReferenceCollector,类型是 FInternalReferenceCollector,继承自 FGCObject 。( ReferenceCollector 是 UniquePtr类型指针,因此它不能复制,只能转移所有权(OwnerShip))

为什么继承了 FGCObject 就可以被 GC 追踪 ?我们先看它的实现(注意:为了代码简短,去除了部分内容)

//GCObject.h
class COREUOBJECT_API FGCObject
{
   static UGCObjectReferencer* GGCObjectReferencer;
   static void StaticInit(void)
   {
      if (GGCObjectReferencer == NULL)
      {
         GGCObjectReferencer = NewObject<UGCObjectReferencer>();
         GGCObjectReferencer->AddToRoot();
      }
   }

   FGCObject(void)
   {
       StaticInit();
       GGCObjectReferencer->AddObject(this); 
   }

   virtual ~FGCObject(void)
   {
       GGCObjectReferencer->RemoveObject(this);
   }

   virtual void AddReferencedObjects( FReferenceCollector& Collector ) = 0;
};

静态指针 GGCObjectReferencer,用来管理所有继承了 GFGCObject 的指针对象;初始化执行如下两个方法

  • StaticInit() 内 AddToRoot;表示将 GGCObjectReferencer 设置为根节点,因此它永远不会被 GC 清理(除非调用 RemoveFromRoot)
  • AddObject(this) ,则是将自身放入 GGCObjectReferencer 内的ReferencedObjects 数组内

每一次GC的引用标记阶段,执行到 GGCObjectReferencer 指向的对象时,会执行它的 AddReferencedObjects 方法,而它会遍历调用 ReferencedObjects 数组内的所有 FGCObject 对象的 AddReferencedObjects 方法

//GCObjectReferencer.cpp
void UGCObjectReferencer::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector)
{	
    UGCObjectReferencer* This = CastChecked<UGCObjectReferencer>(InThis);
    for (FGCObject* Object : This->ReferencedObjects)
    {
	Object->AddReferencedObjects(Collector);
    }
}

我们来看看指针 ReferenceCollector 内的 AddReferencedObjects 方法:

//StrongObjectPtr.h
virtual void AddReferencedObjects(FReferenceCollector& Collector) override
{
   Collector.AddReferencedObject(Object);
}

Collector.AddReferencedObject( Object) 就是将 StrongObjectPtr 所修饰的 UObject 指针对象放入 GC 体系(被 Collector 引用,因此不会被 GC )

以上,我们解释了第一条:持有该对象,并且保证它不被 GC ;那怎么告诉 GC,我现在用完了,可以清理呢?

上面的 ~FGCObject 析构函数已经给到答案了,就是将 ReferenceCollector 从 GGCObjectReferencer 移除。那么下次 GC 引用标记阶段执行到 GGCObjectReferencer 指向的对象时,它的 ReferencedObjects内就没有我们的 ReferenceCollector,那么它的 AddReferencedObjects 方法就不会被执行到; 那么 ReferenceCollector 内的UObject* Object 就没人引用,那么 GC 就会清理它

其实,大部分情况下,都用不到 StrongObjectPtr ,因为 UPROPERTY 标记足够好用。然而它可以在任何地方使用,包括F类内,这也是它的优势

参考引用: