TObjectPtr 好处
- 懒加载(什么是TObjectPtr 的懒加载,在什么时机会进行加载,怎么判断是否加载过)
- 安全(访问数据安全性,释放的时候其它引用对象是否会安全释放)
- 为啥奇数DebugPtr 表示没加载过(不知)
查看源码:
TObjectPtr 是个模板类型,他把T类型的数据存储为FObjectPtr 成员,并提供数据的访问,数据的访问通过FObjectPtr 的Get 方法
template <typename T>
struct TObjectPtr
{
explicit FORCEINLINE TObjectPtr(FObjectPtr ObjPtr)
: ObjectPtr(ObjPtr)
{
}
FORCEINLINE TObjectPtr(T& Object)
: ObjectPtr(const_cast<std::remove_const_t<T>*>(&Object))
{
}
/
/ 访问数据
FORCEINLINE T* Get() const { return (T*)ObjectPtr.Get(); }
// 类型
FORCEINLINE UClass* GetClass() const { return ObjectPtr.GetClass(); }
// outer 对象
FORCEINLINE TObjectPtr<UObject> GetOuter() const
{
FObjectPtr Ptr = ObjectPtr.GetOuter();
return TObjectPtr<UObject>(Ptr);
}
// 包对象
FORCEINLINE TObjectPtr<UPackage> GetPackage() const
{
FObjectPtr Ptr = ObjectPtr.GetPackage();
return TObjectPtr<UPackage>(Ptr);
}
// 访问都是通过
Get 函数进行访问
FORCEINLINE T* operator->() const { return Get(); }
FORCEINLINE UObject& operator*() const { return *Get(); }
FORCEINLINE FObjectHandle GetHandle() const { return ObjectPtr.GetHandle(); }
union
{
FObjectPtr ObjectPtr; // 指针数据内容
T* DebugPtr; // 如果指针地址是奇数ObjectPtr还没解析过(快速判定指针数据是否解析过)
};
}
FObjectPtr 数据,通过MakeObjectHandle 兼容指针或者对象的引用
struct FObjectPtr
{
public:
// 这边只拷贝句柄,没有进行解析(MakeObjectHandle 只是进行c 格式类型强转)
explicit FORCEINLINE FObjectPtr(UObject* Object)
: Handle(UE::CoreUObject::Private::MakeObjectHandle(Object))
{
// 防止构造时机进入垃圾回收被删除(正常不需要写吧,gc 的时候主线程是停止的,不会再构造对象吧)
// 业务逻辑上没在构造函数上用过这种写法
#if UE_OBJECT_PTR_GC_BARRIER
ConditionallyMarkAsReachable(Object);
#endif // UE_OBJECT_PTR_GC_BARRIER
}
// Get 和GetClass 都会进行对象的解析
FORCEINLINE UObject* Get() const
{
return UE::CoreUObject::Private::ResolveObjectHandle(Handle);
}
FORCEINLINE UClass* GetClass() const
{
return UE::CoreUObject::Private::ResolveObjectHandleClass(Handle);
}
// 数据成员
union
{
mutable FObjectHandle Handle; // 是句柄(数据的索引)或者指针数据
UObject* DebugPtr; // 如果指针地址是奇数ObjectPtr还没解析过(快速判定指针数据是否解析过)
};
FObjectHandle 数据解析称为对象的方法
可以看到数据是存放在FObjectHandle 中,分为懒加载和直接加载。对于懒加载有分为数据是否已经解析过还是需要进行解析,对于已经解析过的对象FObjectHandle 存放的时对象的指针
关于懒加载:WITH_EDITORONLY_DATA 的时候(编辑器模式下UE_WITH_OBJECT_HANDLE_LATE_RESOLVE=1)不需要构造的时候加载对象,用到Get()获取对象的时候加载。同时引擎提供对于对象名称FObjectPtr 中的取包名称,路径名称可以不去解析指针的内容,直接访问这些名称。这些是为了加快编辑器加载速度,对于打包版本因为不带WITH_EDITORONLY_DATA,TObjectPtr 会退化回UObject 不会有影响。
这里继续深入查看对象解析方法
PackedObjectRef 数据结构: 30位包对象ID | 32位对象ID | 1位(值3表示不安全)| 1位(值0表示数据已经解析)
通过解析到的数据从GObjectHandleIndex 表里面查找对象的名称和包名,并判断FObjectPathId 对象是否时WeakObj 对象进行返回。FObjectPathId是转换成FName 的方式,对大小写不敏感,路径少的时候可以不查表,后续再分析这个类型。如果是WeakObj 会拼接回下标从GUObjectArray里面找到UObject 对象。
对于名字数据又进行数据缓存,编辑器可以在不解析对象的情况下直接访问
对于FObjectPathId 还没找到对象的数据通过下面的方式进行加载
UObject* FObjectRef::Resolve(uint32 LoadFlags /*= LOAD_None*/) const
{
TRACE_CPUPROFILER_EVENT_SCOPE(ResolveObjectRef);
UE::CoreUObject::Private::FObjectPathId ObjectPath = GetObjectPath();
if (IsNull() || !ObjectPath.IsValid())
{
UE::CoreUObject::Private::OnReferenceResolved(*this, nullptr, nullptr);
return nullptr;
}
bool bWasObjectOrPackageLoaded = false;
// 加载包对象
UPackage* TargetPackage = FindOrLoadPackage(PackageName, LoadFlags, bWasObjectOrPackageLoaded);
if (!TargetPackage)
{
UE::CoreUObject::Private::OnReferenceResolved(*this, nullptr, nullptr);
return nullptr;
}
UE::CoreUObject::Private::FObjectPathId::ResolvedNameContainerType ResolvedNames;
ObjectPath.Resolve(ResolvedNames);
UObject* CurrentObject = TargetPackage;
for (int32 ObjectPathIndex = 0; ObjectPathIndex < ResolvedNames.Num(); ++ObjectPathIndex)
{
UObject* PreviousOuter = CurrentObject;
CurrentObject = StaticFindObjectFastInternal(nullptr, CurrentObject, ResolvedNames[ObjectPathIndex]);
// 包重定向
if (UObjectRedirector* Redirector = dynamic_cast<UObjectRedirector*>(CurrentObject))
{
CurrentObject = Redirector->DestinationObject;
if (CurrentObject != nullptr)
{
TargetPackage = CurrentObject->GetPackage();
}
}
if (!CurrentObject && !TargetPackage->IsFullyLoaded() && TargetPackage->GetLinker() && TargetPackage->GetLinker()->IsLoading())
{
if (IsInAsyncLoadingThread() || IsInGameThread())
{
// 加载包的所有对象
TargetPackage->GetLinker()->LoadAllObjects(true);
bWasObjectOrPackageLoaded = true;
}
else
{
// Shunt the load request to happen on the game thread and block on its completion. This is a deadlock risk! The game thread may be blocked waiting on this thread.
UE_LOG(LogObjectRef, Warning, TEXT("Resolve of object in package '%s' from a non-game thread was shunted to the game thread."), *PackageName.ToString());
TGraphTask<FFullyLoadPackageOnHandleResolveTask>::CreateTask().ConstructAndDispatchWhenReady(TargetPackage)->Wait();
bWasObjectOrPackageLoaded = true;
}
// 加载包里面指定位置的对象
CurrentObject = StaticFindObjectFastInternal(nullptr, PreviousOuter, ResolvedNames[ObjectPathIndex]);
if (UObjectRedirector* Redirector = dynamic_cast<UObjectRedirector*>(CurrentObject))
{
// 处理对象重定向
CurrentObject = Redirector->DestinationObject;
if (CurrentObject != nullptr)
{
TargetPackage = CurrentObject->GetPackage();
}
}
}
if (!CurrentObject)
{
UE::CoreUObject::Private::OnReferenceResolved(*this, TargetPackage, nullptr);
return nullptr;
}
}
if (CurrentObject->HasAnyFlags(RF_NeedLoad) && TargetPackage->GetLinker())
{
if (IsInAsyncLoadingThread() || IsInGameThread())
{
TargetPackage->GetLinker()->LoadAllObjects(true);
bWasObjectOrPackageLoaded = true;
}
else
{
// Shunt the load request to happen on the game thread and block on its completion. This is a deadlock risk! The game thread may be blocked waiting on this thread.
UE_LOG(LogObjectRef, Warning, TEXT("Resolve of object in package '%s' from a non-game thread was shunted to the game thread."), *PackageName.ToString());
TGraphTask<FFullyLoadPackageOnHandleResolveTask>::CreateTask().ConstructAndDispatchWhenReady(TargetPackage)->Wait();
bWasObjectOrPackageLoaded = true;
}
}
UE::CoreUObject::Private::OnReferenceResolved(*this, TargetPackage, CurrentObject);
if (bWasObjectOrPackageLoaded)
{
UE::CoreUObject::Private::OnReferenceLoaded(*this, TargetPackage, CurrentObject);
}
return CurrentObject;
}
关于安全:不带WITH_EDITORONLY_DATA 的版本(打包DS模式或者客户端模式)等价于UObject ,没有更加安全。对于编辑器模式指针访问都是走Get 函数,Get 里面会用路径从全局对象里面判断是否有效,无效的话返回的是空指针。不会出现野指针情况。
和UPROPERTY 区别:
UPROPERTY 会把对象加到GC 进行管理,每隔一段时间都会有GC检测会带来消耗,但是打包版本还是会有引用对象可靠性保证,引用对象如果消耗会通知这里变为空指针。普通情况下是直接判空,保险情况下用IsValid(Object) 去全局对象表判断对象是否安全
TObjectPtr 没有GC 管理,没有GC消耗,直接判空可能会出问题,需要每次访问都用xxx.IsValid() 进行判断
TObjectPtr 对象也可以加UPROPERTY 进行对象的引用
对于对象因为非法访问内存崩溃的可以加stompmalloc 方式定位出问题堆栈进行排查