首页 > 其他分享 >Unreal 浅谈TWeakObjectPtr

Unreal 浅谈TWeakObjectPtr

时间:2024-05-31 15:21:49浏览次数:12  
标签:FWeakObjectPtr UObject const 浅谈 ObjectSerialNumber Unreal bool TWeakObjectPtr

【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!


前言

在Unreal的开发过程中,正确的引用和管理UObject是十分重要的,尤其Unreal有着它自己的UObject的GC机制,这使得对UObject的有效引用和管理变得尤为关键。不正确的引用UObject将造成循环引用或者影响该UObject的生命周期,这都是不希望看到的。所以Unreal通过TWeakObjectPtr提供一种弱引用来解决这个问题,在本篇文章中将详细探讨TWeakObjectPtr的工作原理。

TWeakObjectPtr是什么?

TWeakObjectPtr是UE4中的一个C++模板类,用于存储对UObject以及其派生类的弱引用。在UE4中UObject是所有游戏对象的基类,包括角色、场景组件、蓝图等,并且UE提供了一套GC机制来管理UObject的生命周期。弱引用意味着TWeakObjectPtr不会阻止其引用的UObject被GC销毁。这在某些情况下是非常有用的,比如当你需要访问一个UObject时,但不希望因为你的对UObject引用而导致永远不会被销毁。

示例代码如下所示,这里引用一个AActor*指针并且使用UPROPERTY将其纳入到UE的GC机制,这种写法相当对该AActor就添加了一次引用,意味着该Actor将不会被GC。当然如果使用TWeakObjectPtr来包装对应AActor,这样可以使用该AActor,但是并不会阻止其GC。

UPROPERTY()
AActor* A;

TWeakObjectPtr<AActor> A;
A = GetXXXActor();

  

当然既然TWeakObjectPtr不会影响对应UObject的GC流程,那么使用时也需要注意其有效性。使用前当然需要检验当前UObjecct的可用性,TWeakObjectPtr提供了IsValid方法用于判断引用的UObject是否可用,Get方法则会返回一个UObject指针。具体代码如下所示:

AMyActor* Actor
TWeakObjectPtr<AMyActor> ActorReference = Actor;
.......

if (ActorReference.IsValid())
{
    // 使用Get()方法获取对象指针
    AMyActor* ValidActor = ActorReference.Get();
    // 在有效的Actor上执行操作
    ValidActor->SomeMethod();
}
else
{
    // TWeakObjectPtr无效,可能是因为对象已被销毁
    UE_LOG(LogTemp, Warning, TEXT("Actor reference is invalid!"));
}

  

TWeakObjectPtr实现

接下来看看TWeakObjectPtr的实现。TWeakObjectPtr的声明如下所示:

template<class T=UObject, class TWeakObjectPtrBase=FWeakObjectPtr>
struct TWeakObjectPtr;

template<class T, class TWeakObjectPtrBase>
struct TWeakObjectPtr : private TWeakObjectPtrBase

  

可以看出TWeakObjectPtr是一个模板类,它继承自TWeakObjectPtrBase(一般默认为FWeakObjectPtr)。TWeakObjectPtr的模板参数T表示要引用的UObject派生类的类型,而模板参数TWeakObjectPtrBase表示实际弱引用实现的类型,也就是说TWeakObjectPtr实际上是FWeakObjectPtr的模版化包装。在Unreal中有很多的例子,当你看到TXXX的模板类时,应该想到后面有一个FXXX类在默默支持它。首先来看看隐藏在幕后的FWeakObjectPtr吧!

需要注意的一点,Unreal的实现使用的是Private继承。可能平时开发中使用Private继承的场景相对较少,但是对于各种基础库中使用Private继承还是不少的。略微揣摩一下Unreal使用Private继承的意图所在,先列一下Private继承可能会发挥到的作用:

  1. Private继承表面是继承,其实本质是一种更加简便达到组合的方式,子类和父类并不是Is-a的关系。完全避免类型兼容原则(子类可被认为是父类),可显示阻止不合理的类型转化(编译期暴露问题)。
  2. 使用继承便可以利用上空基类优化(EBO)以减少内存占用。
  3. 不希望派生类的子类直接使用基类的任何变量和方法(可以用using改变可见性)。
  4. 派生类可以覆盖基类的虚函数,但是不将这些函数暴露给外部。

