写在前面:
关于序列化的概念我曾在《Intent传递数据》这篇博文中第一次使用到,但是我并没有对它进行讲解,只是为了传递对象而直接使用了它。因此在这里有必要简单地说下何为序列化。
Serializable接口
在传递对象的时候,我们让对象去实现了一个Serializable接口,但是如果我们点开这个接口呢,就会发现它其实是个空的接口。实际上,仅仅实现Serializable接口虽然可行但不完整,完整的序列化方式应该如下:
public class User implements Serializable {
private static final long serialVersionUID = 195773L;
public int userId;
public String userName;
public boolean isMale;
......
}
可以看到我们还声明了一个静态常量serialVersionUID,而这个serialVersionUID是用来辅助序列化和反序列化过程的。原则上来讲,序列化后的数据中的serialVersionUID只有和当前类的serialVersionUID相同才能够正常地被反序列化。
serialVersionUID的详细工作机制是这样,序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中(也可能是其他中介),当反序列化的时候系统就会去检测文件中的serialVersionUID,看它是否和当前类的serialVersionUID一致,如果一致就说明序列化的类的版本和当前类的版本是相同的,这个时候就可以成功反序列化;否则就说明当前类和序列化的类相比发生了某些变换,比如成员变量的数量、类型可能发生了改变,这个时候反序列化就会报错InvalidClassException。
简单点说serialVersionUID就是类结构的标识,关系到我们反序列化能否成功,那么我们在传递对象的时候没有声明它为啥也没出问题呢?当然,一般来说我们应该手动指定serialVersionUID的值,但是在没有指定的情况下,系统会根据类的结构自动去生成它的hash值,从而保证了序列化和反序列化的可行性。因此,当我们没有手动指定serialVersionUID的时候,反序列化时当前类有所改变,比如增加或删除了某些成员变量,那么系统就会重新计算当前类的hash值并赋给serialVersionUID,这么一来就会和序列化数据中的serialVersionUID不一致,于是反序列化失败。所以手动指定serialVersionUID的值,可以很大程度避免反序列化过程失败。
值得注意的是,手动指定serialVersionUID也不是万能的,当成员变量在发生增删的情况下,虽然可以反序列化成功,但是也只是尽最大限度的去恢复数据。另一方面,如果类结构发生了非常规改变,比如修改类类名,修改了成员变量的类型,这种时候即便serialVersionUID验证通过,反序列化过程仍然会失败。最后在使用序列化时还应注意:
- 静态成员变量属于类不属于对象,所以不会参与序列化过程
- transient标记的成员变量不参与序列化过程
Parcelable接口
相比于Serializable的序列化方式, Parcelable的序列化方式使用起来较为繁琐。但Serializable是Java中的序列化接口,在序列化和反序列化过程中有大量I/O操作,因此效率较低,而Parcelable是Android中的序列化方式,效率较高。
下面是一个实现了序列化的User类,并且它含有一个实现了序列化的Book成员变量:
public class User implements Parcelable {
public int userId;
public String userName;
public boolean isMale;
public Book book;
public User(int userId, String userName, boolean isMale) {
this.userId = userId;
this.userName = userName;
this.isMale = isMale;
}
protected User(Parcel in) {
userId = in.readInt();
userName = in.readString();
isMale = in.readByte() != 0;
book = in.readParcelable(Book.class.getClassLoader());
}
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(userId);
dest.writeString(userName);
dest.writeByte((byte) (isMale ? 1 : 0));
dest.writeParcelable(book, flags);
}
}
public class Book implements Parcelable {
public int bookId;
public String bookName;
public Book(int bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
protected Book(Parcel in) {
bookId = in.readInt();
bookName = in.readString();
}
public static final Creator<Book> CREATOR = new Creator<Book>() {
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(bookId);
dest.writeString(bookName);
}
}
虽然比起Serializable序列化方式要多了很多代码,但这些都可以通过AndroidStudio自动生成,因此也没有繁琐多少。我们先来看一下Parcel,Parcel对象内部包装了可序列化的数据,可以在Binder中自由传输。从上述代码中可以看出,在序列化过程中需要实现以下三个功能:
- 通过writeToParcel方法来实现序列化功能,实际上的操作是借助Parcel中的一系列write方法来完成的
- 通过CREATOR来实现反序列化功能,实际上的操作是借助Parcel中的一系列read方法来完成的,并在其内部标明了如何创建序列化对象和数组
- 通过describeContents方法来完成内容描述功能,通常返回值都是0,仅当当前对象中存在文件描述符时,此方法返回1