Serializable & Parcelable

1,836 阅读8分钟

对象序列化的简单介绍

所谓对象的序列化其实就是把JVM运行过程中生成的对象通过特殊的处理手段转换为字节形式的文件。转换之后就可以将其永久保存到磁盘中,或者以字节流进行网络传输。

在Android中使用Intent传递数据时,基本数据类型可以直接传递,而比较复杂的引用类型的数据就需要先将对象序列化再进行传递。

序列化的转换只是将对象的属性进行序列化,不针对方法进行序列化。

Android中有两种实现序列化的方法,一种是实现Java提供的Serializable接口,另一种是Android提供的Parcelable接口。

使用Serializable 序列化对象

Serializable是Java提供的接口(interface),它里面没有任何的属性跟方法,纯粹就是起到个标识的作用。如果想让某个类下的对象能够序列化需要先实现Serializable接口。

例如,我们想让一个Person类的对象能够序列化,这个类就需要被声明为:

public class Person implements Serializable{

    private String name;
    private int age;
    
    ...
}

之后我们就可以将Person类的对象序列化写入文件中永久保存了,这个环节你需要ObjectOutStream的帮助:

// 构造一个指定具体文件的ObjectOutStream ,path为文件的路径
ObjectOutputStream out = new ObjectOutputStream(Files.newOutPutStream(path));

//实例化对象
Person peter = new Person("peter" , 18);
Person mike = new Person("mike" , 20);

// 写入对象
out.writeObject(peter);
out.writeObject(mike);

上面代码就完成了写入对象的操作,要想读回对象的话需要用到ObjectInputStream

ObjectInputStream in = new ObjectInputStream(Files.newInPutStream(path));

// 读取 peter
Person p1 = (Person) in.readObject(); 

// 读取mike
Person p2 = (Person) in.readObject(); 

注意!读取对象的顺序与写入对象的顺序是一致的。

如果序列化对象的属性是基本数据类型的则会以二进制形式保存数据,如果属性也是一个对象那么它会被writeObject()再次写入,直到所有属性都是基本数据类型为止。

还有一点,如果写入的两个对象里引用了同一个对象,当读取回这两个对象时它们引用的对象还是同一个,而不会是两个内容相同却是不同引用的对象。这归功于在读写对象时会为每个对象记录一个唯一序列号。

使用transient关键字忽略某些属性

在实际中某些属性是不需要被序列化的,例如数据库连接对象就没必要序列化,为了实现某些属性不被序列化,我们可以给这些属性加上一个transient修饰标记符,那么这些属性在序列化时就会被自动忽略。

public class Person implements Serializable{

    private String name;
    private int age;
    
    // 不需要序列化的属性
    private transient Connection mConn;
    ...
}

关于序列化版本

有时候我们会将序列化的对象从一台JVM传到另一台JVM上运行,为保证读取的对象与写入的对象一致,JVM在写入对象的时候为类分配了一个serialVersionUID属性.

serialVersionUID属性用来标识当前序列化对象的类版本,如果我们没有手动指定它,JVM会根据类的信息自动生成一个UID。但如果是两台JVM互传数据时为保证类的一致性,我们最好自己手动声明这个属性:

public class Person implements Serializable{

    // 序列化的版本,自己定义具体数据来实现每次的版本更新
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    
    // 不需要序列化的属性
    private transient Connection mConn;
    ...
}

自定义序列化细节

到现在为止我们序列化对象的方法只是直接调用了Java的API,序列化的过程全部由Java帮我们默认实现。但是有些情况我们需要在序列化时进行一些特殊处理,例如某些表示状态的属性序列化时不需要保存而反序列化成对象时希望能够被赋值,显然transient关键字不能帮我们实现,这时候我们就需要自定义序列化的细节。

ObjectOutputStreamObjectInputStream在序列化与反序列化时会检查我们的类是否声明了如下几个方法:

  • void writeObject(ObjectOutputStream oos) throws IOException 序列化对象时调用的方法
  • void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException 反序列化对象时调用的方法
  • Object writeReplace() throws ObjectStreamException ObjectOutPutStream 序列化对象之前调用的方法,在这里可以替换真正被序列化的对象
  • Object readResolve() throws ObjectStreamException 在反序列化对象后调用的方法,在这里可以替换反序列化后得到的对象

以上方法如果你自己声明了那么就执行你自定义的方法,否则使用系统默认的方法。至于自定义方法的权限修饰符private protected public都无所谓,因为使用ObjectXXXputStream使用反射调用的。他们在序列化与反序列化的调用流程如下图。