第1、2点应该是Unreal使用Private继承来实现的原因,首先是符合语义,本质上TWeakObjectPtr是FWeakObjectPtr的一层模板包装,这也就是一种组合的实践方式;其二就是能够享受空基类优化来减少内存占用。对于第3、4点来说,一是FWeakObjectPtr并没有虚函数,二是一般来说正常开发流程中不会再去继承TWeakObjectPtr了。所以在这里第3、4点效果并无体现。

接下来看看FWeakObjectPtr的具体实现。具体的文件路径:\Engine\Source\Runtime\CoreUObject\Public\UObject\WeakObjectPtr.h

先列出一些重要的代码,更多细节请看源码:

struct FWeakObjectPtr
{
public: 
    FORCEINLINE FWeakObjectPtr()
    {
        Reset();
    }
    FORCEINLINE void Reset()
    {
        ObjectIndex = INDEX_NONE;
        ObjectSerialNumber = 0;
    }

    FWeakObjectPtr(const FWeakObjectPtr& Other) = default;

    FWeakObjectPtr& operator=(const FWeakObjectPtr& Other) = default;

    COREUOBJECT_API class UObject* Get(bool bEvenIfPendingKill) const;

    COREUOBJECT_API class UObject* Get(/*bool bEvenIfPendingKill = false*/) const;

    COREUOBJECT_API bool IsValid(bool bEvenIfPendingKill, bool bThreadsafeTest = false) const;

    COREUOBJECT_API bool IsValid(/*bool bEvenIfPendingKill = false, bool bThreadsafeTest = false*/) const;
private:
    FORCEINLINE FUObjectItem* Internal_GetObjectItem() const;

    FORCEINLINE_DEBUGGABLE bool Internal_IsValid(bool bEvenIfPendingKill, bool bThreadsafeTest) const;

    FORCEINLINE_DEBUGGABLE UObject* Internal_Get(bool bEvenIfPendingKill) const;

    int32       ObjectIndex;
    int32       ObjectSerialNumber;
}

  

Unreal的UObject管理
这里需要额外关注的就是ObjectIndex和ObjectSerialNumber字段,你可能会疑惑为什么只需要两个int32类型就可以了,甚至在FWeakObjectPtr中都不需要存储任何的UObject。这和Unreal对于UObject管理有关。首先在Unreal中所有的UObject对象都会放到FUObjectArray来管理,在UObjectBase构造时会调用AddObject,再将AddObject调用AllcateUObjectIndex函数的UObject放到一个FUObjectArray对象中,如下所示:

 

并且在AllocateUObjectIndex中会构造一个FUObjectItem对象并会将FUObjectItem和UObject两者一一对应,同时广播UObjectCreate事件,如下图所示:

 

FUObjectItem对象会保存指向对应UObject的指针,UObject持有InternalIndex以便后续直接拿到对应的FUObjectItem。因为FUObjectItem持有UObject的指针(双向奔赴了),所以在UObject销毁时广播UObjectDelete事件,调用FreeObjectIndex后对应的FUObjectItem也会被销毁。

 

看到这里就可以知道Unreal并不直接管理具体的UObject,而是通过FUObjectItem去获取对应的UObject,并且这个FUObjectItem相当的轻量,所以在FUObjectArray持有一个FChunkFixedUObjectArray来储存所有的FUObjectItem。它们之间的关系可以通过下面这张图来解释:

 

 

还有FUObjectHashTables也是管理全局UObject对象,但是它是维护UObject名字(Name)到对象指针的映射,父子(Outer)对象之间的映射等,方便对UObject进行查找等操作和FWeakObjectPtr实现关系不大,所以在这里就不展开了。

现在已经知道ObjectIndex是被用于获取指定的UObject,那么ObjectSerialNumber是用来干什么的呢?还得补充一个知识点,那就是ObjAvailableList其实在AllocateUObjectIndex的实现是有用到的。获取对应的Index时会优先从ObjAvailableList获取一个可用的索引,无需遍历寻找。如果ObjAvailableList没有,则FChunkFixedUObjectArray需要扩容。

 

当然在FreeObjectIndex调用时,会向ObjAvailableList放入新的索引。

 

这就代表InternalIndex是循环使用,这样索引缓存机制会带来一个问题,那就是Unreal只会保证在同一时刻的所有的UObject的InternalIndex是不同的,但是在不同的时间下,不同的UObject会持有相同的InternalIndex。这对于FWeakObjectPtr来说显然是有问题的,因为可能会有FWeakObjectPtr当前指向的UObject并不是当初的UObject,但是它们的InternalIndex还是相同的,所以在这里多加了一个ObjectSerialNumber来确保能找到唯一的UObject。

