android 序列化

什么是序列化

将 java 对象转化为二进制过程,就是序列化,
将二进制转化为 java 对象的过程,就是反序列化

为什么要序列化

在下面几种场景下我们需要序列化

  • 永久性保存对象,保存对象的字节序列到本地文件中;
  • 对象在网络中传递;
  • 对象在 IPC 间传递(进程通信)

如何序列化

序列化有两种方法,java 中自带的实现 Serializable,android 中特有的 Parcelable

Serializable

Serializable 接口是一个标记接口,不用实现任何方法。一旦实现了此接口,该类的对象就是可序列化的。
新建类实现 Serializable,Serializable 为空接口,没有要实现的方法, 需要 定义一个静态常量 serialVersionUID

1
2
3
4
5
6
7
8
9
10
11
12
13
14

public class SerBean implements Serializable {

private static final long serialVersionUID = 263894729013938L;

private String name;

private int age;

public SerBean(String name, int age) {
this.name = name;
this.age = age;
}
}

这样这个类的序列化就完成了。

Serializable 序列化过程其实是将数据写入到文件,而反序列化则是将读取文件中的数据,重新生成对象。

Serializable 原理

  • 序列化:将对象写入到 IO 流中,并保存信息到文件中
  • 反序列化:从文件中读取信息, 然后构建 IO 流中恢复对象。 反序列化过程并没有调用构造函数,而是 JVM 生成

Parcelable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class ParBean implements Parcelable {

private String name;

private int age;

// 系统自动添加,给createFromParcel里面用
protected ParBean(Parcel in) {
name = in.readString();
age = in.readInt();
}

public static final Creator<ParBean> CREATOR = new Creator<ParBean>() {

/**
*
* @param in
* @return
*createFromParcel()方法中我们要去读取刚才写出的name和age字段,
* 并创建一个Person对象进行返回,其中color和size都是调用Parcel的readXxx()方法读取到的,
* 注意这里读取的顺序一定要和刚才写出的顺序完全相同。
* 读取的工作我们利用一个构造函数帮我们完成了
*
*/
@Override
public ParBean createFromParcel(Parcel in) {
return new ParBean(in);
}

//供反序列化本类数组时调用的
@Override
public ParBean[] newArray(int size) {
return new ParBean[size];
}
};

// 内容接口描述,默认返回0即可。
@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name); // 写出name
dest.writeInt(age); // 写出age
}

// --------下面为自己写的构造函数和get set


public ParBean() {
}

public ParBean(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

Parcelable 序列号是在内存空间进行的。

Parcelable 原理

Parcelable 是序列话过程是通过 native 函数进行的,属性是通过指针偏移来保存信息,所以反序列化过程读取属性的顺序必须要与写入属性的过程一样,否则会反序列化失败。
简单理解就是将 java 中对象的各个属性都保存到 native 空间,然后将各个地址指针存放到 java 引用中,
当反序列化时,调用 writeToParcel 写入保存到 native 空间中的属性,然后调用 createFromParcel 生成实例对象,然后就完成了整个反序列化过程。

writeToParcel 中的 write 顺序要与 protected 构造函数中的 read 顺序保持一致,否则会在反序列化时出错。

序列化方案区别

上面讲了两个序列化方案,
Serializable:是 java 就有的,代码量少,但在序列化时,会产生大量的临时对象,容易造成频繁的 minor GC

Parcelable:android 特有的,代码量比 Serializable 要多,但使用效率高,且没那么占内存

因为在选择序列化时,优先使用 Parcelable,但有一种情况特殊,当数据保存在磁盘上时,Parcelable 在外界有变化的情况下,
不能很好的保证数据的连续性,因此在此种场景下推荐使用 Serializable;

选择序列化方法的原则

1)在使用内存的时候,Parcelable 比 Serializable 性能高,所以推荐使用 Parcelable。

2)Serializable 在序列化的时候会产生大量的临时变量,从而引起频繁的 GC。

3)Parcelable 不能使用在要将数据存储在磁盘上的情况,因为 Parcelable 不能很好的保证数据的持续性在外界有变化的情况下。尽管 Serializable 效率低点,但此时还是建议使用 Serializable 。

序列化某种程度来说并不安全

  • 因为序列化的对象数据转换为二进制,并且完全可逆。但是在 RMI 调用时
    所有 private 字段的数据都以明文二进制的形式出现在网络的套接字上,这显然是不安全的

  • 解决方案
    1、 序列化 Hook 化(移位和复位)
    2、 序列数据加密和签名
    3、 利用 transient 的特性解决
    4、 打包和解包代理

补充

  • static 和 transient 字段不能被序列化(感兴趣的同学可以深入研究下)

因为 static 修饰的变量不属于对象,而是属于类

PS:

如果一个可序列化的类的成员不是基本类型,也不是 String 类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。