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(&#34;NULL object&#34;) );
return false;
}
if( !ClassPrivate )
{
UE_LOG(LogUObjectBase, Warning, TEXT(&#34;Object is not registered&#34;) );
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(&#34;Object is not in global object array&#34;) );
return false;
}
if( !ObjObjects.IsValidIndex(Index))
{
UE_LOG(LogUObjectArray, Warning, TEXT(&#34;Invalid object index %i&#34;), Index );
return false;
}
const FUObjectItem& Slot = ObjObjects;
if( Slot.Object == NULL )
{
UE_LOG(LogUObjectArray, Warning, TEXT(&#34;Empty slot&#34;) );
return false;
}
if( Slot.Object != Object )
{
UE_LOG(LogUObjectArray, Warning, TEXT(&#34;Other object in slot&#34;) );
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]