ObjectSerialNumber是一个递增的数字,每当通过UObject去创建一个FWeakObjectPtr时都会递增,也就是ObjectSerialNumber并不会重复(当然你可能会担心int32会不会爆掉,但是你得先创建21亿个FWeakObjectPtr,如果真出现这种问题,你应该优先考虑为什么会有21亿个FWeakObjectPtr,而不是担心它会爆掉),同时这里会对FUObjectItem中的SerialNumber做比较。具体代码如下所示:

 

也可以从FWeakObjectPtr的一些实现,看出ObjectSerialNumber的意义所在,比如==和!=的运算符重载函数,这里都对ObjectIndex和ObjectSerialNumber进行了双重验证,才能真正确认指向的UObject是当初哪个。

 

到这里ObjectIndex和ObjectSerialNumber字段的含义就解释完毕。

回到FWeakObjectPtr
可以继续看FWeakObjectPtr的一些重要的实现。首先来看一下针对UObject的赋值构造函数,这是通过FUObjectArray的ObjectToIndex函数获取到对应的Index,并且通过AllocateSerialNumber函数生成唯一的ObjectSerialNumber,最后还调用了SerialNumbersMatch函数以确保UObject唯一性。

FORCEINLINE_DEBUGGABLE bool SerialNumbersMatch() const
{
    checkSlow(ObjectSerialNumber > FUObjectArray::START_SERIAL_NUMBER && ObjectIndex >= 0); // otherwise this is a corrupted weak pointer
    int32 ActualSerialNumber = GUObjectArray.GetSerialNumber(ObjectIndex);
    checkSlow(!ActualSerialNumber || ActualSerialNumber >= ObjectSerialNumber); // serial numbers should never shrink
    return ActualSerialNumber == ObjectSerialNumber;
}
void FWeakObjectPtr::operator=(const class UObject *Object)
{
    if (Object // && UObjectInitialized() we might need this at some point, but it is a speed hit we would prefer to avoid
        )
    {
        ObjectIndex = GUObjectArray.ObjectToIndex((UObjectBase*)Object);
        ObjectSerialNumber = GUObjectArray.AllocateSerialNumber(ObjectIndex);
        checkSlow(SerialNumbersMatch());
    }
    else
    {
        Reset();
    }
}

  

接下来就是FWeakObjectPtr的一些常用方法的实现,比如Get和IsVaild的实现都是通过以下函数实现的。主要就是通过ObjectIndex和ObjectSerialNumber判断当前的FWeakObjectPtr是否可用的操作。当调用Get,如果ObjectItem可用则直接返回UObject指针否则返回nullptr。

FORCEINLINE FUObjectItem* Internal_GetObjectItem() const
{
    if (ObjectSerialNumber == 0)
    {
        checkSlow(ObjectIndex == 0 || ObjectIndex == -1); // otherwise this is a corrupted weak pointer
        return nullptr;
    }
    if (ObjectIndex < 0)
    {
        return nullptr;
    }
    FUObjectItem* const ObjectItem = GUObjectArray.IndexToObject(ObjectIndex);
    if (!ObjectItem)
    {
        return nullptr;
    }
    if (!SerialNumbersMatch(ObjectItem))
    {
        return nullptr;
    }
    return ObjectItem;
}

FORCEINLINE_DEBUGGABLE bool Internal_IsValid(bool bEvenIfPendingKill, bool bThreadsafeTest) const
{
    FUObjectItem* const ObjectItem = Internal_GetObjectItem();
    if (bThreadsafeTest)
    {
        return (ObjectItem != nullptr);
    }
    else
    {
        return (ObjectItem != nullptr) && GUObjectArray.IsValid(ObjectItem, bEvenIfPendingKill);
    }
}

FORCEINLINE_DEBUGGABLE UObject* Internal_Get(bool bEvenIfPendingKill) const
{
    FUObjectItem* const ObjectItem = Internal_GetObjectItem();
    return ((ObjectItem != nullptr) && GUObjectArray.IsValid(ObjectItem, bEvenIfPendingKill)) ? (UObject*)ObjectItem->Object : nullptr;
}

  

FWeakObjectPtr的解读就到这里。

