Mr.菟 发表于 2020-11-25 16:25

Unreal--GC回收笔记

前言

在进行任何开发时,都应该对所有操作内存生命周期有一个清晰的认知。但是当在进行多人开发时,难免会引用其他人或者第三方库代码,若对其中的内存管理方式不清晰,很容易造成程序Crash。这种情况在不带有自动回收机制的语言中尤为常见。
在Unreal中所有Uobject都被统一管理在GUObjectArray中。声明如下:
class FChunkedFixedUObjectArray
{
        enum
        {
                NumElementsPerChunk = 64 * 1024,
        };

        /** Master table to chunks of pointers **/
        FUObjectItem** Objects;
        /** If requested, a contiguous memory where all objects are allocated **/
        FUObjectItem* PreAllocatedObjects;
        /** Maximum number of elements **/
        int32 MaxElements;
        /** Number of elements we currently have **/
        int32 NumElements;
        /** Maximum number of chunks **/
        int32 MaxChunks;
        /** Number of chunks we currently have **/
        int32 NumChunks;
}

struct FUObjectItem
{
        // Pointer to the allocated object
        class UObjectBase* Object;
        // Internal flags
        int32 Flags;
        // UObject Owner Cluster Index
        int32 ClusterRootIndex;       
        // Weak Object Pointer Serial number associated with the object
        int32 SerialNumber;
}

class UObjectBase
{
        EObjectFlags                                                ObjectFlags;
        /** Index into GObjectArray...very private. */
        int32                                                        InternalIndex;
        /** Class the object belongs to. */
        UClass*                                                        ClassPrivate
        /** Name of this object */
        FName                                                        NamePrivate;
        /** Object this object resides in. */
        UObject*                                                OuterPrivate;
}
但是在开发中经常会用到没有注册为UPROPERTY之类的原生指针,这使得当该对象被GC时,该变量对其是不可知的,从而不经过检查直接使用会出现各式各样的问题。但在某些情况下我们只是希望对目标内存有一个引用关系,或者说直接使用其原生指针,因为一旦使用宏标记注册进了UObject的GC系统后,原来对象的生命周期就会发生改变。因此常用的解决方案有如下两种:(第二种还是存在问题,当一个新对象在这个内存上创建能通过检测,因为不像ptr储存了对象序列值,这种方案只比较了对象是否相等)

1.WeakObjectPtr

在Unreal可以使用FWeakObjectPtr持有对目标的引用,通过调用其内部实现的IsValid()函数检查内存有效性。函数实现如下:
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;
}

class FUObjectArray
{
        FORCEINLINE bool IsValid(FUObjectItem* ObjectItem, bool bEvenIfPendingKill)
        {
                if (ObjectItem)
                {
                        return bEvenIfPendingKill ? !ObjectItem->IsUnreachable() : !(ObjectItem->IsUnreachable() || ObjectItem->IsPendingKill());
                }
                return false;
        }
}逻辑上十分简单,首先检查其index是否有效,再到全局Array中取出检查是否为空,再匹配序列号看是否原始的UObject。检查都通过再用返回的FUObjectItem检查其IsPendingKill标志与IsUnreachable,默认的Get函数参数是false的,所以既要UObject在GC时能够从根部搜索到,并且没有被标记清楚才返回最后的UObjectBase。
UObject* FWeakObjectPtr::Get(/*bool bEvenIfPendingKill = false*/) const
{
        // Using a literal here allows the optimizer to remove branches later down the chain.
        return Internal_Get(false);
}从这里也可以看出在检查时可以对FWeakObjectPtr仅仅做一个Get操作,然后判断是否为空即可。不需要每次判断IsValid后再调用Get,对内部来说这会进行两次检查。
2.isValidLowLevel()

通过调用UObjectBase的isValidLowLevel()函数来检查UObject的有效性。
bool UObjectBase::IsValidLowLevel() const
{
        if( this == nullptr )
        {
                UE_LOG(LogUObjectBase, Warning, TEXT("NULL object") );
                return false;
        }
        if( !ClassPrivate )
        {
                UE_LOG(LogUObjectBase, Warning, TEXT("Object is not registered") );
                return false;
        }
        return GUObjectArray.IsValid(this);
}

bool FUObjectArray::IsValid(const UObjectBase* Object) const
{
        int32 Index = Object->InternalIndex;
        if( Index == INDEX_NONE )
        {
                UE_LOG(LogUObjectArray, Warning, TEXT("Object is not in global object array") );
                return false;
        }
        if( !ObjObjects.IsValidIndex(Index))
        {
                UE_LOG(LogUObjectArray, Warning, TEXT("Invalid object index %i"), Index );
                return false;
        }
        const FUObjectItem& Slot = ObjObjects;
        if( Slot.Object == NULL )
        {
                UE_LOG(LogUObjectArray, Warning, TEXT("Empty slot") );
                return false;
        }
        if( Slot.Object != Object )
        {
                UE_LOG(LogUObjectArray, Warning, TEXT("Other object in slot") );
                return false;
        }
        return true;
}可以看到逻辑与weakptr十分相似,先检查index的有效性,然后在全局表中索引到值检查是否为空,最后检查是否与原UObject相同。但是这种方式很有可能内存被替换而通过检查,所以还是存在问题。


额外需要说明的是全局函数中有IsValid函数,定义如下。
FORCEINLINE bool IsValid(const UObject *Test)
{
        return Test && !Test->IsPendingKill();
}可以看到该函数只是简单的检查对应的UObject是否被设置为Pending标志。其并不保证内存的可访问性,所以如果保存了一个原生指针,通过该函数直接检查标志不一定是当前UObject的Pending标志。
总结

虽然Unreal的GC是多线程的(4.22Editor下单线程),但却和游戏线程是同步的(不会同时执行)。且Unreal中所有对指针的IsValid判断可以大致分为两种:
第一种是对内存进行有效判断,大致流程都是对目标指针到全局表中查询判断有效性。但是这种判断是没有判断其GC标志位的,虽然内存都是有效的,但是不能确保内存与原本的一致性。
第二种是对GC标志位的判断,这种判断的前提是指针值必须要有效,否者很有可能造成访问错误或者没有意义的访问造成更多问题。
综上所述,若要完整的检查一个UObject的原生指针是否有效,只有通过weakbaseptr是严格正确的。而调用IsValidLowLevel() + IsPendingKillOrUnreachable()还是存在隐患。第二种方案时直接使用WeakObjectPtr然后调用Get,因为从上面代码可以看到在调用Get函数时引擎已经对其做了相应检测。这种方案是目前唯一提供原生方案中简单有效的。
当最最简单的还是加上UPROPERTY(),但是也需要注意以下几点:
当Actor或者UObject被Destroy时在同一帧的游戏逻辑中不会置任何值为空,只是设置PendingKill标志,而Unreachable标志也是进入GC后才置位。具体清空逻辑视当前的内存情况而定,可能在下一帧就清空,也可能间隔几千帧。而在这个时间里面,UPROPERTY指针不会被置空。所以UPROPERTY能保证内存可访问,但此时执行逻辑不一定正确(例如PendingKill之类的标志被设置),为了确保安全加上IsValid判断标志即可。
当内存真正被GC后,引擎会自动设置其值为nullptr,但注意GC标志还是得自己判断,但是没有悬空等一大堆烂事~
页: [1]
查看完整版本: Unreal--GC回收笔记