一、什么是運(yùn)行時(shí)序列化
序列化的作用就是將對(duì)象圖(特定時(shí)間點(diǎn)的對(duì)象連接圖)轉(zhuǎn)換為字節(jié)流,這樣這些對(duì)象圖就可以在文件系統(tǒng)/網(wǎng)絡(luò)進(jìn)行傳輸。
二、序列化/反序列化快速入門
一般來說我們通過 FCL 提供的 BinaryFormatter 對(duì)象就可以將一個(gè)對(duì)象序列化為字節(jié)流進(jìn)行存儲(chǔ),或者通過該 Formatter
將一個(gè)字節(jié)流反序列化為一個(gè)對(duì)象。
FCL 的序列化與反序列化
序列化操作:
public MemoryStream SerializeObj(object sourceObj) { var memStream = new
MemoryStream(); var formatter = new BinaryFormatter();
formatter.Serialize(memStream, sourceObj); return memStream; }
反序列化操作:
public object DeserializeFromStream(MemoryStream stream) { var formatter = new
BinaryFormatter(); stream.Position = 0; return formatter.Deserialize(stream); }
反序列化通過 Formatter 的 Deserialize() 方法返回序列化好的對(duì)象圖的根對(duì)象的一個(gè)引用。
深拷貝
通過序列化與反序列化的特性,可以實(shí)現(xiàn)一個(gè)深拷貝的方法,用戶創(chuàng)建源對(duì)象的一個(gè)克隆體。
public object DeepClone(object originalObj) { using (var memoryStream = new
MemoryStream()) { var formatter = new BinaryFormatter();
formatter.Serialize(memoryStream, originalObj); // 表明對(duì)象是被克隆的,可以安全的訪問其他托管資源
formatter.Context = new StreamingContext(StreamingContextStates.Clone);
memoryStream.Position = 0; return formatter.Deserialize(memoryStream); } }
另外一種技巧就是可以將多個(gè)對(duì)象圖序列化到一個(gè)流當(dāng)中,即調(diào)用多次 Serialize()
方法將多個(gè)對(duì)象圖序列化到流當(dāng)中。如果需要反序列化的時(shí)候,按照序列化時(shí)對(duì)象圖的序列化順序反向反序列化即可。
BinaryFormatter 在序列化的時(shí)候會(huì)將類型的全名與程序集定義寫入到流當(dāng)中,這樣在反序列化的時(shí)候,格式化器會(huì)獲取這些信息,并且通過
System.Reflection.Assembly.Load() 方法將程序集加載到當(dāng)前的 AppDomain。
在程序集加載完成之后,會(huì)在該程序集搜索待反序列化的對(duì)象圖類型,找不到則會(huì)拋出異常。
【注意】
某些應(yīng)用程序通過 Assembly.LoadFrom()
來加載程序集,然后根據(jù)程序集中的類型來構(gòu)造對(duì)象。序列化該對(duì)象是沒問題的,但是反序列化的時(shí)候格式化器使用的是Assembly.Load()
方法來加載程序集,這樣的話就會(huì)導(dǎo)致無法正確加載對(duì)象。
這個(gè)時(shí)候,你可以實(shí)現(xiàn)一個(gè)與 System.ResolveEventHandler 簽名一樣的委托,并且在反序列化注冊(cè)到當(dāng)前 AppDomain 的
AssemblyResolve 事件。
這樣當(dāng)程序集加載失敗的時(shí)候,你可以在該方法內(nèi)部根據(jù)傳入的事件參數(shù)與程序集標(biāo)識(shí)自己使用 Assembly.LoadFrom() 來構(gòu)造一個(gè) Assembly
對(duì)象。
記得在反序列化完成之后,馬上向事件注銷這個(gè)方法,否則會(huì)造成內(nèi)存泄漏。
三、使類型可序列化
在設(shè)計(jì)自定義類型時(shí),你需要顯式地通過 Serializable
特性來聲明你的類型是可以被序列化的。如果沒有這么做,在使用格式化器進(jìn)行序列化的時(shí)候,則會(huì)拋出異常。
[Serializable] public class DIYClass { public int x { get; set; } public int y
{ get; set; } }
【注意】
正因?yàn)檫@樣,我們一般都會(huì)現(xiàn)將結(jié)果保存到 MemoryStream 之中,當(dāng)沒有拋出異常之后再將這些數(shù)據(jù)寫入到文件/網(wǎng)絡(luò)。
Serializable 特性
Serializable 特性只能用于值類型、引用類型、枚舉類型(默認(rèn))、委托類型(默認(rèn)),而且是不可被子類繼承。
如果有一個(gè) A 類與其派生類 B 類,那么 A 類沒擁有 Serializable 特性,而子類擁有,一樣的是無法進(jìn)行序列化操作。
而且序列化的時(shí)候,是將所有訪問級(jí)別的字段成員都進(jìn)行了序列化,包括 private 級(jí)別成員。
四、簡(jiǎn)單控制序列化操作
禁止序列化某個(gè)字段
可以通過 System.NonSerializedAttribute 特性來確保某個(gè)字段在序列化時(shí)不被處理其值,例如下列代碼:
[Serializable] public class DIYClass { public DIYClass() { x = 10; y = 100; z
= 1000; } public int x { get; set; } public int y { get; set; } [NonSerialized]
public int z; }
在序列化之前,該自定義對(duì)象 z 字段的值為 1000,在序列化時(shí),檢測(cè)到了忽略特性,則不會(huì)寫入該字段的值到流當(dāng)中。并且在反序列化之后,z 的值為 0,而 x
,y 的值是 10 和 100。
序列化與反序列化的四個(gè)生命周期特性
通過 OnSerializing 、OnSerialized、OnDeserializing、OnDeserialized
這四個(gè)特性,我們可以在對(duì)象序列化與反序列化時(shí)進(jìn)行一些自定義的控制。只需要將這四個(gè)特性分別加在四個(gè)方法上面即可,但是針對(duì)方法簽名必須返回值為
void,同時(shí)也需要用有一個(gè)StreamingContext 參數(shù)。
而且一般建議將這四個(gè)方法標(biāo)識(shí)為 private ,防止其他對(duì)象誤調(diào)用。
[Serializable] public class DIYClass { [OnDeserializing] private void
OnDeserializing(StreamingContext context) {
Console.WriteLine("反序列化的時(shí)候,會(huì)調(diào)用本方法."); } [OnDeserialized] private void
OnDeserialized(StreamingContext context) {
Console.WriteLine("反序列化完成的時(shí)候,會(huì)調(diào)用本方法."); } [OnSerializing] public void
OnSerializing(StreamingContext context) { Console.WriteLine("序列化的時(shí)候,會(huì)調(diào)用本方法.");
} [OnSerialized] public void OnSerialized(StreamingContext context) {
Console.WriteLine("序列化完成的時(shí)候,會(huì)調(diào)用本方法."); } }
【注意】
如果 A 類型有兩個(gè)版本,第 1 個(gè)版本有 5 個(gè)字段,并被序列化存儲(chǔ)到了文件當(dāng)中。后面由于業(yè)務(wù)需要,針對(duì)于 A 類型增加了 2
個(gè)新的字段,這個(gè)時(shí)候如果從文件中讀取第 1 個(gè)版本的對(duì)象流信息,就會(huì)拋出異常。
我們可以通過 System.Runtime.Serialization.OptionalFieldAttribute
添加到我們新加的字段之上,這樣的話在反序列化數(shù)據(jù)時(shí)就不會(huì)因?yàn)槿鄙僮侄味鴴伋霎惓!?br>
五、格式化器的序列化原理
格式化器的核心就是 FCL 提供的 FormatterServices 的靜態(tài)工具類,下列步驟體現(xiàn)了序列化器如何結(jié)合 FormatterServices
工具類來進(jìn)行序列化操作的。
* 格式化器調(diào)用 FormatterService.GetSerializableMembers() 方法獲得需要序列化的字段構(gòu)成的 MemberInfo
數(shù)組。
* 格式化器調(diào)用 FormatterService.GetObjectData() 方法,通過之前獲取的字段 MethodInfo
信息來取得每個(gè)字段存儲(chǔ)的值數(shù)組。該數(shù)組與字段信息數(shù)組是并行的,下標(biāo)一致。
* 格式化器寫入類型的程序集等信息。
* 遍歷兩個(gè)數(shù)組,寫入字段信息與其數(shù)據(jù)到流當(dāng)中。
反序列化操作的步驟與上面相反。
* 首先從流頭部讀取程序集標(biāo)識(shí)與類型信息,如果當(dāng)前 AppDomain 沒有加載該程序集會(huì)拋出異常。如果類型的程序集已經(jīng)加載,則通過
FormatterServices.GetTypeFromAssembly() 方法來構(gòu)造一個(gè) Type 對(duì)象。
* 格式化器調(diào)用 FormatterService.GetUninitializedObject() 方法為新對(duì)象分配內(nèi)存,但是 不會(huì)調(diào)用對(duì)象的構(gòu)造器。
* 格式化器通過 FormatterService.GetSerializableMembers() 初始化一個(gè) MemberInfo 數(shù)組。
* 格式化器根據(jù)流中的數(shù)據(jù)創(chuàng)建一個(gè) Object 數(shù)組,該數(shù)組就是字段的數(shù)據(jù)。
* 格式化器通過 FormatterService.PopulateObjectMembers()
方法,傳入新分配的對(duì)象、字段信息數(shù)組、字段數(shù)據(jù)數(shù)組進(jìn)行對(duì)象初始化。
六、控制序列化/反序列化的數(shù)據(jù)
一般來說通過在第四節(jié)說的那些特性控制就已經(jīng)滿足了大部分需求,但格式化器內(nèi)部使用的是反射,反射性能開銷比較大,如果你想要針對(duì)序列化/反序列化進(jìn)行完全的控制,那么你可以實(shí)現(xiàn)
ISerializable 接口來進(jìn)行控制。
該接口只提供了一個(gè) GetObjectData() 方法,原型如下:
public interface ISerializable{ void GetObjectData(SerializationInfo
info,StreamingContext context); }
【注意】
使用了 ISerializable 接口的代價(jià)就是其集成類都必須實(shí)現(xiàn)它,而且還要保證子類必須調(diào)用基類的 GetObjectData()
方法與其構(gòu)造函數(shù)。一般來說密封類才使用ISerializable ,其他的類型使用特性控制即可滿足。
另外為了防止其他的代碼調(diào)用 GetObjectData() 方法,可以通過一下特性來防止誤操作:
[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter =
true)]
如果格式化器檢測(cè)到了類型實(shí)現(xiàn)了該接口,則會(huì)忽略掉原有的特性,并且將字段值傳入到 SerializationInfo 之中。
通過這個(gè) Info 我們可以被序列化的類型,因?yàn)?Info 提供了 FullTypeName 與 AssemblyName,不過一般推薦使用該對(duì)象提供的
SetType(Type type) 方法來進(jìn)行操作。
格式化器構(gòu)造完成 Info 之后,則會(huì)調(diào)用 GetObjectData() 方法,這個(gè)時(shí)候?qū)⒅皹?gòu)造好的 Info
傳入,而該方法則決定需要用哪些數(shù)據(jù)來序列化對(duì)象。這個(gè)時(shí)候我們就可以通過 Info 的AddValue() 方法來添加一些信息用于反序列化時(shí)使用。
在反序列化的時(shí)候,需要類型提供一個(gè)特殊的構(gòu)造函數(shù),對(duì)于密封類來說,該構(gòu)造函數(shù)推薦為 private ,而一般的類型推薦為
protected,這個(gè)特殊的構(gòu)造函數(shù)方法簽名與GetObjectData() 一樣。
因?yàn)樵诜葱蛄谢臅r(shí)候,格式化器會(huì)調(diào)用這個(gè)特殊的構(gòu)造函數(shù)。
以下代碼就是一個(gè)簡(jiǎn)單實(shí)踐:
public class DIYClass : ISerializable { public int X { get; set; } public int
Y { get; set; } public DIYClass() { } protected DIYClass(SerializationInfo
info, StreamingContext context) { X = info.GetInt32("X"); Y = 20; } public void
GetObjectData(SerializationInfo info, StreamingContext context) {
info.AddValue("X", 10); } }
該類型的對(duì)象在反序列化之后,X 的值為序列化之前的值,而 Y 的值始終都會(huì)為 20。
【注意】
如果你存儲(chǔ)的 X 值是 Int32 ,而在獲取的時(shí)候是通過 GetInt64() 進(jìn)行獲取。那么格式化器就會(huì)嘗試使用 System.Convert
提供的方法進(jìn)行轉(zhuǎn)換,并且可以通過實(shí)現(xiàn)IConvertible 接口來自定義自己的轉(zhuǎn)換。
不過只有在 Get 方法轉(zhuǎn)換失敗的情況下才會(huì)使用上述機(jī)制。
子類與基類的 ISerializable
如果某個(gè)子類集成了基類,那么子類在其 GetObjectData() 與特殊構(gòu)造器中都要調(diào)用父類的方法,這樣才能夠完成正確的序列化/反序列化操作。
如果基類沒有實(shí)現(xiàn) ISerializable 接口與特殊的構(gòu)造器,那么子類就需要通過 FormatterService 來手動(dòng)針對(duì)基類的字段進(jìn)行賦值。
七、流上下文
流上下文 StreamingContext 只有兩個(gè)屬性,第一個(gè)是狀態(tài)標(biāo)識(shí)位,用于標(biāo)識(shí)序列化/反序列化對(duì)象的來源與目的地。而第二個(gè)屬性就是一個(gè) Object
引用,該引用則是一個(gè)附加的上下文信息,由用戶進(jìn)行提供。
八、類型序列化為不同的類型與對(duì)象反序列化為不同的對(duì)象
在某些時(shí)候可能需要更改序列化完成之后的對(duì)象類型,這個(gè)時(shí)候只需要對(duì)象在其實(shí)現(xiàn) ISerializable 接口的 GetObjectData() 方法內(nèi)部通過
SerializationInfo 的 SetType() 方法變更了序列化的目標(biāo)類型。
下面的代碼演示了如何序列化一個(gè)單例對(duì)象:
[Serializable] public sealed class Singleton : ISerializable { private static
readonly Singleton _instance = new Singleton(); private Singleton() { } public
static Singleton GetSingleton() { return _instance; }
[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter
=true)] void ISerializable.GetObjectData(SerializationInfo info,
StreamingContext context) { info.SetType(typeof(SingletonHelper)); } }
這里通過顯式實(shí)現(xiàn)接口的 GetObjectData() 方法來將序列化的目標(biāo)類型設(shè)置為 SingletonHelper ,該類型的定義如下:
[Serializable] public class SingletonHelper : IObjectReference { public object
GetRealObject(StreamingContext context) { return Singleton.GetSingleton(); } }
這里因?yàn)?SingletonHelper 實(shí)現(xiàn)了 IObjectReference 接口,當(dāng)格式化器嘗試進(jìn)行反序列化的時(shí)候,由于在
GetObjectData() 欺騙了轉(zhuǎn)換器,因此反序列化的時(shí)候檢測(cè)到類型有實(shí)現(xiàn)該接口,所以會(huì)嘗試調(diào)用其 GetRealObject()
方法來進(jìn)行反序列化操作。
而以上動(dòng)作完成之后,SingletonHelper 會(huì)立即變?yōu)椴豢蛇_(dá)對(duì)象,等待 GC 進(jìn)行回收處理。
九、序列化代理
當(dāng)某些時(shí)候需要對(duì)一個(gè)第三方庫對(duì)象進(jìn)行序列化的時(shí)候,沒有其源碼,但是想要進(jìn)行序列化,則可以通過序列化代理來進(jìn)行序列化操作。
要實(shí)現(xiàn)序列化代理,需要實(shí)現(xiàn) ISerializationSurrogate 接口,該接口擁有兩個(gè)方法,其簽名分別如下:
void GetObjectData(Object obj,SerializationInfo info,StreamingContext
context); void SetObjectData(Object obj,SerializationInfo info,StreamingContext
context,ISurrogateSelector selector);
GetObjectData() 方法會(huì)在對(duì)象序列化時(shí)進(jìn)行調(diào)用,而 SetObjectData() 會(huì)在對(duì)象反序列化時(shí)調(diào)用。
比如說我們有一個(gè)需求是希望 DateTime 類型在序列化的時(shí)候通過 UTC 時(shí)間序列化到流中,而在反序列化時(shí)則更改為本地時(shí)間。
這個(gè)時(shí)候我們就可以自己實(shí)現(xiàn)一個(gè)序列化代理類 UTCToLocalTimeSerializationSurrogate:
public sealed class UTCToLocalTimeSerializationSurrogate :
ISerializationSurrogate { public void GetObjectData(object obj,
SerializationInfo info, StreamingContext context) { info.AddValue("Date",
((DateTime)obj).ToUniversalTime().ToString("u")); } public object
SetObjectData(object obj, SerializationInfo info, StreamingContext context,
ISurrogateSelector selector) { return
DateTime.ParseExact(info.GetString("Date"), "u", null).ToLocalTime(); } }
并且在使用的時(shí)候,通過構(gòu)造一個(gè) SurrogateSelector 代理選擇器,傳入我們針對(duì)于 DateTime
類型的代理,并且將格式化器與代理選擇器相綁定。那么在使用格式化器的時(shí)候,就會(huì)通過我們的代理類來處理DateTime 類型對(duì)象的序列化/反序列化操作了。
static void Main(string[] args) { using (var stream = new MemoryStream()) {
var formatter = new BinaryFormatter(); // 創(chuàng)建一個(gè)代理選擇器 var ss = new
SurrogateSelector(); // 告訴代理選擇器,針對(duì)于 DateTime 類型采用 UTCToLocal 代理類進(jìn)行序列化/反序列化代理
ss.AddSurrogate(typeof(DateTime), formatter.Context, new
UTCToLocalTimeSerializationSurrogate()); // 綁定代理選擇器 formatter.SurrogateSelector
= ss; formatter.Serialize(stream,DateTime.Now); stream.Position = 0; var
oldValue = new StreamReader(stream).ReadToEnd(); stream.Position = 0; var
newValue = (DateTime)formatter.Deserialize(stream);
Console.WriteLine(oldValue); Console.WriteLine(newValue); } Console.ReadLine();
}
而一個(gè)代理選擇器允許綁定多個(gè)代理類,選擇器內(nèi)部維護(hù)一個(gè)哈希表,通過 Type 與 StreamingContext 作為其鍵來進(jìn)行搜索,通過
StreamintContext 地不同可以方便地為 DateTime 類型綁定不同用途的代理類。
十、反序列化對(duì)象時(shí)重寫程序集/類型
通過繼承 SerializationBinder 抽象類,我們可以很方便地實(shí)現(xiàn)類型反序列化時(shí)轉(zhuǎn)化為不同的類型,該抽象類有一個(gè) Type
BindToType(String assemblyName,String typeName) 方法。
重寫該方法你就可以在對(duì)象反序列化時(shí),通過傳入的兩個(gè)參數(shù)來構(gòu)造自己需要返回的真實(shí)類型。第一個(gè)參數(shù)是程序集名稱,第二個(gè)參數(shù)是格式化器想要反序列化時(shí)轉(zhuǎn)換的類型。
編寫好 Binder 類重寫該方法之后,在格式化器的 Binder 屬性當(dāng)中綁定你的 Binder 類即可。
【注意】
抽象類還有一個(gè) BindToName() 方法,該方法是在序列化時(shí)被調(diào)用,會(huì)傳入他想要序列化的類型。
熱門工具 換一換