一、什麼是運行時序列化 序列化的作用就是將對象圖(特定時間點的對象連接圖)轉換為位元組流,這樣這些對象圖就可以在文件系統/網路進行傳輸。 二、序列化/反序列化快速入門 一般來說我們通過 FCL 提供的 對象就可以將一個對象序列化為位元組流進行存儲,或者通過該 Formatter 將一個位元組流反序列化為一 ...
一、什麼是運行時序列化
序列化的作用就是將對象圖(特定時間點的對象連接圖)轉換為位元組流,這樣這些對象圖就可以在文件系統/網路進行傳輸。
二、序列化/反序列化快速入門
一般來說我們通過 FCL 提供的 BinaryFormatter
對象就可以將一個對象序列化為位元組流進行存儲,或者通過該 Formatter 將一個位元組流反序列化為一個對象。
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()
方法返回序列化好的對象圖的根對象的一個引用。
深拷貝
通過序列化與反序列化的特性,可以實現一個深拷貝的方法,用戶創建源對象的一個克隆體。
public object DeepClone(object originalObj)
{
using (var memoryStream = new MemoryStream())
{
var formatter = new BinaryFormatter();
formatter.Serialize(memoryStream, originalObj);
// 表明對象是被克隆的,可以安全的訪問其他托管資源
formatter.Context = new StreamingContext(StreamingContextStates.Clone);
memoryStream.Position = 0;
return formatter.Deserialize(memoryStream);
}
}
另外一種技巧就是可以將多個對象圖序列化到一個流當中,即調用多次 Serialize()
方法將多個對象圖序列化到流當中。如果需要反序列化的時候,按照序列化時對象圖的序列化順序反向反序列化即可。
BinaryFormatter
在序列化的時候會將類型的全名與程式集定義寫入到流當中,這樣在反序列化的時候,格式化器會獲取這些信息,並且通過 System.Reflection.Assembly.Load()
方法將程式集載入到當前的 AppDomain
。
在程式集載入完成之後,會在該程式集搜索待反序列化的對象圖類型,找不到則會拋出異常。
【註意】
某些應用程式通過
Assembly.LoadFrom()
來載入程式集,然後根據程式集中的類型來構造對象。序列化該對象是沒問題的,但是反序列化的時候格式化器使用的是Assembly.Load()
方法來載入程式集,這樣的話就會導致無法正確載入對象。這個時候,你可以實現一個與
System.ResolveEventHandler
簽名一樣的委托,並且在反序列化註冊到當前AppDomain
的AssemblyResolve
事件。這樣當程式集載入失敗的時候,你可以在該方法內部根據傳入的事件參數與程式集標識自己使用
Assembly.LoadFrom()
來構造一個Assembly
對象。記得在反序列化完成之後,馬上向事件註銷這個方法,否則會造成記憶體泄漏。
三、使類型可序列化
在設計自定義類型時,你需要顯式地通過 Serializable
特性來聲明你的類型是可以被序列化的。如果沒有這麼做,在使用格式化器進行序列化的時候,則會拋出異常。
[Serializable]
public class DIYClass
{
public int x { get; set; }
public int y { get; set; }
}
【註意】
正因為這樣,我們一般都會現將結果保存到
MemoryStream
之中,當沒有拋出異常之後再將這些數據寫入到文件/網路。
Serializable 特性
Serializable
特性只能用於值類型、引用類型、枚舉類型(預設)、委托類型(預設),而且是不可被子類繼承。
如果有一個 A 類與其派生類 B 類,那麼 A 類沒擁有 Serializable
特性,而子類擁有,一樣的是無法進行序列化操作。
而且序列化的時候,是將所有訪問級別的欄位成員都進行了序列化,包括 private 級別成員。
四、簡單控制序列化操作
禁止序列化某個欄位
可以通過 System.NonSerializedAttribute
特性來確保某個欄位在序列化時不被處理其值,例如下列代碼:
[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;
}
在序列化之前,該自定義對象 z 欄位的值為 1000,在序列化時,檢測到了忽略特性,則不會寫入該欄位的值到流當中。並且在反序列化之後,z 的值為 0,而 x ,y 的值是 10 和 100。
序列化與反序列化的四個生命周期特性
通過 OnSerializing
、OnSerialized
、OnDeserializing
、OnDeserialized
這四個特性,我們可以在對象序列化與反序列化時進行一些自定義的控制。只需要將這四個特性分別加在四個方法上面即可,但是針對方法簽名必須返回值為 void,同時也需要用有一個 StreamingContext
參數。
而且一般建議將這四個方法標識為 private ,防止其他對象誤調用。
[Serializable]
public class DIYClass
{
[OnDeserializing]
private void OnDeserializing(StreamingContext context)
{
Console.WriteLine("反序列化的時候,會調用本方法.");
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
Console.WriteLine("反序列化完成的時候,會調用本方法.");
}
[OnSerializing]
public void OnSerializing(StreamingContext context)
{
Console.WriteLine("序列化的時候,會調用本方法.");
}
[OnSerialized]
public void OnSerialized(StreamingContext context)
{
Console.WriteLine("序列化完成的時候,會調用本方法.");
}
}
【註意】
如果 A 類型有兩個版本,第 1 個版本有 5 個欄位,並被序列化存儲到了文件當中。後面由於業務需要,針對於 A 類型增加了 2 個新的欄位,這個時候如果從文件中讀取第 1 個版本的對象流信息,就會拋出異常。
我們可以通過
System.Runtime.Serialization.OptionalFieldAttribute
添加到我們新加的欄位之上,這樣的話在反序列化數據時就不會因為缺少欄位而拋出異常。
五、格式化器的序列化原理
格式化器的核心就是 FCL 提供的 FormatterServices
的靜態工具類,下列步驟體現了序列化器如何結合 FormatterServices
工具類來進行序列化操作的。
- 格式化器調用
FormatterService.GetSerializableMembers()
方法獲得需要序列化的欄位構成的MemberInfo
數組。 - 格式化器調用
FormatterService.GetObjectData()
方法,通過之前獲取的欄位MethodInfo
信息來取得每個欄位存儲的值數組。該數組與欄位信息數組是並行的,下標一致。 - 格式化器寫入類型的程式集等信息。
- 遍歷兩個數組,寫入欄位信息與其數據到流當中。
反序列化操作的步驟與上面相反。
- 首先從流頭部讀取程式集標識與類型信息,如果當前 AppDomain 沒有載入該程式集會拋出異常。如果類型的程式集已經載入,則通過
FormatterServices.GetTypeFromAssembly()
方法來構造一個 Type 對象。 - 格式化器調用
FormatterService.GetUninitializedObject()
方法為新對象分配記憶體,但是 不會調用對象的構造器。 - 格式化器通過
FormatterService.GetSerializableMembers()
初始化一個MemberInfo
數組。 - 格式化器根據流中的數據創建一個 Object 數組,該數組就是欄位的數據。
- 格式化器通過
FormatterService.PopulateObjectMembers()
方法,傳入新分配的對象、欄位信息數組、欄位數據數組進行對象初始化。
六、控制序列化/反序列化的數據
一般來說通過在第四節說的那些特性控制就已經滿足了大部分需求,但格式化器內部使用的是反射,反射性能開銷比較大,如果你想要針對序列化/反序列化進行完全的控制,那麼你可以實現 ISerializable
介面來進行控制。
該介面只提供了一個 GetObjectData()
方法,原型如下:
public interface ISerializable{
void GetObjectData(SerializationInfo info,StreamingContext context);
}
【註意】
使用了
ISerializable
介面的代價就是其集成類都必須實現它,而且還要保證子類必須調用基類的GetObjectData()
方法與其構造函數。一般來說密封類才使用ISerializable
,其他的類型使用特性控制即可滿足。另外為了防止其他的代碼調用
GetObjectData()
方法,可以通過一下特性來防止誤操作:[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter = true)]
如果格式化器檢測到了類型實現了該介面,則會忽略掉原有的特性,並且將欄位值傳入到 SerializationInfo
之中。
通過這個 Info 我們可以被序列化的類型,因為 Info 提供了 FullTypeName
與 AssemblyName
,不過一般推薦使用該對象提供的 SetType(Type type)
方法來進行操作。
格式化器構造完成 Info 之後,則會調用 GetObjectData()
方法,這個時候將之前構造好的 Info 傳入,而該方法則決定需要用哪些數據來序列化對象。這個時候我們就可以通過 Info 的 AddValue()
方法來添加一些信息用於反序列化時使用。
在反序列化的時候,需要類型提供一個特殊的構造函數,對於密封類來說,該構造函數推薦為 private ,而一般的類型推薦為 protected,這個特殊的構造函數方法簽名與 GetObjectData()
一樣。
因為在反序列化的時候,格式化器會調用這個特殊的構造函數。
以下代碼就是一個簡單實踐:
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);
}
}
該類型的對象在反序列化之後,X 的值為序列化之前的值,而 Y 的值始終都會為 20。
【註意】
如果你存儲的 X 值是 Int32 ,而在獲取的時候是通過 GetInt64() 進行獲取。那麼格式化器就會嘗試使用
System.Convert
提供的方法進行轉換,並且可以通過實現IConvertible
介面來自定義自己的轉換。不過只有在 Get 方法轉換失敗的情況下才會使用上述機制。
子類與基類的 ISerializable
如果某個子類集成了基類,那麼子類在其 GetObjectData()
與特殊構造器中都要調用父類的方法,這樣才能夠完成正確的序列化/反序列化操作。
如果基類沒有實現 ISerializable
介面與特殊的構造器,那麼子類就需要通過 FormatterService
來手動針對基類的欄位進行賦值。
七、流上下文
流上下文 StreamingContext
只有兩個屬性,第一個是狀態標識位,用於標識序列化/反序列化對象的來源與目的地。而第二個屬性就是一個 Object 引用,該引用則是一個附加的上下文信息,由用戶進行提供。
八、類型序列化為不同的類型與對象反序列化為不同的對象
在某些時候可能需要更改序列化完成之後的對象類型,這個時候只需要對象在其實現 ISerializable
介面的 GetObjectData()
方法內部通過 SerializationInfo
的 SetType()
方法變更了序列化的目標類型。
下麵的代碼演示瞭如何序列化一個單例對象:
[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));
}
}
這裡通過顯式實現介面的 GetObjectData()
方法來將序列化的目標類型設置為 SingletonHelper
,該類型的定義如下:
[Serializable]
public class SingletonHelper : IObjectReference
{
public object GetRealObject(StreamingContext context)
{
return Singleton.GetSingleton();
}
}
這裡因為 SingletonHelper
實現了 IObjectReference
介面,當格式化器嘗試進行反序列化的時候,由於在 GetObjectData()
欺騙了轉換器,因此反序列化的時候檢測到類型有實現該介面,所以會嘗試調用其 GetRealObject()
方法來進行反序列化操作。
而以上動作完成之後,SingletonHelper
會立即變為不可達對象,等待 GC 進行回收處理。
九、序列化代理
當某些時候需要對一個第三方庫對象進行序列化的時候,沒有其源碼,但是想要進行序列化,則可以通過序列化代理來進行序列化操作。
要實現序列化代理,需要實現 ISerializationSurrogate
介面,該介面擁有兩個方法,其簽名分別如下:
void GetObjectData(Object obj,SerializationInfo info,StreamingContext context);
void SetObjectData(Object obj,SerializationInfo info,StreamingContext context,ISurrogateSelector selector);
GetObjectData()
方法會在對象序列化時進行調用,而 SetObjectData()
會在對象反序列化時調用。
比如說我們有一個需求是希望 DateTime
類型在序列化的時候通過 UTC 時間序列化到流中,而在反序列化時則更改為本地時間。
這個時候我們就可以自己實現一個序列化代理類 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();
}
}
並且在使用的時候,通過構造一個 SurrogateSelector
代理選擇器,傳入我們針對於 DateTime
類型的代理,並且將格式化器與代理選擇器相綁定。那麼在使用格式化器的時候,就會通過我們的代理類來處理 DateTime
類型對象的序列化/反序列化操作了。
static void Main(string[] args)
{
using (var stream = new MemoryStream())
{
var formatter = new BinaryFormatter();
// 創建一個代理選擇器
var ss = new SurrogateSelector();
// 告訴代理選擇器,針對於 DateTime 類型採用 UTCToLocal 代理類進行序列化/反序列化代理
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();
}
而一個代理選擇器允許綁定多個代理類,選擇器內部維護一個哈希表,通過 Type
與 StreamingContext
作為其鍵來進行搜索,通過 StreamintContext
地不同可以方便地為 DateTime
類型綁定不同用途的代理類。
十、反序列化對象時重寫程式集/類型
通過繼承 SerializationBinder
抽象類,我們可以很方便地實現類型反序列化時轉化為不同的類型,該抽象類有一個 Type BindToType(String assemblyName,String typeName)
方法。
重寫該方法你就可以在對象反序列化時,通過傳入的兩個參數來構造自己需要返回的真實類型。第一個參數是程式集名稱,第二個參數是格式化器想要反序列化時轉換的類型。
編寫好 Binder 類重寫該方法之後,在格式化器的 Binder
屬性當中綁定你的 Binder 類即可。
【註意】
抽象類還有一個
BindToName()
方法,該方法是在序列化時被調用,會傳入他想要序列化的類型。