看向TWeakObjectPtr
现在来看看TWeakObjectPtr是如何做好一个模板包装器的作用的。Get和IsVaild函数基本都是使用FWeakObjectPtr的实现,这里就不重复提及了。来看看一些不一样的操作,比如是TWeakObjectPtr和拷贝构造和赋值构造,如下所示:

template <typename OtherT,typename = decltype(ImplicitConv<T*>((OtherT*)nullptr))>
FORCEINLINE TWeakObjectPtr(const TWeakObjectPtr<OtherT, TWeakObjectPtrBase>& Other) :
    TWeakObjectPtrBase(*(TWeakObjectPtrBase*)&Other) // we do a C-style cast to private base here to avoid clang 3.6.0 compilation problems with friend declarations
{
}

template <typename OtherT,typename = decltype(ImplicitConv<T*>((OtherT*)nullptr))>
FORCEINLINE TWeakObjectPtr& operator=(const TWeakObjectPtr<OtherT, TWeakObjectPtrBase>& Other)
{
    *(TWeakObjectPtrBase*)this = *(TWeakObjectPtrBase*)&Other; // we do a C-style cast to private base here to avoid clang 3.6.0 compilation problems with friend declarations
    return *this;
}

  

这里使用decltype(ImplicitConv((OtherT*)nullptr))来做了一个是否能够隐式转换的检测,如果不可以隐式转换则直接在编译期报错。确保这里传入的类型都是兼容的,随后就是对TWeakObjectPtrBase初始化的操作。

接下来看到TWeakObjectPtr另外一个赋值构造函数,具体实现如下所示:

template<class U>
FORCEINLINE typename TEnableIf<!TLosesQualifiersFromTo<U, T>::Value, TWeakObjectPtr&>::Type operator=(U* Object)
{
    T* TempObject = Object;
    TWeakObjectPtrBase::operator=(TempObject);
    return *this;
}

  

这里使用TEnableIf和TLosesQualifiersFromTo两个模板元类,TEnableIf实现如下所示,很简单就能理解其用途,TEnableIf的主要作用是根据给定的条件(模板参数Predicate)来启用或禁用某个类型。

template <bool Predicate, typename Result = void>
class TEnableIf;

template <typename Result>
class TEnableIf<true, Result>
{
public:
    using type = Result;
    using Type = Result;
};

template <typename Result>
class TEnableIf<false, Result>
{ };

  

但是TLosesQualifiersFromTo是由多个模板类组合而来,首先是TAreTypesEqual,如下所示,TAreTypesEqual用于比较两个类型是否相等。如果给定的两个类型相等,它的Value成员变量将被设置为true,否则为false。

template<typename A,typename B>
struct TAreTypesEqual;

template<typename,typename>
struct TAreTypesEqual
{
    enum { Value = false };
};

template<typename A>
struct TAreTypesEqual<A,A>
{
    enum { Value = true };
};

  

随后是TCopyQualifiersFromTo,它的作用就是将一个类型的const和volatile限定符复制到另一个类型上。

template <typename From, typename To> struct TCopyQualifiersFromTo                          { typedef                To Type; };
template <typename From, typename To> struct TCopyQualifiersFromTo<const          From, To> { typedef const          To Type; };
template <typename From, typename To> struct TCopyQualifiersFromTo<      volatile From, To> { typedef       volatile To Type; };
template <typename From, typename To> struct TCopyQualifiersFromTo<const volatile From, To> { typedef const volatile To Type; };

  

最后TLosesQualifiersFromTo的实现如下所示,它的主要作用是判断从一个类型From复制限定符到另一个类型To时,是否会丢失任何限定符。

template <typename From, typename To>
struct TLosesQualifiersFromTo
{
    enum { Value = !TAreTypesEqual<typename TCopyQualifiersFromTo<From, To>::Type, To>::Value };
};

  

那么重新回到TWeakObjectPtr的赋值构造函数,使用这些模板类的目的在于检查只有在U类型的限定符不会在赋值时丢失时,才可以进行赋值操作。关于TWeakObjectPtr中的其他的函数就不再赘述,若大家感兴趣可以翻翻源码!

总结

TWeakObjectPtr是一个平时在开发过程中出现频率很高的工具,通常对于一些不确定其生命周期的UObject对象都可以通过TWeakObjectPtr包装一层以避免循环引用问题,并且可以避免对其UObject生命周期有所影响,确保引用的对象在其生命周期内被正确管理。当然在搞清楚TWeakObjectPtr的实现中也学到了很多其他知识,比如UObject在Unreal中是怎么被管理,当然还有一些模板元编程的技巧。

