Fork me on GitHub

Android两种序列化方式详解(一):Serializable

前言

在 Android 开发中,我们经常需要对对象进行序列化与反序列化操作,最常见的就是通过 Intent 传输数据时,Intent 只能传输基本数据类型、String 类型和可序列化与反序列化的对象类型,要想通过 Intent 传递对象类型,我们需要让该对象类型支持序列化和反序列化。

我们知道,Android 给我们提供了两种方式来完成序列化与反序列化过程:一种是 Serializable 方式,另一种是 Parcelable 方式;本篇文章将详细讲述使用 Serializable 方式实现序列化你所需要知道的一切。

你可能会疑问,使用 Serializable 实现序列化,不是只要让类实现 Serializable 接口就可以了吗,有什么好讲的?那你就 too naive 了少年!除了基础的直接实现 Serializable 接口之外,我们使用 Serializable 方式实现序列化的过程还有很多需要注意的细节,例如 serialVersionUID 是干什么的呢?如果我们想自定义实现序列化与反序列化过程该怎么办呢?本文将会详细介绍这些知识。

目录

本文讲述的知识点如下:

  1. 怎样序列化与反序列化一个对象
  2. serialVersionUID 的作用
  3. 如何自定义序列化和反序列化过程
  4. 总结

一、怎样序列化和反序列化一个对象

想要序列化和反序列化一个对象,首先要让对象支持序列化与反序列化,使用 Serializable 方式实现序列化相当简单,只需要让类实现 Serializable 接口就可以:

1
public class UserSerial implements Serializable {

现在 UserSerial 类就支持序列化和反序列化了,那么我们应该怎么将 UserSerial 类的某个对象序列化到文件,然后再将其读取出来呢?当然是使用 ObjectOutputStreamObjectInputStream 啦~

完整的序列化流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void serial(UserSerial user) {
ObjectOutputStream out = null;
try {
out = new ObjectOutputStream(new FileOutputStream("temp.txt"));
out.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

完整的反序列化流程如下,这里为了防止反序列化异常返回 null,默认我们返回了一个新构造的空 UserSerial 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public UserSerial deserial() {
ObjectInputStream in = null;
try {
in = new ObjectInputStream(new FileInputStream("temp.txt"));
return (UserSerial) in.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return new UserSerial();
}

可能有人要吐槽我上面说的都是废话了,上面这些东西,稍微了解点 Java 的都知道啊~没事,且往下看,我们来说一些可能大家使用过程中没注意到的细节。

二、serialVersionUID 的作用

我们在使用 Serializable 方式实现序列化时,除了实现 Serializable 接口之外,一般还需要声明一个 serialVersionUID 静态字段,当然我们也可以选择不声明这个字段,那么我们在使用过程中,要不要指定这个字段呢?如果指定了,这个字段的值又是干什么用的呢?

其实 serialVersionUID 这个字段,是序列化和反序列化过程中,用来校验类是否发生了变动的依据,序列化的时候系统会把当前类的 serialVersionUID 字段写入序列化的文件中,当反序列化的时候,系统会去检测文件中的 serialVersionUID,看它是否和当前类的 serialVersionUID 一致,如果一致就说明序列化时类的版本和当前类的版本是相同的,这个时候可以成功反序列化,否则就说明当前类和序列化时的类相比发生了某些变换,比如增删了某些成员变量等,这个时候是无法正常的反序列化的,并且会报 InvalidClassException

一般来讲,我们都应该手动指定 serialVersionUID 的值,可以随意指定一个数字,或者根据编辑器提示自动根据当前类的结构生成 hash 值,这样如果我们不手动修改,序列化和反序列化过程中 serialVersionUID 字段的值一直都会是一致的,可以最大限度的保证反序列化过程的成功,就算类结构发生了变动,我们也可以保证那些没有发生变动的成员变量被成功的反序列化。如果我们不手动指定 serialVersionUID 字段的值,那么如果反序列化时相比序列化时的类结构发生了变动,比如增删了成员变量等,那么系统就会重新计算当前类的 hash 值,并将其赋值给 serialVersionUID,导致序列化时的类和当前类的 serialVersionUID 值不一致,导致反序列化失败,从而 crash。

最后要说明的是,如果类结构发生了毁灭性的变化,如类名发生了改变,这个时候就算 serialVersionUID 值一致,也是不能被正常反序列化的,这一点后面还会提到。

三、如何自定义序列化和反序列化过程

现在我们知道如果一个类,实现了 Serializable 接口,那么在需要的时候,自动完成该类的序列化,与反序列化过程;那么如果我们想自定义序列化过程与反序列化过程该怎么办呢?例如我在版本 1 的时候,将 User 对象的 name 序列化了,但是反序列化的时候,我想把这个这个字段的值赋值给 nameNew,该怎么做到呢?

第一部分我们我们提到,使用 Serializable 方式实现序列化是,对象的序列化和反序列化过程是通过 ObjectOutputStream.writeObjectObjectInputStream.readObject 实现的,这里以 ObjectOutputStream.writeObject 为例分析,追踪 writeObject 方法代码,发现其内部调用了 writeObject0 方法:

1
2
3
4
5
public final void writeObject(Object obj) throws IOException {
// ...
writeObject0(obj, false);
//...
}

进入 writeObject0 方法:

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
private void writeObject0(Object obj, boolean unshared) throws IOException {
// ...
if (obj instanceof Class) {
writeClass((Class) obj, unshared);
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
// END Android-changed
} else if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
// ...
}

因为我们的对象是 Serializable,所以最终会走到 writeOrdinaryObject 方法:

1
2
3
4
5
6
7
8
9
private void writeOrdinaryObject(Object obj,ObjectStreamClass desc,boolean unshared) throws IOException {
// ...
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);
} else {
writeSerialData(obj, desc);
}
// ...
}

我们看到 writeOrdinaryObject 方法内部做了判断,看当前类是实现的 Externalizable 接口还是 Serializable 接口,因为我们是实现的 Serializable 接口,所以最终走到了 writeSerialData 方法:

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
private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException {
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
if (slotDesc.hasWriteObjectMethod()) {
// ...
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bout.setBlockDataMode(true);
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
} finally {
// ...
}

curPut = oldPut;
} else {
defaultWriteFields(obj, slotDesc);
}
}
}
```

走到这里就很明确了,我们发现,在 `ObjectOutputStream.writeObject` 过程中,最终会判断,当前类本身是否 `hasWriteObjectMethod()`。

如果 `hasWriteObjectMethod()` 为 true,就通过反射,调用类自带的方法 `slotDesc.invokeWriteObject(obj, this);`,点进 `invokeWriteObject` 方法,我们发现内部是通过反射调用的 `writeObjectMethod`,这是一个 `Method` 类型的字段,而给该字段初始化的代码如下:

```java
writeObjectMethod = getPrivateMethod(cl, "writeObject", new Class<?>[]{ ObjectOutputStream.class }, Void.TYPE);