序列化与反序列化流程

此四个方法你可以根据需要任意替换成自己的方法,不过一般都是都是读写成对替换的,下面看我们如何用自定义方法实现序列化:

public class Person implements Serializable{

    // 序列化的版本,自己定义具体数据来实现每次的版本更新
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    
    // 不需要序列化的属性
    private transient Connection mConn;

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

    private void writeObject(ObjectOutputStream oos)  throws IOException {
        // 默认的序列化对象方法
        out.defaultWriteObject();
        //我们自定义添加的东西
        out.writeInt(100);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
          // 默认反序列化方法
         in.defaultReadObject();
         // 读出我们自定义添加的东西
         int flag = in.readInt();
         System.out.println(flag);
    }

    private Object writeReplace(){
         // 替换真正序列化的对象
         return new Person(name,age);
    }

    private Object readResolve(){
        // 替换反序列化后的对象
        return new Person(name,age);
    }
}

此处你可能对writeReplace()readResolve()方法的用处有疑问,在下面序列化代理中会见识到它们的用处。

关于反序列化需要注意的

从字节流中读取的数据后反序列化的对象并不是通过构造器创建的,那么很多依赖于构造器保证的约束条件在对象反序列化时都无法保证。比如一个设计成单例的类如果能够被序列化就可以分分钟克隆出多个实例...

序列化代理

在知道了Java在反序列化时并不是通过构造器创建的对象,那么别人只需要解析你序列化后的字节码就能够轻而易举的获取你的内容,不仅如此,再利用同样的序列化格式生成任意的字节码送你你的程序分分钟就攻破你的程序。

为解决该隐患,大神们推荐我们使用静态内部类作为代理来进行类的序列化:

public class Person implements Serializable{

    // 序列化的版本,自己定义具体数据来实现每次的版本更新
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    
    // 不需要序列化的属性
    private transient Connection mConn;

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

    // 把真正要序列化的对象替换成PersonProxy 代理
    private Object writeReplace() {
        return new PersonProxy (this);
    }

    // 因为真正被序列化的对象是PersonProxy 代理对象,所以Person的readObject()方法永远不会执行
    // 执行的是PersonProxy 代理对象的readObject()方法
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        // 如果该方法被执行说明有流氓入侵,直接抛异常
        throw new InvalidObjectException("proxy requied");
    }

    static class PersonProxy implements Serializable{
        private String name;
        private int age;

        public PersonProxy(Person person){
            this.name = person.name;
            this.age = person.age;
        }

        // 把读取出来的代理对象再替换回Person对象
        private Object readResolve(){
            return new Person(name,age);
        }
    }
}

使用Parcelable 序列化对象

Parcelable 虽然也是序列化对象的方法,但是它跟java提供的Serializable 在使用上有着极大的差别。

Android设计 Parcelable 的目的是让其支持进程间通信的功能,因此它不具备类似Serializable的版本功能,所以Parcelable 不适合永久存储。

实现Parcelable 接口需要满足两个条件:

  1. 实现Parcelable 接口下的两个方法describeContents()writeToParcel(Parcel out,int flags)

  2. 声明一个非空的静态属性CREATOR且类型为Parcelable.Creator <T>

例如我们想让person类实现Parcelable 接口:

public class Person implements Parcelable {

     private int age;

     // 定义当前传送的 Parcelable实例包含的特殊对象的类别
     public int describeContents() {
         // 一般情况我们用不到,直接为0就行 
         return 0;
     }

     // 在该方法中将对象的属性写入字节流
     public void writeToParcel(Parcel out, int flags) {
         out.writeInt(age);
     }

     // 该静态属性会从Parcel 字节流中生成Parcelable类的实例
     public static final Parcelable.Creator<MyParcelable> CREATOR
             = new Parcelable.Creator<Person>() {
         
         // 该方法接收Parcel解析成对应的实例
         public Person createFromParcel(Parcel in) {
             return new Person(in);
         }
        
         // 根据size创建对应数量的实例数组
         public Person[] newArray(int size) {
             return new Person[size];
         }
     };
     
     private Person(Parcel in) {
         age= in.readInt();
     }
 }

到此Person就具备了序列化的条件。至于读和写就看具体的需求了,最简单的使用方法可以利用Intent传递。

让我惊讶的是Parcelable 的使用并没有那么简单,它牵扯出了一大堆进程间通信相关的问题,待学习到进程间通信时需要再重新梳理一遍。