Android序列化之Parcelable和Serializable的使用详解

2020-10-29 22:38:56 浏览数 (1)

序列化与反序列

首先来了解一下序列化与反序列化。

序列化

由于存在于内存中的对象都是暂时的,无法长期驻存,为了把对象的状态保持下来,这时需要把对象写入到磁盘或者其他介质中,这个过程就叫做序列化。

反序列化

反序列化恰恰是序列化的反向操作,也就是说,把已存在在磁盘或者其他介质中的对象,反序列化(读取)到内存中,以便后续操作,而这个过程就叫做反序列化。

概括性来说序列化是指将对象实例的状态存储到存储媒体(磁盘或者其他介质)的过程。在此过程中,先将对象的公共字段和私有字段以及类的名称(包括类所在的程序集)转换为字节流,然后再把字节流写入数据流。在随后对对象进行反序列化时,将创建出与原对象完全相同的副本。

实现序列化的必要条件

一个对象要实现序列化操作,该类就必须实现了Serializable接口或者Parcelable接口,其中Serializable接口是在java中的序列化抽象类,而Parcelable接口则是android中特有的序列化接口,在某些情况下,Parcelable接口实现的序列化更为高效,关于它们的实现案例我们后续会分析,这里只要清楚知道实现序列化操作时必须实现Serializable接口或者Parcelable接口之一即可。

序列化的应用情景

主要有以下情况(但不限于以下情况)

1)内存中的对象写入到硬盘; 2)用套接字在网络上传送对象; 3)通过RMI(Remote Method Invoke 远程方法调用)传输对象;

1.Parcelable和Serializable有什么用,它们有什么差别?

Parcelable和Serializable是两个接口,它们的作用是让实现了其中一个接口的类的对象能够被序列化和反序列化。

(1)Serializable是java提供的序列化接口,它是一个空的接口,仅标识该类型可序列化的,具体的序列化/反序列化工作由 ObjectInputStream(readObject)/ObjectOutputStream(writeObject) 完成,这个过程包含大量的I/O操作,使用比较简单,但需要考量性能的影响。使用场景:将对象持久化到存储介质或者通过网络传输。

(2)Parcelable接口是Android平台下的序列化接口,通常跨进程传递的数据都要正确实现这个接口,比如Intent,Bitmap等。Parcelable实现起来比Serializable复杂,但性能较好。使用场景:在内存中实现序列化,例如跨进程传递。若某个字段不需要序列化,在实现 writeToParcel 方法中忽略此字段即可。

2.自定义一个类让其实现Parcelable,大致流程是什么?

(1)首先实现Parcelable接口,并实现接口中的方法。

代码语言:javascript复制
/**
 * 返回当前对象的内容描述,如果有文件描述符返回1,否则返回0。
 */
@Override
public int describeContents() {
 return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
 dest.writeInt(userId);
 dest.writeString(userName);
 dest.writeInt(isMan ? 1 : 0);
}

(2)接着创建一个Parcelable接口内部的接口类型Creator的一个成员,内部需要用到一个带一个Parcel参数的构造方法。

代码语言:javascript复制
protected User(Parcel in) {
 userId = in.readInt();
 userName = in.readString();
 isMan = in.readByte() != 0;
}

/**
 * 在aidl中,参数使用in或者inout来修饰时,服务端的onTransact()会调用CREATOR中方法来反序列化客户端传过来的参数
 */
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];
 }
};

(3)如果在aidl文件中使用out或者inout定向tag来修饰参数,还必须实现一个 readFromParcel(Parcel) 方法,这是因为使用这两个定向tag修饰的参数,在服务端onTransact()返回后,客户端会调用 readFromParcel() 来读取(反序列化)_reply中的数据。该方法与writeToParcel是对应的,实现如下:

代码语言:javascript复制
public void readFromParcel(Parcel in) {
 userId = in.readInt();
 userName = in.readString();
 isMan = in.readInt() == 1 ? true : false;
}

3.实现Serializable接口

通过实现Serializable接口来实现序列化比较简单,只需要实现该接口,并指定 serialVersionUID 即可,当然这个ID是可选的,可以不手动指定。通过ObjectIntputStream /ObjectOutputStream来序列化和反序列化。

代码语言:javascript复制
ObjectOutputStream oos = null;
try {
 oos = new ObjectOutputStream(new FileOutputStream(new File("sdcard/user.cache")));
 User user = new User(111, "Jdqm", true);
 oos.writeObject(user);
} catch (IOException e) {
 e.printStackTrace();
} finally {
 try {
  oos.close();
 } catch (IOException e) {
  e.printStackTrace();
 }
}
代码语言:javascript复制
ObjectInputStream ois = null;
try {
 ois = new ObjectInputStream(new FileInputStream(new File("sdcard/user.cache")));
 User user = (User) ois.readObject();
 Log.d(TAG, "user: "   user);
} catch (IOException | ClassNotFoundException e) {
 e.printStackTrace();
} finally {
 try {
  ois.close();
 } catch (IOException e) {
  e.printStackTrace();
 }
}

输出结果

User{userId=111, userName=’Jdqm’, isMan=true}

serialVersionUID的意义:辅助完成序列化和反序列化,当一个类实现SerSerializable接口,没有添加serialVersionUID的作用字段时,IDE会发出警告,这个字段可以手动指定一个值,比如1L,也可指定为IED根据类的结构生成一个long值,它们的效果是一样的。在序列化时会将这个值写入存储介质,反序列化时就校验本地类的serialVersionUID和序列化介质中的是否一致,不一致将抛出异常 java.io.InvalidClassException

(1)若不指定:系统会根据类的结构计算出一个serialVersionUID,一旦类的结构发生改变这个值就会改变,将导致反序列化失败;

(2)指定一个值:当类的结构发生改变时,也可以不修改serialVersionUID的值,这种情况下能最大程度上通过反序列化回复数据,若类的结构发生毁灭性的改变,例如字段数据类型改变了,也会导致反序列失败。

Note: 类的结构发生改变指的是类的成员变量的改变,添加一个普通的方法是不会导致计算得到的serialVersionUID改变的。构造方法、toString()、getter/setter改变会引起serialVersionUID改变。

  1. transient修饰的成员变量不参与序列化,反序列化时改成员为该数据类型的默认值
  2. 静态成员不参与序列化
  3. 反序列化得到的一个新对象的过程并没有调用构造方法

以上就是本文的全部内容,希望对大家的学习有所帮助。

0 人点赞