最后我们发现,是通过反射调用的实现 Serializable 接口的类的 writeObject 方法,而参数类型是 ObjectOutputStream 类型。

而如果 hasWriteObjectMethod() 为 false,就使用 defaultWriteFields 完成序列化,也就是系统默认的序列化方法,想了解系统默认序列化方法的可以点进源码自己查看。

通过同样的步骤,我们可以发现 ObjectInputStream.readObject 方法内部,也判断了类是否有 readObject 方法,有就使用类自己的,没有就使用默认的。

所以如果我们在使用 Serializable 实现序列化与反序列化是,想实现自定义的序列化和反序列化过程,只需要给当前类添加 writeObjectreadObject 方法即可,参考代码如下:

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
/**
* 自定义序列化过程
*/
private void writeObject(ObjectOutputStream out) {
ObjectOutputStream.PutField putFields = null;
try {
putFields = out.putFields();
putFields.put("name", name);
// ...
} catch (IOException e) {
e.printStackTrace();
}

}

/**
* 自定义反序列化过程
*/
private void readObject(ObjectInputStream in) {
try {
ObjectInputStream.GetField readFields = in.readFields();
name = (String) readFields.get("name", "");
// ...
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

认真的同学可能还会有一个疑问,那就是为啥你这自己写的 readObject 方法没有返回值呢?这个方法不应该返回一个当前类对象吗,像 ObjectInputStream.readObject 方法一样?这个问题的答案也可以通过查看源码得到解答,其实反序列化过程可以分为两步,一步是通过反射创建类对象的过程,一个是给创建的对象内的变量赋值的过程,而我们重写的方法只是完成了给当前创建的对象复制的过程,是赋给自身变量的,所以没有返回值,对象创建的过程在 ObjectInputStream.readObject 方法内,可通过源码验证,这也印证了我们上面所说,如果类名发生了变化,那么反序列化是不可能成功的,因为找不到类了。

四、总结

经过以上分析,我们可以得出一下结论:

  1. 我们总应该给使用 Serializable 方式实现序列化与反序列化的类指定 serialVersionUID
  2. 我们可以通过给类添加 writeObject(ObjectOutputStream out)readObject(ObjectInputStream in) 方法的方式自定义序列化和反序列化过程
  3. 静态变量和 transient 关键字标注的字段是不参与序列化与反序列化过程的,但是我们仍然可以通过自定义序列化和反序列化的过程打破这个限制,当然也可以通过 Java 提供的另一个序列化接口 Externalizable 来打破这个限制,内部原理差不多
小马哥Mark wechat
欢迎关注我的微信公众号,订阅我的博客!
我一直在期待,那个最帅的人出现,哈哈!

热评文章