初探反序列化
有一套程序,分布运行在几台电脑上,电脑之间使用 Remoting 通信。这套程序内,有很多 Message 会按照一定的规则产生,然后在几台电脑之间传来传去。
1、要求
现在需要查看某个 Message 什么时候走到了哪台电脑,已经走过了多少台电脑。
2、属性准备
我们不添加其他的日志信息,直接在 Message 类上增加相关的属性来存储这些信息。
暂且在 Message 类上添加一个 int 类型的属性 Steps 用于表示类对象游历的电脑数目,添加一个 IDictionary<string, DateTime> 类型的属性 Logs 用于存储类对象游历到的每一台电脑的名称和时间。
/// <summary> /// 获取或设置游历的电脑数目 /// </summary> public int Steps { get; set; } private IDictionary<string, DateTime> _logs; /// <summary> /// 获取游历记录 /// </summary> public IDictionary<string, DateTime> Logs { get { if (null == _logs) { _logs = new Dictionary<string, DateTime>(); } return _logs; } }
3、 构造函数
能不能在不修改其他业务代码的前提下来实现这个要求呢?
当一个 Message 类对象 m1 在离开电脑 P1 准备去 P2 的时候,m1 的所有可序列化的成员变量信息会被序列化为二进制流 s1,然后 m1 会被回收;
而在电脑 P2 上,系统会根据二进制流 s1 的信息通过反序列化过程新创建出一个与 m1 的可序列化成员变量信息完全相同的 Message 类型对象 m2 来。
既然 m2 是新创建出来的,那么 Message 的构造函数能不能帮点忙呢?
但是,在反序列化对象的时候,类的常规构造函数根本就没有被调用,任何依赖于常规构造函数的代码根本不会被执行。
既然前面特意说明了是常规构造函数不会被调用,那么就有特殊规构造函数会被调用。这个特殊的构造函数如下所示:
private Message(SerializationInfo info, StreamingContext context) { //... }
这个构造函数是对应于 ISerializable 接口的 GetObjectData 方法而存在的。
public void GetObjectData(SerializationInfo info, StreamingContext context) { //... }
在类上实现 ISerializable 接口和添加非常规构造函数是需要完全自主控制类对象序列化行为(序列化哪些成员变量、序列化的顺序等)的推荐方式。
好,我们可以尝试使用这种方式:
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] private Message(SerializationInfo info, StreamingContext context) { // other fields deserialization Steps = info.GetInt32("_steps"); _logs = info.GetValue("_logs", typeof(Dictionary<string, DateTime>)) as Dictionary<string, DateTime>; Steps++; Logs[GetLocalHostName()] = DateTime.Now; } public void GetObjectData(SerializationInfo info, StreamingContext context) { // other fields serialization info.AddValue("_steps", Steps); info.AddValue("_logs", _logs, typeof(Dictionary<string, DateTime>)); }
但是, Message 类里面的其他的成员变量太多了,相对于仅使用 SerializableAttribute 来,这种方式显得太太繁琐了。而且用这种方式总觉得有点大材小用。
生成、测试!
Message 的对象 m1 由 P1 发送到 P2 变成 m2,成功!Steps 等于 1,Logs 里有 1 条记录!
m2 由 P2 发送到 P3……失败!抛出了 NullReferenceException !
通过调试,发现在 P3 上当 Message 的特殊构造函数被调用时,从参数 info 中获取到的 Steps 等于 1,也获取到了一个对应 _logs 属性的 Dictionary<string, DateTime> 类型对象,但是 _logs 的 Count 属性等于 0。在这个时候往 _logs 中添加新的键值对就会抛出 NullReferenceException!
4、监听反序列化完成事件
在 msdn 上的“自定义序列化”看到了 4 个特性:OnDeserializedAttribute、OnDeserializingAttribute、OnSerializedAttribute、OnSerializingAttribute,用于支持在对象的序列化前、序列化后、反序列化前和反序列化后 4 个环节对对象进行控制。
哦,看起来 OnDeserializedAttribute 正是我们需要的,立即应用如下:
[OnDeserialized] private void PostDeserializeThis(StreamingContext context) { Steps++; Logs[GetLocalHostName()] = DateTime.Now; }
立即生成、测试!
Message 的对象 m1 由 P1 发送到 P2 变成 m2,成功!Steps 等于 1,Logs 里有 1 条记录!
m2 由 P2 发送到 P3……失败!抛出了 NullReferenceException !
通过调试,发现在 P3 上当 PostDeserializeThis 方法被调用时,确实已经根据 m2 的流对象 s2 构造了一个新的对象 m3,而且 m3 的 Steps 也等于 1,_logs 属性也已经被初始化为一个 Dictionary<string, DateTime> 的对象,但是 _logs 的 Count 属性等于 0。在这个时候往 _logs 中添加新的键值对就会抛出 NullReferenceException!
5、实现 IDeserializationCallback 接口
再看 msdn,“自定义序列化”最后一段内容如下:
“对象是从内到外重新构造的;在反序列化期间调用方法时,可能会产生非预期的副作用,原因是调用的方法引用的可能是执行调用时尚未反序列化的对象引用。如果正被反序列化的类实现 IDeserializationCallback,则对整个对象图形进行反序列化之后,将会自动调用 OnDeserialization 方法。此时,便已完全还原引用的所有子对象。哈希表是类的典型示例,在不使用事件侦听器的情况下,很难对哈希表进行反序列化。虽然在反序列化期间易于检索键和值对,但是,将这些对象重新添加到哈希表时,可能会引起问题,原因是不能保证派生自哈希表的类已被反序列化。因此,不建议在这个阶段对哈希表调用方法。”
看来 OnDeserializedAttribute 还不能代表对象的所有引用子对象都已经被完全还原,似乎只有 IDeserializationCallback 的 OnDeserialization 方法被调用的时候才能够保证对象自身已经所有的子对象都能够被安全的使用。
msdn 上特意提到了哈希表是个典型示例,看来在反序列化时,哈希表是个奇怪的东西!或者像其他网友说的,这个现象是个 bug?
不说其他的,转移控制到 IDeserializationCallback 接口上来:
public void OnDeserialization(object sender) { Steps++; Logs[GetLocalHostName()] = DateTime.Now; }
立即生成、测试!
Message 的对象 m1 由 P1 发送到 P2 变成 m2,成功!!Steps 等于 1,Logs 里有 1 条记录!
m2 由 P2 发送到 P3……失败!!又抛出了 NullReferenceException !
再次调试,在 P3 上当 OnDeserialization 方法被调用时,确实已经根据 m2 的流对象 s2 构造了一个新的对象 m3,而且 m3 的 Steps 也等于 1,_logs 属性也已经被初始化为一个 Dictionary<string, DateTime> 的对象,但是 _logs 的 Count 属性等于 0。在这个时候往 _logs 中添加新的键值对还会抛出 NullReferenceException!
看来方法 OnDeserialization 被调用的时机还不是最佳时机!看来真是一个 bug。
6、参考 Dictionary 的实现
难道我对 msdn 上的文档内容的理解不正确?
让我来看看什么 _logs 都已经被创建出来了,添加的时候还是有问题。
使用 Reflector 查看了 Dictionary<TKey, TValue> 的常规构造函数和特殊构造函数的区别后,发现特殊构造函数仅仅构造了一个空对象,然后保存了 SerializationInfo 对象,其他常规构造函数里需要被初始化的很多成员变量都没理会。难怪添加新键值对时会抛出 NullReferenceException。
搜索了一下与哈希表反序列化相关的问题,发现很多解决方案都是在主对象里调用子字典对象的 OnDeserialization 方法来强制字典对象完成反序列化过程。原来字典对象的真正反序列化过程是在 OnDeserialization 方法中完成的。
只有当哈希表对象的 OnDeserialization 方法被调用了之后,哈希表自身的反序列化才算完成。
强调哈希表自身的反序列化完成,是因为哈希表中还可以包含哈希表子对象,同理,在这个时候不能保证这些子对象的反序列化已经全部完成。
7、解决方案
仍然使用实现 IDeserializationCallback 接口的方法:
public void OnDeserialization(object sender) { if (null != _logs) { (_logs as Dictionary<string, DateTime>).OnDeserialization(sender); } Steps++; Logs[GetLocalHostName()] = DateTime.Now; }
生成、测试!
Message 的对象 m1 由 P1 发送到 P2 变成 m2,成功!Steps 等于 1,Logs 里有 1 条记录!
m2 由 P2 发送到 P3 变成 m3,成功!!Steps 等于 2,Logs 里有 2 条记录!
……
问题解决!
8、其他
包含子对象的对象的序列化和反序列化过程:
OnSerializingAttribute
->子对象的 OnSerializingAttribute
GetObjectData
->子对象的 GetObjectData
OnSerializedAttribute
->子对象的 OnSerializedAttribute
OnDeserializingAttribute
->子对象的 OnDeserializingAttribute
特殊构造函数
->子对象的特殊构造函数
OnDeserializedAttributeu
->子对象的 OnDeserializedAttribute
IDeserializationCallback.OnDeserialization
->子对象的 IDeserializationCallback.OnDeserialization
只有在子对象的序列化完成事件、反序列化完成事件和IDeserializationCallback.OnDeserialization 方法在主对象的相关事件和方法前面被调用,才能保证主对象序列化完成后整个对象都被完全序列化了,主反序列化完成之后主对象和子对象是完全被复原了,才是可用的。
造成这种现象的原因应该是:
在反序列化对象的时候,从流中最先读出来的就是主对象,就先把主对象自身的所有监听事件的方法和回调函数全部合并到对应的委托中;然后读到子对象,再往对应的委托中合并相关的事件处理方法和回调函数……
当触发某一事件或调用回调函数时,顺序调用对应委托的执行列表中的委托,从而造成主对象的所有事件处理和回调都在子对象的前面。