Ue4_序列化淺析
1. 序列化基本概念
序列化是指將對象轉換成字節流,從而存儲對象或將對象傳輸到內存、數據庫或文件等的過程。 它的主要用途是保存對象的狀態,以便能夠在需要時重新創建對象。 反向過程稱爲“反序列化”。 (通俗來說就是保存和讀取的過程分別爲序列化和反序列化)
而在維基百科裏面是這樣解釋的。
序列化(serialization)在計算機科學的數據處理中,是指將數據結構或對象狀態轉換成可取用格式(例如存成文件,存於緩衝,或經由網絡中發送),以留待後續在相同或另一臺計算機環境中,能恢撤消先狀態的過程。依照序列化格式重新獲取字節的結果時,可以利用它來產生與原始對象相同語義的副本。對於許多對象,像是使用大量引用的複雜對象,這種序列化重建的過程並不容易。面向對象中的對象序列化,並不概括之前原始對象所關係的函數。這種過程也稱爲對象編組(marshalling)。從一系列字節提取數據結構的反向操作,是反序列化(也稱爲解編組、deserialization、unmarshalling)。
序列化的工作原理
下圖展示了序列化的整個過程。
對象被序列化成流,其中不僅包含數據,還包含對象類型的相關信息,如版本、區域性和程序集名稱。 可以將此流中的內容存儲在數據庫、文件或內存中。
2. Ue4的序列化
Ue4的序列化使用了訪問者模式(Vistor Pattern),將序列化的存檔接口抽象化,其中FArchive爲訪問者, 其它UObject實現了void Serialize( FArchive& Ar )
,接口的類爲被訪問者。FArchive可以是磁盤文件訪問, 內存統計,對象統計等功能。
以下是FArchive的類繼承如下:
下面是FArchive提供的一些簡單的接口。一部分代碼已經去掉。
class CORE_API FArchive
{
public:
/** Default constructor. */
FArchive();
/** Copy constructor. */
FArchive(const FArchive&);
/**
* Copy assignment operator.
*
* @param ArchiveToCopy The archive to copy from.
*/
FArchive& operator=(const FArchive& ArchiveToCopy);
/** Destructor. */
virtual ~FArchive();
public:
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, ANSICHAR& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, WIDECHAR& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, uint8& Value);
template<class TEnum>
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, TEnumAsByte<TEnum>& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, int8& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, uint16& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, int16& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, uint32& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, bool& D);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, int32& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, long& Value);
FORCEINLINE friend FArchive& operator<<( FArchive& Ar, float& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, double& Value);
FORCEINLINE friend FArchive& operator<<(FArchive &Ar, uint64& Value);
/*FORCEINLINE*/friend FArchive& operator<<(FArchive& Ar, int64& Value);
template <
typename EnumType,
typename = typename TEnableIf<TIsEnumClass<EnumType>::Value>::Type
>
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, EnumType& Value)
{
return Ar << (__underlying_type(EnumType)&)Value;
}
friend FArchive& operator<<(FArchive& Ar, struct FIntRect& Value);
friend CORE_API FArchive& operator<<(FArchive& Ar, FString& Value);
public:
virtual void Serialize(void* V, int64 Length) ;
virtual void SerializeBits(void* V, int64 LengthBits);
virtual void SerializeInt(uint32& Value, uint32 Max);
};
由上面可以看到,FArchive原始支持一些基本的數據結構的序列化的。但是具體實現過程需要在子類部分實現。
主要是通過重載operator<<實現,具體實現函數只要是Serialize(void* V, int64 Length),並在在該函數中調用Memcpy(void* dest, void* src, int64 Length)進行復制。
3. UObject的序列化
UObject
的序列化和反序列化都對應函數Serialize
。通過傳遞進來的FArchive的類型不同而進行不同的操作。
此處主要討論反序列化,UObject通過反序列化的方式從持久存儲中讀出一個對象,也是需要先實例化對象,然後才能反序列化,而非通過一堆數據,直接就反序列化獲得對象。
反序列化過程:
當實例化一個對象之後,傳遞一個FArchive參數調用反序列化函數,接下來具體過程如下:
-
通過GetClass函數獲取當前的類信息,通過GetOuter函數獲取Outer。這個Outer實際上指定了當前UObject會被當作爲哪一個對象的子對象進行序列化。
-
判斷當前等待序列化的對象的類UClass的信息是否被載入,沒有的話:
a. 預載入當前類的信息;
b. 預載入當前類的默認對象CDO的信息;
-
載入名字
-
載入Outer
-
載入當前對象的類信息,保存於ObjClass對象中。
-
載入對象的所有腳本成員變量信息。這一步必須在類信息加載後,否則無法根據類信息獲得有哪些腳本成員變量需要加載。
對應函數爲
SerializeScriptProperties
,序列化在類中定義的對象屬性。a. 調用
FArchive.MarkScriptSerializationStart
,標誌腳本序列化數據開始;b.調用
SerializeTaggedProperties
,序列化對象屬性,並且加入tag;c. 調用
FArchive.MarkScriptSerializationEnd
,標誌腳本序列化數據結束。
以下大概解釋一下UObject代碼部分的序列化,由於篇幅有限,只列舉部分出來。
UObject::Serialize( FArchive& Ar )
UObject通過實現Serialize接口來序列化對象數據。
void UObject::Serialize( FArchive& Ar )
{
// These three items are very special items from a serialization standpoint. They aren't actually serialized.
UClass *ObjClass = GetClass();
UObject* LoadOuter = GetOuter();
FName LoadName = GetFName();
// ........
// 中間省略了一部分代碼
//
// Serialize object properties which are defined in the class.
// Handle derived UClass objects (exact UClass objects are native only and shouldn't be touched)
if (ObjClass != UClass::StaticClass())
{
SerializeScriptProperties(Ar);
}
// 省略一部分代碼
// 序列化在類中定義的對象屬性。
// 添加GUID
// Serialize a GUID if this object has one mapped to it
FLazyObjectPtr::PossiblySerializeObjectGuid(this, Ar);
// Invalidate asset pointer caches when loading a new object
if (Ar.IsLoading() )
{
FSoftObjectPath::InvalidateTag();
}
// Memory counting (with proper alignment to match C++)
SIZE_T Size = GetClass()->GetStructureSize();
Ar.CountBytes( Size, Size );
}
void UObject::SerializeScriptProperties( FArchive& Ar ) const
void UObject::SerializeScriptProperties( FArchive& Ar ) const
{
Ar.MarkScriptSerializationStart(this);
if( HasAnyFlags(RF_ClassDefaultObject) )
{
Ar.StartSerializingDefaults();
}
UClass *ObjClass = GetClass();
if( (Ar.IsLoading() || Ar.IsSaving()) && !Ar.WantBinaryPropertySerialization() )
{
//@todoio GetArchetype is pathological for blueprint classes and the event driven loader; the EDL already knows what the archetype is; just calling this->GetArchetype() tries to load some other stuff.
UObject* DiffObject = Ar.GetArchetypeFromLoader(this);
if (!DiffObject)
{
DiffObject = GetArchetype();
}
// 省略部分代碼
// 序列化對象屬性,並且加入tag
ObjClass->SerializeTaggedProperties(Ar, (uint8*)this, HasAnyFlags(RF_ClassDefaultObject) ? ObjClass->GetSuperClass() : ObjClass, (uint8*)DiffObject, bBreakSerializationRecursion ? this : NULL);
}
else if ( Ar.GetPortFlags() != 0 && !Ar.ArUseCustomPropertyList )
{
//@todoio GetArchetype is pathological for blueprint classes and the event driven loader; the EDL already knows what the archetype is; just calling this->GetArchetype() tries to load some other stuff.
UObject* DiffObject = Ar.GetArchetypeFromLoader(this);
if (!DiffObject)
{
DiffObject = GetArchetype();
}
ObjClass->SerializeBinEx( Ar, const_cast<UObject *>(this), DiffObject, DiffObject ? DiffObject->GetClass() : NULL );
}
else
{
ObjClass->SerializeBin( Ar, const_cast<UObject *>(this) );
}
if( HasAnyFlags(RF_ClassDefaultObject) )
{
Ar.StopSerializingDefaults();
}
Ar.MarkScriptSerializationEnd(this);
}
ObjClass->SerializeTaggedProperties(Ar, (uint8)this, HasAnyFlags(RF_ClassDefaultObject) ? ObjClass->GetSuperClass() : ObjClass, (uint8)DiffObject, bBreakSerializationRecursion ? this : NULL);
在這個函數中主要序列化屬性和添加tag,由於代碼太大,就不一一列出來了。
下面借用大象無形這本書對UObject序列化所提出的切豆腐理論:
切豆腐理論
對於硬盤上保存的數據來說,其本身不具備“意義”,其含義取決於我們如何解釋這一段數據。我們每次反序列化一個C++基本對象,例如一個float浮點數,我們是從豆腐最開頭切下一塊和浮點數長度一樣大的豆腐,然後把這塊豆腐解釋爲一個浮點數。那讀者會問,你怎麼知道這塊豆腐就是浮點數?不是布爾值?或者是某個字符串?答案是,你按照什麼樣的順序把豆腐排起來的,並按照同樣順序切,那就能保證每次切出來的都是正確的。我放了三塊豆腐:豆腐1(foat)、豆腐2(bool)、豆腐3( double)。然後把它們按順序排好靠緊,於是豆腐之間的縫隙就沒了。我可以把這三塊豆腐看成一塊完整的大豆腐,放冰箱裏凍起來。下次要喫的時候,我該怎麼把它們切成原樣?很簡單,我知道豆腐1、豆腐2、豆腐3的長度,所以我在1號長度處切一刀,1號+2號長度處切一刀。妥了!三塊豆腐就分開了。
遞歸序列化/反序列化
一個類的成員變量會有以下類型:C++基本類型、自定義類型的對象、自定義類型的指針。關於指針問題,我們將會在後文分析。這裏主要分析前兩者。第一個,對於C++基本類型對象,我們可以用切豆腐理論序列化。那麼自定義類型怎麼辦? 畢竟這個類型是我自己定義的,我有些變量不想序列化(比如那些無關緊要的、可以由其他變量計算得來的變量)怎麼解決?答案是,如果需要序列化自定義類型,就調用自定義類型的序列化函數。由該類型自行決定。於是這就轉化爲了一棵像樹一樣的序列化過程。沿用前文的切豆腐理論。當我們需要向豆腐列表增加一塊由別人定義的豆腐的時候,我們遵循誰定義誰負責的原則,讓別人把豆腐給我們。切豆腐的時候,由於我們只知道接下來要切的豆腐的類型,卻不知道具體應該怎麼切,我們就把整塊豆腐都交給那個人,讓他自己處理。切完了再還給我們。
理解這兩個概念之後,我們就能開始分析虛幻引擎的反序列化過程。這其實是一個兩步過程,其中第一步可選:
(1) 從另一塊豆腐中加載對象所屬的類信息,一旦加載完成就像獲得了一張當前豆腐的統計表,這塊豆腐都有哪幾節,每節對應類型是什麼,都在這張表裏。
(2) 根據統計表,開始切豆腐。此時我們已經知道每塊豆腐切割位置和獲取順序了,還原成員變量簡直如同探囊取物。 同時,虛幻引擎也對序列化後的大小進行了優化。我們不妨思考前文所述的切豆腐論,如果我們完成序列化整個類,那麼對於繼承深度較深的子類,勢必要序列化父類的全部數據。那麼每個子類對象都必須佔用較大的空間。有沒有辦法優化?我們會發現其實子類對象很多數據是共同的,它們都是來自同樣父類的默認值。這些數據只需要序列化一份就可以了。換句話說:
虛幻引擎序列化每個繼承自 CLass 的類的默認值(即序列化CDO),然後序列化對象與類默認對象的差異。這樣就節約了大量的子類對象序列化後的存儲空間。
接下來就是改如何去切一塊豆腐了。
4. uasset文件格式
UE中使用統一的格式存儲資源(uasset, umap),每個uasset對應一個包(package),存儲一個UPackage對象時,會將該包下的所有對象都存到uasset中。
- File Summary 文件頭信息。
- Name Table 包中對象的名字表。
- Import Table 存放被該包中對象引用的其它包中的對象信息(路徑名和類型)。
- Export Table 該包中的對象信息(路徑名和類型)。
- Export Objects 所有Export Table中對象的實際數據。
舉一個例子更好地解釋uasset的文件格式。
以一個班級(UPackage)來舉例子,首先班級裏有很多學生(對象),但是這個班級充滿了戀愛的味道(對象之間互相引用,還引用了其他班的對象,甚至還有多角戀),而且老師爲了解決同桌早戀問題,經常讓全班換座位(每次加載內存地址都會改變)。此時來了一個管理者(FArchive),希望能夠記錄全班談戀愛對象配對名單。
-
在包最前方有兩張表,導出表 Export Table和導入表 Import Table,前者可以理解爲本班人員名單,後者可以理解爲隔壁班人員名單;
-
當序列化當前包內一個對象的時候,遇到一個 UObject指針怎麼辦?此時肯定不能直接序列化指針的值。這類似於管理者在記錄小王喜歡的對象小紅的時候,不能直接記錄小王喜歡的人的座位(內存地址),否則第二天座位一變動,就出事了。此時管理者靈機一動。
a. 拿出導出表,往裏面加了一項:1號小紅。
b. 修改“小王喜歡的對象”字段,將其從一個座位編號,變成了導出表裏面的
個項的編號:1號。c.查看誰是小紅真正的男朋友( NewObject的時候指定的 Outer,到時候由他負責真正序列化小紅。
d.繼續存儲其他有關的信息:如果遇到普通數據(小王的名字, FName;小王的年齡,Int8),就直接序列化,如果遇到 UObject,就重複第二和第三步。
-
這時候管理者發現小剛喜歡的對象居然是隔壁班的小花,管理者無奈,只能再找出一張表:導入表,然後加了一項-1:小花(導入表項爲負,導出表項爲正,方便區分)。總不能把隔壁班的人也給序列化到本班的數據裏面吧。然後把這項的編號替換到小剛喜歡的人的座位裏。
-
全部記錄完畢,把兩張表都保存起來,對象本身的數據則逐個排放在表後面,存
放起來。
反序列化
第二天,管理者(負責反序列化的FArchive)來到教室,教室一個學生都沒有(內存此時完全爲空,沒有任何對象信息)。
-
此時,管理者拿出自己昨天記錄的信息,從後面抽出一個對象,看看對象是什麼類型,根據這個類型,把對象先模塑出來:
a. 如果UClass類型數據還沒載入,先把UClass載入了,並把CDO給讀取了。
b. 根據UClass信息,模塑一個對象–通俗來說,管理者先在塑造了一個假人出來,但是這個假人目前還沒有任何特徵。
-
接着根據這個對象的類信息,讀取等大的數據,並根據類信息中包含的成員變量信息,判斷這個成員變量的類型,執行以下步驟:
a. 假如是基礎對象(名字,FName;年齡,Int8),就直接把這個對象給反序列化。此時這個假人擁有了名字和年齡。還有一些其他的屬性。
b. 此時不可避免地遇到了UObject類型的成員變量。此時,管理者查看這個成員變量的PackageIndex 是負的還是正的。
如果是正的,則檢查ExportTable導出表,看看這個對象有沒有被序列化,如果有,就把對應對象的指針替換,否則就先造個假人丟在那裏,等待此人的Outer負責實際序列化。
如果是負的,則檢查ImportTable導入表,看看對應的Package有沒有載入內存,沒載入,就載入該Package;如果已經載入了,管理者直接到該Package找到該對象的地址填到表項處。
-
最後,經歷一波折騰,全班會經歷這樣一個過程:
a. 首先班上會逐漸出現原來的同學和一些假人(已經被 Newobject模塑出來,,但是還沒有根據反序列化信息恢復成原始對象的對象);
b. 隨後假人會逐漸被還原爲原始對象。即隨着讀取的信息越來越多,根據反序列化後的信息還原成和原始對象一致的對象越來越多;
c. 最後全班所有人都會被恢復爲和原始對象一致的人。也許小王同學喜歡的那個人的座位號變了(反序列化後指針的值被修正),但是新的座位號上坐着的人,是和他當年喜歡的小紅一模一樣的人。
通俗地來說就是這樣的過程。從這個過程中,我們能獲取到一些非常有趣的信息和
經驗:
-
序列化必要的、差異性的數據
不必要的引用不需要被序列化和反序列化。因此如果你的成員變量沒有被 UPROPERTY標記,其不會被序列化。如果你的這個成員變量值與默認值一致,也不會佔用空間進行序列化
-
先模塑對象,再還原數據
這個過程筆者多次重點闡述,就是爲了強調虛幻引擎的
這個設計。先把對象通過 Newobject模塑,然後還原差異性的數據。且被模塑出的
對象會作爲其他對象修正指針指向的基礎。正如前文所言,小明不會因爲小王的對
象小紅還沒被序列化就束手無策,沒被序列化就直接實例化一個假人丟在那,大不
了以後讀取到小紅的數據時,把那個假人的信息改成和小紅一樣就好。 -
對象具有“所屬”關係
由 NewObject指定的 Outer負責序列化和反序列化。
-
鴨子理論
叫起來像鴨子,看起來像鴨子,動起來像鴨子,那就是鴨子。說話像小紅,看起來像小紅,做事情像小紅,那就是小紅。也就是說,如果一個對象的所有成員變量與原始對象一致(指針的值可以不同,但指向的對象要一致),則該對象就是原始對象。
5. FlinkerLoad和FlinkerSave
負責將uasset文件中的對象加載到內存中,起橋樑作用。
6. 例子:SaveData And Load Data
MyActor.h
UCLASS()
class HELLOWORLD_API ASaveActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ASaveActor();
public:
friend FArchive& operator<<(FArchive& Ar, ASaveActor& SaveActorRef);
UPROPERTY(EditAnywhere)
float Health;
};
MyActor.cpp
FArchive & operator<<(FArchive & Ar, ASaveActor & SaveActorRef)
{
Ar << SaveActorRef.Health;
return Ar;
}
Character.h
UCLASS()
class HELLOWORLD_API ATP_ThirdPersonCharacter : public ACharacter
{
GENERATED_BODY()
public:
//
void SaveLoadData(FArchive& Ar, float& HealthToSaveOrLoad, int32& CurrentAmmoToSaveOrLoad, FVector& PlayerLocationToSaveOrLoad);
// 你好
UPROPERTY(EditAnywhere)
class ASaveActor* SaveActorRef;
UFUNCTION(BlueprintCallable, Category = SaveLoad)
bool SaveData();
UFUNCTION(BlueprintCallable, Category = SaveLoad)
bool LoadData();
UPROPERTY(EditAnywhere)
float Health;
UPROPERTY(EditAnywhere)
int32 CurrentAmmo;
UPROPERTY(EditAnywhere)
FVector RandomLocation;
};
Character.cpp
void ATP_ThirdPersonCharacter::SaveLoadData(FArchive& Ar, float& HealthToSaveOrLoad, int32& CurrentAmmoToSaveOrLoad, FVector& PlayerLocationToSaveOrLoad)
{
//Save or load values
Ar << HealthToSaveOrLoad;
Ar << CurrentAmmoToSaveOrLoad;
Ar << PlayerLocationToSaveOrLoad;
Ar << *SaveActorRef;
}
bool ATP_ThirdPersonCharacter::SaveData()
{
FBufferArchive ToBinary;
SaveLoadData(ToBinary, Health, CurrentAmmo, RandomLocation);
//No data were saved
if (ToBinary.Num() <= 0) return false;
//Save binaries to disk
bool result = FFileHelper::SaveArrayToFile(ToBinary, TEXT(SAVEDATAFILENAME));
//Empty the buffer's contents
ToBinary.FlushCache();
ToBinary.Empty();
return result;
}
bool ATP_ThirdPersonCharacter::LoadData()
{
TArray<uint8> BinaryArray;
//load disk data to binary array
if (!FFileHelper::LoadFileToArray(BinaryArray, TEXT(SAVEDATAFILENAME))) return false;
if (BinaryArray.Num() <= 0) return false;
//Memory reader is the archive that we're going to use in order to read the loaded data
FMemoryReader FromBinary = FMemoryReader(BinaryArray, true);
FromBinary.Seek(0);
SaveLoadData(FromBinary, Health, CurrentAmmo, RandomLocation);
//Empty the buffer's contents
FromBinary.FlushCache();
BinaryArray.Empty();
//Close the stream
FromBinary.Close();
return true;
}
當按下Q的時候我們就保存,按下E的時候就加載數據。
當我們已經保存數據了,然後再把character的數據改動,此時再按下E,這時候character又變回了原來的數據。
UE4版本 4.20
參考: