FGCObject
UE4有一套自己的GC机制,我觉得这篇文章讲的不错:《虚幻4垃圾回收剖析 - 风恋残雪 - 博客园》
概括来说,就是当一个对象“不再被需要”(如果把引用关系看作是一个“图”,那么就相当于这个对象“不可达”了)时,这个对象就要被清理掉。而为了表示这个对象是“被需要”的,就要让这个对象被引用。
引用一个对象有多种途径,最常见的就是使用UPROPERTY
宏来表示【对象】被另一个【引用者】UObject
所引用,但这对于非UObject
的【引用者】就没办法使用了。也可以使用AddToRoot
表示【这个对象】处于一个引用关系图的“根”位置,但这样就不能表示它被【另一个对象】所“引用”,当【另一个对象】被销毁时,【这个对象】也不会被清理,因为它处于“根”位置,和【另一个对象】无关。
FGCObject
则为非UObject
【引用者】提供了一个引用UObject
【对象】的功能,它是一个纯虚的基类,要求子类去实现AddReferencedObjects()
方法。
/**
* This class provides common registration for garbage collection for
* non-UObject classes. It is an abstract base class requiring you to implement
* the AddReferencedObjects() method.
*/
class COREUOBJECT_API FGCObject
/**
* Pure virtual that must be overloaded by the inheriting class. Use this
* method to serialize any UObjects contained that you wish to keep around.
*
* @param Collector The collector of referenced objects.
*/
virtual void AddReferencedObjects( FReferenceCollector& Collector ) = 0;
本篇的目标是构造一个小实验来测试FGCObject
的效果。另外也对我感兴趣的一个问题进行探究:
AddReferencedObjects
何时被调用?我关心它是因为,如果它是在构造函数之后立马调用,那么我在构造函数之后创建UObject
时,那么新创建的对象就不会被引用了。会发生这种问题吗?
准备实验
1. 创建插件
使用编辑器模式为模板创建一个插件。使用此模板是因为它默认在左侧的“模式”选项卡中生成一个界面,方便放一些按钮。
插件名字TestGCPlg
。
2. 创建一个UObject类型用于测试
TestObject.h
:
#pragma once
#include "UObject/Object.h"
#include "TestObject.generated.h"
UCLASS()
class UTestObject : public UObject
{
GENERATED_UCLASS_BODY()
};
TestObject.cpp
:
#include"TestObject.h"
UTestObject::UTestObject(const FObjectInitializer & ObjectInitializer)
: Super(ObjectInitializer)
{}
他没有任何内容,只是一个空的,用来测试用的。之所以不直接用UObject
类来测试是因为:
3. 创建一个测试的非UObject类,想要引用一个UObject
TestClass.h
:
#pragma once
#include"CoreMinimal.h"
class FTestClass
{
public:
FTestClass();
//想要引用的对象
class UTestObject* MyObject;
};
TestClass.cpp
:
#include"TestClass.h"
#include"TestObject.h"
FTestClass::FTestClass()
{
//创建一个新的UObject
MyObject = NewObject<UTestObject>(GetTransientPackage(),TEXT("TestGCPlgTestObject"));
}
其中NewObject
函数创建一个新的UObject
,第一个参数是Outer。然而我的FTestClass
并不是UObject
,因此并不能将自己作为Outer,所以我用GetTransientPackage()
来得到一个临时的包。
4. 测试用的操作
首先,在FTestGCPlgEdModeToolkit
中声明两个成员:
//FTestClass的指针(记录它是为了方便操作)
class FTestClass* TestClassPtr;
//UObject的弱指针(用来查询UObject有没有被清理)
TWeakObjectPtr<UObject> ObjectPtr;
随后,我需要四种操作来进行测试:
1)创建FTestClass
TestClassPtr = new FTestClass();
ObjectPtr = TestClassPtr->MyObject;
创建一个新的FTestClass
,同时把ObjectPtr
弱指针指向它的对象。
2)销毁FTestClass
delete TestClassPtr;
3)执行一次GC
CollectGarbage(EObjectFlags::RF_NoFlags);
参数是RF_NoFlags
,意味着不会排除任何Flag。
4)查询UObject是否被销毁了
if(ObjectPtr.IsValid())
FMessageDialog::Open(EAppMsgType::Type::Ok, FText::FromString("Is Valid"));
else
FMessageDialog::Open(EAppMsgType::Type::Ok, FText::FromString("Not Valid"));
通过弱指针TWeakObjectPtr
的方法IsValid()
来检测物体是否已经被销毁了。
完整代码如下:
void FTestGCPlgEdModeToolkit::Init(const TSharedPtr<IToolkitHost>& InitToolkitHost)
{
SAssignNew(ToolkitWidget, SBorder)
[
SNew(SVerticalBox)
+SVerticalBox::Slot().AutoHeight()
[
SNew(SButton).Text(FText::FromString("Create new TestClass"))
.OnClicked_Lambda([this]()
{
TestClassPtr = new FTestClass();
ObjectPtr = TestClassPtr->MyObject;
return FReply::Handled();
})
]
+SVerticalBox::Slot().AutoHeight()
[
SNew(SButton).Text(FText::FromString("Delete TestClass"))
.OnClicked_Lambda([this]()
{
delete TestClassPtr;
return FReply::Handled();
})
]
+ SVerticalBox::Slot().AutoHeight()
[
SNew(SButton).Text(FText::FromString("Execute GC"))
.OnClicked_Lambda([]()
{
CollectGarbage(EObjectFlags::RF_NoFlags);
return FReply::Handled();
})
]
+SVerticalBox::Slot().AutoHeight()
[
SNew(SButton).Text(FText::FromString("Query ObjectPtr.IsValid()"))
.OnClicked_Lambda([this]()
{
if(ObjectPtr.IsValid())
FMessageDialog::Open(EAppMsgType::Type::Ok, FText::FromString("Is Valid"));
else
FMessageDialog::Open(EAppMsgType::Type::Ok, FText::FromString("Not Valid"));
return FReply::Handled();
})
]
];
FModeToolkit::Init(InitToolkitHost);
}
实验0:没有使用FGCObject
- 点击Create new TestClass,此举生成一个新的
FTestClass
。 - 点击Query ObjectPtr.IsValid(),此时显示还没被销毁:
- 点击Execute GC,此举强制执行了一次GC
- 点击Query ObjectPtr.IsValid(),此时显示:
可见已经被GC掉了
实验1:使用 FGCObject 引用UObject
下面将FTestClass
继承FGCObject
TestClass.h
:
#pragma once
#include"CoreMinimal.h"
#include"UObject/GCObject.h"
class FTestClass : public FGCObject
{
public:
FTestClass();
//想要引用的对象
class UTestObject* MyObject;
//FGCObject的接口:
virtual void AddReferencedObjects(FReferenceCollector& Collector) override;
};
然后在AddReferencedObjects
中加入对MyObject
的引用
TestClass.cpp
:
#include"TestClass.h"
#include"TestObject.h"
FTestClass::FTestClass()
{
//创建一个新的UObject
MyObject = NewObject<UTestObject>(GetTransientPackage(), TEXT("TestGCPlgTestObject"));
}
void FTestClass::AddReferencedObjects(FReferenceCollector& Collector)
{
Collector.AddReferencedObject(MyObject);
}
下面再测试:
- 点击Create new TestClass,此举生成一个新的
FTestClass
。 - 点击Query ObjectPtr.IsValid(),此时显示还没被销毁
- 点击Execute GC,此举强制执行了一次GC
- 点击Query ObjectPtr.IsValid(),此时,仍旧没有被销毁:
- 点击Delete TestClass,此举删除了之前创建的
FTestClass
。 - 点击Execute GC,此举强制执行了一次GC
- 点击Query ObjectPtr.IsValid(),发现现在销毁了:
可见,当FTestClass
这个引用UObject
的人被销毁后,UObject
也会在下次GC时被销毁。
实验2:观察 AddReferencedObjects 的调用时机
我在AddReferencedObjects
中加入了断点,希望能断到任何的调用。
当我点击Create new TestClass创建TestClass时,断点并没有触发
而当我点击Execute GC调用GC时,立马触发了。
不过看栈,并不是从CollectGarbage
出发的,是从另一个线程:
而且,当下次我调用GC时,AddReferencedObjects
被再次调用。
看来,并不用担心开始提到的,引用的对象不会被加入引用的问题了。