文章目录
- 一、什么是序列化?为什么要序列化?怎么进行序列化?
- 二、Serializable
- 2.1 序列化举例
- 2.2 重写readObject、writeObject、readResolve、writeReplace
- 2.3 serialVersionUID
- 2.4 实现原理
- 2.5 Externalizable
- 三、Parcelable
- 3.1 序列化举例
- 3.2 实现原理
- 四、Parcelable、Serializable比较
- 4.1 效率对比
- 4.2 容错率对比
- 五、总结
- 六、参考
一、什么是序列化?为什么要序列化?怎么进行序列化?
序列化定义:将一个类对象转换成可存储、可传输状态的过程。序列化有两个过程:
1、序列化:将对象编码成字节流(serializing)
2、反序列化:从字节流编码中重新构建对象(deserializing)。对象序列化后,可以在进程内/进程间、网络间进行传输,也可以做本地持久化存储。
为什么要序列化: 系统底层并不认识对象,数据传输是以字节序列形式传递,以进程间通信为例,需要将对象转化为字节序列(字节序列中包括该对象的类型,成员信息等),然后在目标进程里通过反序列化字节序列,将字节序列转换成对象。
序列化方式:
- Serializable(Java提供 后面简称为S)
- Parcelable(Android特有 下面简称为P)
二、Serializable
S是Java API,是一个通用的序列化机制,可以将对象序列化并保存在本地或内存中。S是一个空接口:
S只起到了一个标识的作用,用于告知程序实现了Serializable
的对象是可以被序列化的,但真正进行序列化和反序列化的操作是通过ObjectOutputStream
及ObjectInputStream
实现的。
2.1 序列化举例
S_Shop.java:
执行序列化和反序列化过程:
执行结果:
结果看到反序列化成功,从序列化结构中又重新生成了对象,这里注意一点,类中的变量TRANSIENT_VALUE
是由transient
修饰的,不能被序列化,所以反序列化时得到的是默认值。另外STATIC_VALUE
由static
修饰,也不参与序列化过程。
2.2 重写readObject、writeObject、readResolve、writeReplace
一般来讲,只有当你自行设计的自定义序列化形式与默认的序列化形式基本相同时,才能接受默认的序列化形式。否则就要设计一个自定义的序列化形式,通过它合理地描述对象的状态。——《Effective Java》
Serializable
实现自定义序列化必须重写readObject、writeObject
方法,readResolve、writeReplace
方法是可选的,看下面的例子:
执行结果:
序列化过程的执行顺序:writeReplace->writeObject
;反序列化过程的执行顺序:readObject->readResolve
通过上面四个方法,可以实现Serializable
的自定义序列化。
注:虽然上述的四个方法都是private
级别的,但在反序列化过程中是通过反射执行的。
2.3 serialVersionUID
序列化会导致类的演变收到限制。这种限制与序列化唯一标识符serialVersionUID
(后面简称sUID)有关,每个可序列化的类都有一个唯一标识号与它相关,sUID
用来辅助序列化和反序列化的,序列化过程中会把类中的sUID
写入序列化文件中。在反序列化时,检测序列化文件中sUID
和当前类中的sUID
是否一致,如果一致,才可以继续进行反序列化操作,否则说明序列化后类发生了一些改变,比如成员变量的类型发生改变等,此时是不能反序列化的。
是否需要指定serialVersionUID? 答案是肯定的,如果不指定sUID
,在序列化时系统也会经过一个复杂运算过程,自动帮我们生成一个并写入序列化文件中。sUID
的值受当前类名称、当前类实现的接口名称、以及所有公有、受保护的成员名称等所影响,此时即使当前类发生了微小的变化(如添加/删除一个不重要的方法)也会导致sUID
改变,进而反序列化失败;如果指定了sUID
,上述操作依然可以进行反序列化,但一些类结构发生改变,如类名改变、成员变量的类型发生了改变,此时即使sUID
验证通过了,反序列化依然会失败。
2.4 实现原理
使用hexdump
命令来查看上述生成的shop.obj
二进制文件:
- AC ED:
STREAM_MAGIC
. 声明使用了序列化协议. - 00 05:
STREAM_VERSION
. 序列化协议版本. - 0x73:
TC_OBJECT
. 声明这是一个新的对象. - 0x72:
TC_CLASSDESC
. 声明这里开始一个新Class
。 - 00 1a:
Class
名字的长度.
序列化步骤:
- 将对象实例相关的类元数据输出
- 递归地输出类的超类描述直到不再有超类
- 类元数据完了之后,开始从最顶层的超类开始输出对象实例的实际数据值
- 从上至下递归输出实例的数据
Serializable
的序列化与反序列化分别通过 ObjectOutputStream
和 ObjectInputStream
进行,都是在Java
层实现的。两个相关概念:
ObjectStreamClass: 序列化类的描述符。它包含类的名称和serialVersionUID
。它由Java VM
加载,可以使用lookup
方法找到或创建。
ObjectStreamField: 类(可序列化)的可序列化字段的描述。ObjectStreamFields
数组用于声明类的可序列化字段。
1、序列化过程(writeObject
方法)
- 通过
ObjectStreamClass
记录目标对象的类型、类名等信息,内部有个ObjectStreamFields
数组,用来记录目标对象的内部变量(内部变量可以是基本类型,也可以是自定义类型,但是必须都支持序列化—必须是S不能是P)。 - 首先通过
ObjectStreamClass.lookup()
找到或创建ObjectStreamClass
,然后调用defaultWriteFields
方法,在方法中通过getPrimFieldValues()
获取基本数据类型并赋值到primVals(byte[]类型)
中,再通过getObjFieldValues()
获取到自定义对象(通过Unsafe
类实现而不是反射)并赋值到objVals(Object[]类型)
中,接着遍历objVals
数组,然后递归调用writeObject
方法重复上述操作。 - 调用过程:
writeObject() -> writeObject0()-> writeOrdinaryObject() -> writeSerialData() -> invokeWriteObject() -> defaultWriteFields()
2、反序列化过程(readObject
方法)
- 通过
readClassDescriptor()
读取InputStream
里的数据并初始化ObjectStreamClass
类,再根据这个实例通过反射创建目标对象实例。 - 调用过程:
readObject() -> readObject0() -> readOrdinaryObject() -> readSerialData() -> defaultReadFields()
Serializable常见异常
异常名称 | 异常起因 |
java.io.InvalidClassException | 1、序列化时自动生成 |
java.io.NotSerializableException | 当前类或类中成员变量未实现序列化 |
2.5 Externalizable
Externalizable
继承自Serializable
,并在其基础上添加了两个方法:writeExternal()
和readExternal()
。这两个方法在序列化和反序列化时会被执行,从而可以实现一些特殊的需求(如指定哪些元素不参与序列化,作用等同于transient
)。如果说默认的Serializable
序列化方式是自动序列化,那么Externalizable
就是手动序列化了,通过writeExternal()
指定参与序列化的内部变量个数,然后通过readExternal()
反序列化重新生成对象。
执行代码跟Serializable
一样,只是将对象变成了S_Shop_External
,执行结果:
readExternal()
中得到的数据都是在writeExternal()
中写入的数据。
Externalizable常见异常
异常名称 | 异常起因 |
java.io.InvalidClassException: no valid constructor | 反序列化时,必须要有修饰符为 |
java.io.OptionalDataException:readExternal | 反序列化时 |
三、Parcelable
P是Android SDK API
,其序列化操作完全由底层实现,可以在进程内、进程间(AIDL
)高效传输数据。不同版本的API
实现方式可能不同,不宜做本地持久化存储。
3.1 序列化举例
P_Shop.java
:
注意:createFromParcel()
和writeToParcel()
方法中对应变量读写的顺序必须是一致的,否则序列化会失败。
Parcel
处理工具:
执行序列化/反序列化:
执行结果:
3.2 实现原理
P序列化过程中会用到Parcel
,Parcel
可以被认为是一个包含数据或者对象引用的容器,能够支持序列化及在跨进程之后的反序列化。P的序列化操作在Native
层实现,通过write
内存写入及read
读内存数据重新生成对象。P将对象进行分解,且分解后每一部分都是支持可传递的数据类型。
序列化过程(Parcelable的写过程)
调用过程Parcel.writeValue()->writeParcelable()
,下面主要来看下此方法:
序列化过程中,首先写入序列化类名,然后调用类中复写的writeToParcel()
方法依次写入
反序列化过程(Parcelable的读过程)
调用过程:Pacel.readValue()->readParcelable()
四、Parcelable、Serializable比较
4.1 效率对比
S序列化和反序列化会经过大量的I/O
操作,产生大量的临时变量引起GC
;P是基于内存实现的封装和解封(marshalled& unmarshalled
),效率比S快很多。
下面的测试来自非官方测试,通过Parcelable
和Serializable
分别执行序列化/反序列化过程,循环1000次取平均值,实验结果如下:
数据来自 parcelable-vs-serializable,实验结果对比Parcelable
的效率比Serializable
快10倍以上。
4.2 容错率对比
序列化到本地时,新版本字段改变对旧版本反序列化的影响
改变字段 | 默认的Serializable序列化方式 | Externalizable | Parcelable |
增加字段 | ✔️ | ❌ | 追加到末尾:✔️ 其他:❌ |
删除字段 | ✔️ | 删除末尾:✔️ 其他:❌ | ❌ |
修改字段类型 | ❌ | ❌ | ❌ |
总结:
- Externalizable中,writeExternal参与序列化,readExternal参与的是反序列化。readExternal()中读入的元素一定是writeExternal()中写入过的,且读写的顺序、字段类型要一致。另外,readExternal中的元素可以少于writeExternal中的,但是注意少的元素一定是在末尾的元素(即不能删除前面的元素),否则反序列化就会失败。
- 对于Parcelable来说,如果新版本中修改字段类型,那么该字段反序列化时会失败;如果是添加字段,那么反序列化时在添加字段位置到末尾位置都会失败;同样删除字段,反序列化时在删除字段的位置到末尾位置都会失败。
五、总结
对比 | Serializable | Parcelable |
所属API | Java API | Android SDK API |
特点 | 序列化和反序列化会经过大量的I/O操作,产生大量的临时变量引起GC,且反序列化时需要反射 | 基于内存拷贝实现的封装和解封(marshalled& unmarshalled),序列化基于Native层实现,不同版本的API实现可能不同 |
开销 | 相对高 | 相对低 |
效率 | 相对低 | 相对高 |
适用场景 | 序列化到本地、网络传输 | 主要内存序列化 |
另外序列化过程中的几个注意点:
- 下面两种成员变量不会参与到默认序列化过程中:
1、static
静态变量属于类而不属于对象
2、transient
标记的成员变量 - 参与序列化的成员变量本身也是需要可序列化的
- 反序列化时,非可序列化的(如被
transient
修饰)变量将会调用自身的无参构造函数重新创建,因此也要求此成员变量的构造函数必须是可访问的,否则会报错。
六、参考
【1】Android 面试(七):Serializable 这么牛逼,Parcelable 要你何用?
【2】每日一问 Parcelable 为什么效率高于 Serializable ?
【3】Android中两种序列化方式的比较Serializable和Parcelable
【5】Android 开发之漫漫长途 X——Android序列化