References
WeakObjectPtr

UObject管理篇


这是侑虎科技第1598篇文章,感谢作者不知名书杯供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者主页:https://www.zhihu.com/people/lllzwj

再次感谢不知名书杯的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

标签:FWeakObjectPtr,UObject,const,浅谈,ObjectSerialNumber,Unreal,bool,TWeakObjectPtr
From: https://www.cnblogs.com/uwatech/p/18224640

相关文章

  • 【杂记-浅谈DDos攻击、浅析SYN Flood攻击、Dos及DDos攻击区别】
    一、DDosDistributedDenialofService分布式拒绝服务攻击什么是DDos攻击DDoS攻击是一种常见的网络攻击形,攻击者利用恶意程序对一个或多个目标发起攻击,企图通过大规模互联网流量耗尽攻击目标的网络资源,使目标系统无法进行网络连接、无法提供正常服务。DDoS攻击会给攻......
  • 线程概念浅谈
    1.为什么要有线程我们知道一个集成应用场景需要多个进程同时调度执行各自的功能,那么多进程的本质就是产生多个执行流,每个执行流执行不同的代码和功能,但是一个进程由PCB(task_struct)、进程地址空间、页表、文件描述符表等资源组成,是一个资源集合,创建的开销较大,那么为了满足用户的......
  • bet8链接:浅谈LKL对Linux和新的成果
    由bet8链接 вт989点сс编译,LinuxKernelLibrary(LKL)设计为Linux核心的移植版本,在目录arch/lkl中,约有3500行的程式码。LKL与应用程式连结,以运作于使用者空间,依赖由主机作业系统提供的一组主机端的功能,例如semaphore,POSIXThreads,malloc、计时器(timer)......
  • Cesium4Unreal - # 002 线图元绘制
    文章目录基础点绘制1思路2步骤2.1创建一个自定义组件2.2重写CreateSceneProxy方法2.3实现自定义的场景代理类2.4在场景代理类中实现绘制逻辑2.5使用自定义组件3代码实现3.1c++代码3.1.1自定义组件代码MyPrimitivePointComponent.hMyPri......
  • 浅谈C++函数
    目录一、函数的概念二、调用函数的两个前提三、函数传参的三种形式四、函数返回类型一、函数的概念函数是C++程序的基本模块,通常一个C++程序由一个或多个函数组成。函数可以完成用户指定的任务,一般分为库函数和用户自定义的函数。函数由函数头和函数体组成,函数头中包......
  • Java中继承与接口的区别(浅谈)
    在Java中,继承和接口是两种不同的方式来实现代码的复用和扩展。以下是它们之间的主要区别:1.继承:继承是面向对象编程中的一种方式,允许一个类继承另一个类的属性和方法。继承的主要目的是为了实现代码的复用,即子类可以继承父类的属性和方法,从而避免重复编写相同的代码。此外,继......
  • 浅谈一下C#和java的线程不同点
    C#和Java在线程处理方面有一些显著的区别,这些区别主要体现在线程的创建、管理和生命周期控制上。以下是一些主要的区别:线程的创建和管理Java:Java中线程的创建通常是通过继承Thread类或实现Runnable接口来实现的。Java提供了线程组(ThreadGroup)的概念,允许将线程组织在一起......
  • 浅谈C#中取消令牌CancellationTokenSource
    基础操作CancellationTokenSourcecancellationTokenSource=newCancellationTokenSource();cancellationTokenSource.Token.Register(()=>{Console.WriteLine("取消了1111");});......
  • 「网络流浅谈」最小割的模型
    最大权闭合子图引入Introduction闭合子图指对于子图\(G=(V,E)\),\(\forallu\inV,(u,v)\inE\),都有\(v\inV\)。最大权闭合子图无非就是对于所有的闭合子图\(G\)中\(\sum_{u\inV}w_u\)最大的闭合子图。对于这个图中,闭合子图有哪些呢?红色框圈画出的即为\(1\)个......
  • weblogic漏洞浅谈
    weblogic反序列化漏洞原理分析weblogic是oracle公司出品的applicationserver,用于本地和云端开发,集成,部署和大型分布式web应用,网络应用和数据库应用的Java应用服务器weblogicserver是一个基于JAVAEE架构的中间件,将java的动态功能和javaEnterprise标准的安全性引入大型网络应用......