笔记内容基于:《Android 开发艺术探索》
额外参考博客:
1、详述 IntelliJ IDEA 中自动生成 serialVersionUID 的方法
2、详细介绍Android中Parcelable的原理和使用方法
3、Android中Parcel的解读
序列化的作用:
- 在网络上传送对象的字节序列;
- 把对象的字节序列永久地保存到本地。
静态变量属于类不属于对象,不会参与序列化过程,用 transient 关键字标记的成员变量也不会。
1、基于 Java 的 Serializable 接口
Serializable 为空接口,只需要继承它即可。
与 Serializable 相关的还有 serialVersionUID,该字段不是非必要实现的,没有它也可以正常的实现序列化,但是会对反序列化过程产生影响。
// 实现对象可序列化
public class User implements Serializable {
private static final long serialVersionUID = .....
public int uid;
public String userName;
public boolean isMale;
}
// 序列化过程
// 把对象持久化到文件中
User user1 = new User(0, "Bill", true)
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"))
out.writeObject(user1);
out.close();
// 反序列化过程
// 从文件中反序列化得到之前存储的对象数据
ObjectInputStream out = new ObjectInputStream(new FileInputStream("cache.txt"))
User user2 = (User)in.readObject();
in.close();
在序列化与反序列化过程中设计到的对应对象实例 user1 和 user2 不是同一对象,只不过内容一致。
serialVersionUID 则是在反序列化的时候起到验证的作用,起到辅助序列化与反序列化过程。
原则上序列化后的数据中的 serialVersionUID 只有和当前类的 serialVersionUID 相同才能正常的反序列化。
工作机制:
Java 的序列化机制是通过在运行时判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就表示当前类与被反序列化成的类相比发生了变化(如成员变量的类型可能发生了变化),会出现序列化版本不一致的异常:
java.io.InvalidClassException:
local class incompatible: stream classdesc serialVersionUID = 3,
local class serialVersionUID = 4
但是有测试,如果只是成员数量的变化, 如序列化后为类增加了新的成员变量,再反序列化,或者是反序列化之前删除了某一成员变量,这个是不会引起异常的。
需要注意的是,如果序列化之后类发生了改变(如改变成员变量的类型),但是没有改变设置 serialVersionUID,在反序列化的时候也会失败,报异常:
java.io.InvalidClassException: incompatible types for field XXX
因此 serialVersionUID 是保证反序列化成功的必要非充分条件。
即能反序列化成功,则表示序列化与反序列化时的 serialVersionUID 一致;而 serialVersionUID 一致,则不一定能够反序列化成功。
一般来说,定义 serialVersionUID 的方式有两种,分别为:
- 采用默认的1L,具体为private static final long serialVersionUID = 1L;
- 根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,例如 private static final long serialVersionUID = XXXL;
serialVersionUID 可以手动设置,也可以利用编译器插件实现,具体可见 这里。
2、基于 Android 的 Parcelable 接口
实现 Parcelable 接口,一个类的对象就可以实现序列化并且通过 Intent 和 Binder 传递。(Serializable 接口的能在 Intent 中传递,也能在 Binder 中传递)
public class Book implements Parcelable {
public int bookId;
public String bookName;
public int price;
public Book() {
}
public Book(int bookId, String bookName,int price) {
this.bookId = bookId;
this.bookName = bookName;
this.price = price;
}
public static final Creator<Book> CREATOR = new Creator<Book>() {
// 从序列化后的对象中创建原始对象
// Parcel 内部包装了可序列化数据
@Override
public Book createFromParcel(Parcel in) {
int bookId = in.readInt();
String s1 = in.readString();
int price = in.readInt();
return new Book(bookId,s1,price);
}
//指定长度的原始对象数组
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
//返回当前对象的内容描述。如果含有文件描述符,返回 1,否则返回 0,几乎所有情况都返回 0
@Override
public int describeContents() {
return 0;
}
//将当前对象写入序列化结构中,其 flags 标识有两种(1|0)。
//为 1 时标识当前对象需要作为返回值返回,不能立即释放资源,几乎所有情况下都为 0.
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(bookId);
dest.writeString(bookName);
dest.writeInt(price);
}
}
从 Parcel 中读取数据的顺序最好按照在 writeToParcel() 中写入的顺序,避免数据错乱或者出现异常。
参见:通过Parcelable协议传递数据出现错误(Unmarshalling unknown type code 7471205 at offset 232)
且 readXXX 一次 Parcel 内包装的数据就会少一点,即不能重复读取。
两种方式的区别:
使用 Serializable 的方式开销大,因为序列化与反序列化过程都需要大量 IO 操作,但是实现起来比较简单。
而 Parceable 是针对 Android 平台的,效率高,但是实现起来相对复杂。 主要用在内存序列化上, 虽然也可以用在序列化到存储设备中或者将对象序列化并在网络中传输。
另外两种方式实现序列化,在 Intent 中传递前后的对象都是不一样的,即传递后接收到的对象是新建的,只是数据内容一致。只不过取的时候一个用 intent.getSerializableExtra(),另一个用 intent.getParcelableExtra()。
下面两种情况就发日常适合 Serializable:
- 需要将对象序列化到设备;
- 对象序列化后需要网络传输。
补充:
在面试的时候有问到两种实现存在效率差异的原因。
对应的解释可以参阅:Android 面试(七):Serializable 这么牛逼,Parcelable 要你何用?
简单的说,原因如下:
Serializable 会使用反射,序列化和反序列化过程需要大量 I/O 操作;
Parcelable 自已实现封送和解封(marshalled &unmarshalled)操作不需要用反射,数据也存放在 Native 内存中,效率要快很多。
后来又再思考跨进程的时候,如果不使用反射来得到对应的对象,那还怎么生成对象,推测应该是跟实现的 CREATOR 的 createFromParcel() 方法有关,这个里面需要手动实现从 Parcel 读取数据并自己 new 出对应的对象,将读取的数据赋值进去。

本文深入探讨了序列化机制,包括Java的Serializable接口和Android的Parcelable接口的原理与使用方法。详细解析了serialVersionUID的作用及其在反序列化过程中的重要性,并对比了Serializable与Parcelable在实现复杂度和效率上的区别。

620

被折叠的 条评论
为什么被折叠?



