序列化简述

342 阅读10分钟

定义以及相关概念

  • 由于在系统底层,数据的传输形式是简单的字节序列形式传递,即在底层,系统不认识对象,只认识字节序列,而为了达到进程通讯的目的,需要先将数据序列化,而序列化就是将对象转化字节序列的过程。相反地,当字节序列被运到相应的进程的时候,进程为了识别这些数据,就要将其反序 列化,即把字节序列转化为对象。

  • 无论是在进程间通信、本地数据存储又或者是网络数据传输都离不开序列化的支持。而针对不同场景选择合适的序列化方案对于应用的性能有着极大的影响。

  • 从广义上讲,数据序列化就是将数据结构或者是对象转换成我们可以存储或者传输的数据格式的一个过程,在序列化的过程中,数据结构或者对象将其状态信息写入到临时或者持久性的存储区中,而在对应的反序列化过程中,则可以说是生成的数据被还原成数据结构或对象的过程。

  • 这样来说,数据序列化相当于是将我们原先的对象序列化概念做出了扩展,在对象序列化和反序列化中,我们熟知的有两种方法,其一是Java语言中提供的Serializable接口,其二是Android提供的Parcelable接口。而在这里,因为我们对这个概念做出了扩展,因此也需要考虑几种专门针对数据结构进行序列化的方法,如现在那些个开放API一般返回的数据都是JSON格式的,又或者是我们Android原生的SQLite数据库来实现数据的本地存储,从广义上来说,这些都可以算做是数据的序列化。

序列化

将数据结构或对象转换成二进制串的过程。

反序列化

将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。

数据结构、对象与二进制串

  • 不同的计算机语言中,数据结构,对象以及二进制串的表示方式并不相同。
  • 数据结构和对象:对于类似 Java 这种完全面向对象的语言,工程师所操作的一切都是对象(Object),来自于类的实例化。
  • 在 Java 语言中最接近数据结构的概念,就是 POJO(Plain OldJavaObject)或者Javabean--那些只有setter/getter方法的类。而在 C 二进制串:序列化所生成的二进制串指的是存储在内存中的一块数据。C语言的字符串可以直接被传输层使用,因为其本质上就是以'0'结尾的存储在内存中的二进制串。在Java语言里面,二进制串的概念容易和String混淆。实际上String是Java的一等公民,是一种特殊对象(Object)。对于跨语言间的通讯,序列化后的数据当然不能是某种语言的特殊数据类型。二进制串在Java里面所指的是 byte[],byte 是 Java 的 8 中原生数据类型之一(Primitive data types)。

序列化/反序列化的目的

简单的概括

  • 序列化: 主要用于网络传输,数据持久化,一般序列化也称为编码(Encode)
  • 反序列化: 主要用于从网络,磁盘上读取字节数组还原成原始对象,一般反序列化也称为解码(Decode)

具体的讲:

  • 永久的保存对象数据(将对象数据保存在文件当中,或者是磁盘中)
  • 通过序列化操作将对象数据在网络上进行传输(由于网络传输是以字节流的方式对数据进行传输的.因此序列化的目的是将对象数据转换成字节流的形式)
  • 将对象数据在进程之间进行传递(Activity之间传递对象数据时,需要在当前的Activity中对对象数据进行序列化操作.在另一个Activity中需要进行反序列化操作讲数据取出)
  • Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长(即每个对象都在JVM中)
  • 但在现实应用中,就可能要停止JVM运行,但有要保存某些指定的对象,并在将来重新读取被保存的对象。这是Java对象序列化就能够实现该功能。(可选择入数据库、或文件的形式保存)
  • 序列化对象的时候只是针对变量进行序列化,不针对方法进行序列化.
  • 在Intent之间,基本的数据类型直接进行相关传递即可,但是一旦数据类型比较复杂的时候,就需要进行序列化操作了.

序列化协议特性

通用性

  • 技术层面,序列化协议是否支持跨平台、跨语言。如果不支持,在技术层面上的通用性就大大降低了。
  • 流行程度,序列化和反序列化需要多方参与,很少人使用的协议往往意味着昂贵的学习成本;另一方面,流行度低的协议,往往缺乏稳定而成熟的跨语言、跨平台的公共包。

强健性 / 鲁棒性

  • 成熟度不够
  • 语言 / 平台的不公平性

可调试性 / 可读性

  • 支持不到位
  • 访问限制

性能

性能包括两个方面,时间复杂度和空间复杂度。

  • 空间开销(Verbosity), 序列化需要在原有的数据上加上描述字段,以为反序列化解析之用。如果序列化过程引入的额外开销过高,可能会导致过大的网络,磁盘等各方面的压力。对于海量分布式存储系统,数据量往往以TB为单位,巨大的的额外空间开销意味着高昂的成本。
  • 时间开销(Complexity),复杂的序列化协议会导致较长的解析时间,这可能会使得序列化和反序列化阶段成为整个系统的瓶颈。

可扩展性 / 兼容性

