讀完本篇文章,你會了解為何UE中C++作為其開發語言,使用的指標,為何各式各樣。
你需要對UE有所瞭解,如果不瞭解也沒關係,也可以看下這篇文章,就當瞭解一下最複雜的應用的系統指標設計是如何。
可以肉眼可見,類物件存在還是被釋放了。
我這邊給出的是自己個人對指標種類分類的看法,主要是結合專案使用情況,大致得出下列型別。
[圖1]
可以通過觀察上圖記憶體變化,肉眼可見物件是否徹底釋放。(其實或者看Log,主要是建構函式和解構函式)
//自定義原生C++類別
class FCustomDefinedClass
{
public:
FCustomDefinedClass()
{
Arr.AddDefaulted(100*1024*1024); //為了測試便於觀察對比,申請記憶體
UE_LOG(LogTemp, Log, TEXT("FCustomDefinedClass() Start"));
}
~FCustomDefinedClass()
{
Arr.Reset();//為了測試方便,釋放記憶體
UE_LOG(LogTemp, Log, TEXT("~FCustomDefinedClass() Stop"));
}
void PrintArr()
{
UE_LOG(LogTemp, Log, TEXT("FCustomDefinedClass PrintArr"));
}
TArray<bool> Arr;
};
UCLASS()
class UCustomDefinedObject :public UObject
{
GENERATED_BODY()
public:
UCustomDefinedObject(const class FObjectInitializer& ObjectInitializer) {
Arr.AddDefaulted(100 * 1024 * 1024); //為了測試便於觀察對比,申請記憶體
UE_LOG(LogTemp, Log, TEXT("UCustomDefinedObject() Start"));
};
~UCustomDefinedObject()
{
Arr.Reset();//為了測試方便,釋放記憶體
UE_LOG(LogTemp, Log, TEXT("~UCustomDefinedObject() Stop"));
}
void PrintArr()
{
UE_LOG(LogTemp, Log, TEXT("UCustomDefinedObject PrintArr"));
}
TArray<bool> Arr;
};
建構函式中我們申請100MB的記憶體,在解構函式中釋放這100MB的物件。
在程式碼中New出一個該類物件,記憶體就會增大100M,該類被解構,就會釋放,於是肉眼可見的物件是否存活,實現了。
一步一步來,從最簡單的開始分析。
1.原生C++裸指標
其實這個比較簡單,我new一個,之後我必須手動釋放。程式碼如下
//UE中觀察引擎記憶體顯示(類似圖1)
// Mem:1309MB
FCustomDefinedClass* InCustomDefinedObject = new FCustomDefinedClass();
// Mem:1407MB
delete InCustomDefinedObject;
InCustomDefinedObject = nullptr;
// Mem:1299MB
(大約都是100MB的落差,符合預期,有點誤差,可以忽略,FCustomDefinedClass類的作用完成,類物件肉眼可見是否存在實現)
2.原生C++共用指標
上述程式碼如果不寫或者漏調 delete InCustomDefinedObject,觀察記憶體顯示,即使我停止(Play)遊戲,數目都沒有減少,再次Play啟動遊戲 New該類,再停止Play,會發現記憶體一直在增加,這就是傳說的記憶體漏失。 非常嚴重。我只是沒調這個解構,忘記調了(物件那麼多,每個都要delete,肯定忘記),可是每個物件都需要手動這麼寫,也太累了。 於是C++原生的智慧指標出現了。
MakeShareable<FCustomDefinedClass> InCustomShareObject = MakeShareable<FCustomDefinedClass>(new FCustomDefinedClass());
InCustomShareObject = nullptr;
再次觀察記憶體情況,記憶體可以正常釋放。
關於共用指標的原理,可以參考:手把手帶你實現一個智慧指標
3.原生C++弱指標
使用共用指標的主要原因是避免手動管理指標釋放資源。但是,在某些情況下共用指標不能實現預期的行為:
一種情況是迴圈參照。如果兩個物件使用共用指標相互參照,並且不存在對這些物件的其他參照,若要釋放這些物件及其關聯的資源,則共用指標不會釋放資料,因為每個物件的參照計數仍為1。在這種情況下,可能想使用普通的指標,但是這樣做需要手動管理相關資源的釋放。
另一種情況是當明確想要共用但不擁有物件。這種情況下參照的生存期超過了它所參照的物件的生命週期。如果使用共用指標則其將永遠不會釋放物件。如果使用普通指標則可能出現指標所參照的物件不再有效,這會帶來存取已釋放資料的風險。
對於這兩種情況都可以使用弱指標指標處理。弱指標是共用指標的輔助類,弱指標需要共用指標才能建立。
上述我們知道共用指標是如果有參照計數,就不會被釋放,那麼如果我只是想用一個物件,但是又不想對他造成影響,就是不想影響他的計數,不想影響他的生命週期。換而言之就是共用指標那邊該幹嘛就幹嘛,我這邊WeakPtr這邊不影響他。只是說他那邊沒了,我這邊也要沒了,他那邊還在,我這邊就還在。
於是弱指標就來了。
void ATestObjectActorManager::TestCallGenerate()
{
const TSharedPtr<FCustomDefinedClass> WeakSharePtr = MakeShareable<FCustomDefinedClass>(new FCustomDefinedClass());
InCustomWeakObject = WeakSharePtr;
}
//WeakSharePtr 在這個函數執行完,因為是臨時變數,會被幹掉,參照計數為0,釋放記憶體了。
void ATestObjectActorManager::TestCallDestory()
{
if (InCustomWeakObject.IsValid()) //執行到這的時候InCustomWeakObject已經invalid了,為false了。
{
// ....
}
}
(共用指標&弱指標用法,都需要IsValid來預先判斷)
4.UObject裸指標
終於到了UE這邊了,因為UE考慮到C++的指標釋放記憶體啥的是個麻煩的事,C++原生雖然有自己的智慧指標,但是作為遊戲,有一些覺得C++原生做的不好的(具體我也不知道哪裡不好)。自己搞的,才是適合自己的,適合遊戲的,於是UE 讓UObject(組成UE世界的最小單元)就附帶了垃圾回收的功能
案例一
void ATestObjectActorManager::TestCallGenerate()
{
UCustomDefinedObject* TempDefinedObj = NewObject<UCustomDefinedObject>();
}
該函數執行完,因為是臨時變數,做得事跟上述共用指標類似得事,參照計數為0,但是觀察記憶體情況,嘗試執行3次,每次都在不斷增長1
0MB記憶體,漲了300MB
我們這個時候在輸入強制GC指令:gc.ForceCollectGarbageEveryFrame 1
之後會發現上漲得300MB都被釋放了。
void ATestObjectActorManager::TestCallGenerate()
{
TempDefinedObj = NewObject<UCustomDefinedObject>();
}
因為沒有UProperty,執行GC,該因為沒有參照,所以被釋放且指標沒有置nullPtr,就是傳說「野指標」了
小結:繼承自UObject得裸指標在沒有參照計數後,可能算是「洩漏」,但是隻要有UE得垃圾回收機制執行,這些所謂「洩漏」得記憶體還是會被釋放。
5.UObject帶UProperty指標
因為有UPROPERTY,參照關係計算了,
void ATestObjectActorManager::TestCallGenerate()
{
TempDefinedObj = NewObject<UCustomDefinedObject>();
}
這個時候使用ForceGC指令,記憶體是不會變化的。
這個時候我給所在物件使用MarkPendingKill,則記憶體被釋放掉。
加了的話,如果所參照的UObject被MarkPendingKill,則該Uobject也會被強制回收。
小結:加了UProperty,算這個UObject指標加入計數了,不然就會被當作沒有計數被釋放且野指標。
6.UObject弱指標
我們前面已經說過了原生C++ 有共用指標,弱指標。當然UE這邊有自己的智慧指標Uibject,但是沒有弱指標,對於繼承於UObject的指標,可以使用UObject的弱指標使用方式。
UCustomDefinedObject* InObject = NewObject<UCustomDefinedObject>();
TWeakObjectPtr<UObject> ObjectWithWeak(InObject);
也是跟上述原生的C++弱指標的使用方式類似。這裡因為UObject的指標本身就自帶共用功能,所以這邊直接賦值即可。
來源:
C++裡有原生指標,可是真的太麻煩,太危險,不好使,所以出了共用指標,自動幫你管理釋放,但是共用指標因為計數原理,還有一些副作用弊端,還有需求就是隻是單純的想使用並不想計入參照,於是出了弱指標。在遊戲,就是UE這邊因為效能等的綜合考慮弄了自己的一套自動管理釋放物件的系統,就是UObject系統,還有專門針對UObject物件使用的弱指標。
應用:
首先想直接使用原生C++裸指標,肯定是不建議的, 太危險,因為忘記delete後果非常嚴重。
如果你的類不是繼承自UObject,不需要UObject提供的反射等其他複雜功能,真的很簡單的類物件的話,那麼就使用原生C++的共用指標儲存,如果在其他地方需要對共用指標有個參照,但是又不想影響其計數,就使用弱指標。
對於繼承自UObject的指標,非常不推薦裸指標的方式,就是不加UPROPERTY, 一定要加UPROPERTY,如果不想加的話,那麼使用弱指標的方式即可。