定义以及相关概念
- 由于在系统底层,数据的传输形式是简单的字节序列形式传递,即在底层,系统不认识对象,只认识字节序列,而为了达到进程通讯的目的,需要先将数据序列化,而序列化就是将对象转化字节序列的过程。相反地,当字节序列被运到相应的进程的时候,进程为了识别这些数据,就要将其反序列化,即把字节序列转化为对象。
- 无论是在进程间通信、本地数据存储又或者是网络数据传输都离不开序列化的支持。而针对不同场景选择合适的序列化方案对于应用的性能有着极大的影响。
- 从广义上讲,数据序列化就是将数据结构或者是对象转换成我们可以存储或者传输的数据格式的一个过程,在序列化的过程中,数据结构或者对象将其状态信息写入到临时或者持久性的存储区中,而在对应的反序列化过程中,则可以说是生成的数据被还原成数据结构或对象的过程。
- 这样来说,数据序列化相当于是将我们原先的对象序列化概念做出了扩展,在对象序列化和反序列化中,我们熟知的有两种方法,其一是 Java 语言中提供的
Serializable
接口,其二是 Android 提供的Parcelable
接口。而在这里,因为我们对这个概念做出了扩展,因此也需要考虑几种专门针对数据结构进行序列化的方法,如现在那些个开放 AP I一般返回的数据都是 JSON 格式的,又或者是我们 Android 原生的 SQLite 数据库来实现数据的本地存储,从广义上来说,这些都可以算做是数据的序列化。
序列化
将数据结构或对象转换成二进制串的过程。
反序列化
将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。
序列化/反序列化的目的
概括:
- 序列化:主要用于网络传输,数据持久化,一般序列化也称为编码(Encode)
- 反序列化:主要用于从网络,磁盘上读取字节数组还原成原始对象,一般反序列化也称为解码(Decode)
目的:
- 永久的保存对象数据(将对象数据保存在文件当中,或者是磁盘中)
- 通过序列化操作将对象数据在网络上进行传输(由于网络传输是以字节流的方式对数据进行传输的。因此序列化的目的是将对象数据转换成字节流的形式)
- 将对象数据在进程之间进行传递( Activity 之间传递对象数据时,需要在当前的 Activity 中对对象数据进行序列化操作。在另一个 Activity 中需要进行反序列化操作将数据取出)
- Java平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行时,这些对象才可能存在,即这些对象的生命周期不会比 JVM 的生命周期更长(即每个对象都在 JVM 中)但在现实应用中,就可能要停止 JVM 运行,但有要保存某些指定的对象,并在将来重新读取被保存的对象。这时 Java 对象序列化就能够实现该功能。(可选择入数据库、或文件的形式保存)
- 序列化对象的时候只是针对变量进行序列化,不针对方法进行序列化。
- 在 Intent 之间,基本的数据类型直接进行相关传递即可,但是一旦数据类型比较复杂的时候,就需要进行序列化操作了。
序列化方案
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 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();
}
...
}
////Course也需要实现Serializable接口
public class Course implements Serializable {
private static final long serialVersionUID = 667279791530738499L;
private String name; private float score;
...
}
Serializable 有以下几个特点:
- 可序列化类中,未实现 Serializable 的属性状态无法被序列化/反序列化
- 也就是说,反序列化一个类的过程中,它的非可序列化的属性将会调用无参构造函数重新创建
- 因此这个属性的无参构造函数必须可以访问,否者运行时会报错
- 一个实现序列化的类,它的子类也是可序列化的
serialVersionUID 与兼容性
- serialVersionUID 的作用
serialVersionUID 用来表明类的不同版本间的兼容性。如果你修改了此类, 要修改此值。否则以前用老版本的类序列化的类恢复时会报错: InvalidClassException - 设置方式
在JDK中,可以利用 JDK 的 bin 目录下的 serialver.exe 工具产生这个serialVersionUID,对于 Test.class,执行命令:serialver Test - 兼容性问题
为了在反序列化时,确保类版本的兼容性,最好在每个要序列化的类中加入 private static final long serialVersionUID 这个属性,具体数值自己定义。这样,即使某个类在与之对应的对象已经序列化出去后做了修改,该对象依然可以被正确反序列化。否则,如果不显式定义该属性,这个属性值将由 JVM 根据类的相关信息计算,而修改后的类的计算结果与修改前的类的计算结果往往不同,从而造成对象的反序列化因为类版本不兼容而失败。不显式定义这个属性值的另一个坏处是,不利于程序在不同的 JVM 之间的移植。因为不同的编译器实现该属性值的计算策略可能不同,从而造成虽然类没有改变,但是因为 JVM 不同,出现因类版本不兼容而无法正确反序列化的现象出现
因此 JVM 规范强烈 建议我们手动声明一个版本号,这个数字可以是随机的,只要固定不变就可以。同时最好是 private 和 final 的,尽量保证不变。
序列化与反序列化 Serializable
Serializable 的序列化与反序列化分别通过 ObjectOutputStream 和 ObjectInputStream 进行。
Parcelable 接口
Parcelable 是 Android 为我们提供的序列化的接口,Parcelable 相对于 Serializable 的使用相对复杂一些,但 Parcelable 的效率相对 Serializable 也高很多。
Parcelable 是Android SDK提供的,它是基于内存的,由于内存读写速度高于硬盘,因此 Android 中的跨进程对象的传递一般使用 Parcelable。
Parcelable入门
public class Course implements Parcelable {
private String name;
private float score;
...
/**
* 描述当前 Parcelable 实例的对象类型
* 比如说,如果对象中有文件描述符,这个方法就会返回上面的 CONTENTS_FILE_DESCRIPTOR
* 其他情况会返回一个位掩码
* @return
*/
@Override
public int describeContents() {
return 0;
}
/**
* 将对象转换成一个 Parcel 对象
* @param dest 表示要写入的 Parcel 对象
* @param flags 示这个对象将如何写入
*/
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.name);
dest.writeFloat(this.score);
}
protected Course(Parcel in) {
this.name = in.readString();
this.score = in.readFloat();
}
/**
* 实现类必须有一个 Creator 属性,用于反序列化,将 Parcel 对象转换为 Parcelable
* @param
*/
public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
//反序列化的方法,将Parcel还原成Java对象
@Override
public Course createFromParcel(Parcel source) {
return new Course(source);
}
//提供给外部类反序列化这个数组使用。
@Override
public Course[] newArray(int size) {
return new Course[size];
}
};
}
Parcel 的简介
Parcel 就是包装了我们需要传输的数据,然后在 Binder 中传输,也就是用于跨进程传输数据。
Parcel 提供了一套机制,可以将序列化之后的数据写入到一个共享内存中,其他进程通过 Parcel 可以从这块共享内存中读出字节流,并反序列化成对象,下图是这个过程的模型。
Parcel 可以包含原始数据类型(用各种对应的方法写入,比如writeInt(),writeFloat()等),可以包含 Parcelable 对象,它还包含了一个活动的 IBinder 对象的引用,这个引用导致另一端接收到一个指向这个 IBinder 的代理 IBinder。
Parcelable 通过 Parcel 实现了 read 和 write 的方法,从而实现序列化和反序列化。
Parcelable 与 Serializable 的性能比较
Serializable 性能分析
Serializable 是 Java 中的序列化接口,其使用起来简单但开销较大(因为 Serializable 在序列化过程中使用了反射机制,故而会产生大量的临时变量,从而导致频繁的GC),并且在读写数据过程中,它是通过 IO 流的形式将数据写入到硬盘或者传输到网络上。
Parcelable 性能分析
Parcelable 则是以 IBinder 作为信息载体,在内存上开销比较小,因此在内存之间进行数据传递时,推荐使用 Parcelable,而 Parcelable 对数据进行持久化或者网络传输时操作复杂,一般这个时候推荐使用 Serializable。
性能比较总结描述
首先 Parcelable 的性能要强于Serializable的原因简单的阐述一下
- 在内存的使用中,前者在性能方面要强于后者
- 后者在序列化操作的时候会产生大量的临时变量,(原因是使用了反射机制)从而导致 GC 的频繁调用,因此在性能上会稍微逊色
- Parcelable 是以 Ibinder 作为信息载体的。在内存上的开销比较小,因此在内存之间进行数据传递的时候,Android 推荐使用 Parcelable ,既然是内存方面比价有优势,那么自然就要优先选择。
- 在读写数据的时候,Parcelable 是在内存中直接进行读写,而 Serializable 是通过使用 IO 流的形式将数据读写入在硬盘上。
虽然 Parcelable 的性能要强于 Serializable,但是仍然有特殊的情况需要使用 Serializable,而不去使用 Parcelable,因为 Parcelable 无法将数据进行持久化,因此在将数据保存在磁盘的时候,仍然需要使用后者,因为前者无法很好的将数据进行持久化。(原因是在不同的 Android 版本当中,Parcelable 可能会不同,因此数据的持久化方面仍然是使用 Serializable)
两种如何选择
- 在使用内存方面,Parcelable 比 Serializable性能高,所以推荐使用Parcelable。
- Serializable 在序列化的时候会产生大量的临时变量,从而引起频繁的 GC。
- Parcelable 不能使用在要将数据存储在磁盘上的情况,因为 Parcelable 不能很好的保证数据的持续性,在外界有变化的情况下,建议使用 Serializable。
面试相关的问题
Android 里面为什么要设计出 Bundle 而不是直接用 Map 结构
Bundle 内部是由 ArrayMap 实现的,ArrayMap 的内部实现是两个数组,一个 int 数组是存储对象数据对应下标,一个对象数组保存 key 和 value,内部使用二分法对 key 进行排序,所以在添加、删除、查找数据的时候,都会使用二分法查找,只适合于小数据量操作,如果在数据量比较大的情况下,那么它的性能将退化。而 HashMap 内部则是数组+链表结构,所以在数据量较少的时候,HashMap 的 Entry Array 比 ArrayMap 占用更多的内存。因为使用 Bundle 的场景大多数为小数据量,我没见过在两个 Activity 之间传递 10 个以上数据的场景,所以相比之下,在这种情况下使用 ArrayMap 保存数据,在操作速度和内存占用上都具有优势,因此使用 Bundle 来传递数据,可以保证更快的速度和更少的内存占用。
另外一个原因,则是在 Android 中如果使用 Intent 来携带数据的话,需要数据是基本类型或者是可序列化类型,HashMap 使用 Serializable 进行序列化,而 Bundle 则是使用 Parcelable 进行序列化。而在 Android 平台中,更推荐使用 Parcelable 实现序列化,虽然写法复杂,但是开销更小,所以为了更加快速的进行数据的序列化和反序列化,系统封装了 Bundle 类,方便我们进行数据的传输。Android 中 Intent/Bundle 的通信原理及大小限制
Intent 中的 Bundle 是使用 Binder 机制进行数据传送的。能使用的 Binder 的缓冲区是有大小限制的(有些手机是 2 M),而一个进程默认有 16 个 Binder 线程,所以一个线程能占用的缓冲区就更小了( 有人以前做过测试,大约一个线程可以占用 128 KB)。所以当看到 The Binder transaction failed because it was too large 这类 TransactionTooLargeException 异常时,就知道怎么解决了。为何 Intent 不能直接在组件间传递对象而要通过序列化机制?
Intent 在启动其他组件时,会离开当前应用程序进程,进入 ActivityManagerService进程(intent.prepareToLeaveProcess()),这也就意味着,Intent 所携带的数据要能够在不同进程间传输。首先我们知道,Android 是基于 Linux 系统,不同进程之间的 java 对象是无法传输,所以我们此处要对对象进行序列化,从而实现对象在应用程序进程 和 ActivityManagerService 进程之间传输。
而 Parcel 或者 Serializable 都可以将对象序列化,其中,Serializable 使用方便,但性能不如 Parcel容器,后者也是 Android 系统专门推出的用于进程间通信等的接口。