移动互联时代,业务系统需求的更新周期变得更快,新的需求不断涌现,而老的系统还是需要继续维 护。如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大 提供系统的灵活度。

  • 因为访问限制而降低服务可用性;
  • 被迫重新实现安全协议而导致实施成本大大提高;
  • 开放更多的防火墙端口和协议访问,而牺牲安全性
  • 注意点:Android的Parcelable也有安全漏洞

最近几个月,Android安全公告公布了一系列系统框架层的高危提权漏洞,如下表所示。 www.anquanke.com/post/id/103…

几种常见的序列化和反序列化协议

XML&SOAP

XML 是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点,SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于 XML 为序列化和反序列化协议的结构化消息传递协议

JSON(Javascript Object Notation)

JSON 起源于弱类型语言 Javascript, 它的产生来自于一种称之为"Associative array"的概念,其本质是就是采用"Attribute-value"的方式来描述对象。实际上在 Javascript 和 PHP 等弱类型语言中,类的描述方式就是 Associative array。JSON 的如下优点,使得它快速成为最广泛使用的序列化协议之一。

  • 这种 Associative array 格式非常符合工程师对对象的理解。它保持了 XML 的人眼可读(Human-readable)的优点。
  • 相对于 XML 而言,序列化后的数据更加简洁。 来自于的以下链接的研究表明:XML 所产生序列化之后文件的大小接近 JSON 的两倍
  • 它具备 Javascript 的先天性支持,所以被广泛应用于 Web browser 的应用常景中,是 Ajax 的事实标准协议。
  • 与 XML 相比,其协议比较简单,解析速度比较快。
  • 松散的 Associative array 使得其具有良好的可扩展性和兼容性

Protobuf

Protobuf 具备了优秀的序列化协议的所需的众多典型特征。

  • 标准的 IDL 和 IDL 编译器,这使得其对工程师非常友好。
  • 序列化数据非常简洁,紧凑,与 XML 相比,其序列化之后的数据量约为 1/3 到 1/10。解析速度非常快,比对应的 XML 快约 20-100 倍。
  • 提供了非常友好的动态库,使用非常简介,反序列化只需要一行代码。

Android程序员该如何选择序列化方案

Serializable接口

是 Java 提供的序列化接口,它是一个空接口:
public interface Serializable { }

Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。

Serializable入门

public class Student implements Serializable {  
//serialVersionUID唯一标识了一个可序列化的类
private static final long serialVersionUID = -2100492893943893602L;   
private String name;  
private String sax;  
private Integer age;
//Course也需要实现Serializable接口
private List<Course> courses;
//用transient关键字标记的成员变量不参与序列化(在被反序列化后,transient 变量的值被
设为初始值,如 int 型的是 0,对象型的是 null) private transient Date createTime;
//静态成员变量属于类不属于对象,所以不会参与序列化(对象序列化保存的是对象的“状态”,也  就是它的成员变量,因此序列化不会关注静态变量)
private static SimpleDateFormat simpleDateFormat = new
SimpleDateFormat();
public Student() { 
System.out.println("Student: empty");
public Student(String name, String sax, Integer age) { System.out.println("Student: " + name + " " + sax + " " + age); this.name = name;
this.sax = sax; this.age = age;
courses = new ArrayList<>(); createTime = new Date();
}

private static final long serialVersionUID = 667279791530738499L; private String name;
private float score;
...
}
  • 可序列化类中,未实现 Serializable 的属性状态无法被序列化/反序列化
  • 也就是说,反序列化一个类的过程中,它的非可序列化的属性将会调用无参构造函数重新创建
  • 因此这个属性的无参构造函数必须可以访问,否者运行时会报错
  • 一个实现序列化的类,它的子类也是可序列化的

serialVersionUID与兼容性

  • serialVersionUID的作用
    serialVersionUID 用来表明类的不同版本间的兼容性。如果你修改了此类,要修改此值。否则以前用老版本的类序列化的类恢复时会报错: InvalidClassException
  • 设置方式 在JDK中,可以利用JDK的bin目录下的serialver.exe工具产生这个serialVersionUID,对于Test.class,执行命令:serialver Test
  • 兼容性问题

为了在反序列化时,确保类版本的兼容性,最好在每个要序列化的类中加入privat test satic final long serialVersionUID这个属性,具体数值自己定义。这样,即使某个类在与之对应的对象 已经序列化出去后做了修改,该对象依然可以被正确反序列化。否则,如果不显式定义该属性,这个属 性值将由JVM根据类的相关信息计算,而修改后的类的计算 结果与修改前的类的计算结果往往不同,从而造成对象的反序列化因为类版本不兼容而失败。

不显式定义这个属性值的另一个坏处是,不利于程序在不同的JVM之间的移植。因为不同的编译器实现该属性值的计算策略可能不同,从而造成虽然类没有改变,但是因为JVM不同,出现因类版本不兼容而无法正确反序列化的现象出现

因此 JVM 规范强烈 建议我们手动声明一个版本号,这个数字可以是随机的,只要固定不变就可以。同时最好是 private 和 final 的,尽量保证不变。