Android中的序列化方案

一、Serializable接口

Serializable是 Java 提供的序列化接口,它是一个空接口,里面什么都没有,那么这个序列化和反序列化的工作的细节最后是谁来完成的呢?答案是:Serializable默认由JVM来完成这些工作,可以理解成Serializable 接口只是一个提供给JVM识别的标识接口。

public interface Serializable {}

Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。

(1)、Serializable入门

/**
 * 创建时间:2019/8/21
 * 创建人:singleCode
 * 功能描述:
 **/
public class Student implements Serializable {
    //serialVersionUID唯一标识了一个可序列化的类
    private static final long serialVersionUID = -3057674708637541821L;
    private String name;
    private String age;
    private String sex;
    private Cursor1 cursor1;//Cursor1也需要实现Serializable接口
    //用transient关键字标记的成员变量不参与序列化(在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null)
    private transient Cursor1 cursor2;
    //静态成员变量属于类不属于对象,所以不会参与序列化(对象序列化保存的是对象的“状态”,也就是它的成员变量,因此序列化不会关注静态变量)
    private static Cursor1 cursor3;

    public Student(String name, String age, String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
        cursor1 = new Cursor1(name);
        cursor2 = new Cursor1(age);
        cursor3 = new Cursor1(sex);
    }

    public static void main(String ...args){
        Student student = new Student("婧儿","18","女");
        try {
            byte[] bytes = SerializableUtil.writeSerializableObj(student);
            System.out.println(Arrays.toString(bytes));
            Student student1 = SerializableUtil.readSerializableObj(bytes);
            System.out.println(student1.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
...//seter、geter方法
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                ", sex='" + sex + '\'' +
                ", cursor1=" + cursor1 +
                ", cursor2=" + cursor2 +
                '}';
    }
}
​
/**
 * 创建时间:2019/8/21
 * 创建人:singleCode
 * 功能描述:Cursor1 也需要实现Serializable接口
 **/
public class Cursor1 implements Serializable {
    private static final long serialVersionUID = 9176659720108956460L;
    private String name;

    public Cursor1() {
    }

    public Cursor1(String name) {
        this.name = name;
    }
...//seter、geter方法
    @Override
    public String toString() {
        return "Cursor1{" +
                "name='" + name + '\'' +
                '}';
    }
}

Serializable 有以下几个特点:

  • 可序列化类中,如果有未实现 Serializable 的属性,则该属性无法被序列化/反序列化
  • 也就是说,反序列化一个类的过程中,它的非可序列化的属性将会调用无参构造函数重新创建
  • 因此这个属性的无参构造函数必须可以访问,否者运行时会报错
  • 一个实现序列化的类,它的子类也是可序列化的

(2)、serialVersionUID与兼容性

  • serialVersionUID的作用

serialVersionUID 用来表明类的不同版本间的兼容性。如果你修改了此类, 要修改此值。否则以前用老版本的类序列化的类恢复时会报错: InvalidClassException。这句话什么意思呢?就如上面代码Cursor1 的serialVersionUID = 9176659720108956460L;这个时候我们将序列化后的字节序列存到文件中,然后我们改掉这个serialVersionUID 值,改完后我们再拿刚刚存的文件进行反序列化,这时候就会报错啦。因为两者的serialVersionUID 是不同的校验不通过

  • Android studio如何提示我们生成serialVersionUID

Settings->Editor->Inspections->Java->Serialization issues 找到“ Serializable class without serialVersionUID ”选项,并勾选上,且修改右侧Severity为Error。最后点击apply即可生效。这样我们一旦继承Serializable 接口就会标红提示我们生成serialVersionUID ,如同Java中的导包一样,很方便。

  • 兼容性问题

为了在反序列化时,确保类版本的兼容性,最好在每个要序列化的类中加入 private static final long serialVersionUID这个属性,具体数值自己定义。这样,即使某个类在与之对应的对象 已经序列化出去后做了修改,该对象依然可以被正确反序列化。否则,如果不显式定义该属性,这个属性值将由JVM根据类的相关信息计算,而修改后的类的计算 结果与修改前的类的计算结果往往不同,从而造成对象的反序列化因为类版本不兼容而失败。

不显式定义这个属性值的另一个坏处是,不利于程序在不同的JVM之间的移植。因为不同的编译器实现该属性值的计算策略可能不同,从而造成虽然类没有改变,但是因为JVM不同,出现因类版本不兼容而无法正确反序列化的现象出现。因此 JVM 规范强烈 建议我们手动声明一个版本号,这个数字可以是随机的,只要固定不变就可以。同时最好是 private 和 final 的,尽量保证不变。

二、Externalizable接口

Serializable 接口默认是通过JVM来实现序列化和反序列化操作的,而Externalizable与Serializable 最大的区别就在于Externalizable将 序列化和反序列化细节是交给开发者去完成,需要我们重写下面的两个方法来实现序列化和放序列化的细节。

这里就有人会问了,为什么会说Serializable 接口默认是通过JVM来实现序列化和反序列化操作的呢?难道Serializable接口也可以由我们自己来掌控序列化和反序列化的细节?答案对啦!Serializable同样能够像Externalizable一样来自己接管序列化细节。

public interface Externalizable extends Serializable {
   void writeExternal(ObjectOutput var1) throws IOException;
​
   void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException;
}

看Externalizable 的接口源码可以看出来,实际上它还是继承自Serializable 。只是这个接口将序列化和反序列化的方法抛出来给用户自己去实现。

/**
 * 创建时间:2019/8/21
 * 创建人:singleCode
 * 功能描述:
 **/
public class Student1 implements Externalizable {
    private static final long serialVersionUID = 138348194244579432L;
    private String name;
    private String age;
    private String sex;
/**
 * 使用Externalizable进行序列化时,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,
 * 然后再将被保存对象的字段的值分别填充到新对象中,因此,在实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public
 */
public Student1() {
}

    public Student1(String name, String age, String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    public static void main(String... args) {
        Student1 student = new Student1("婧儿", "19", "女");
        try {
            byte[] bytes = SerializableUtil.writeSerializableObj(student);
            System.out.println(Arrays.toString(bytes));
            Student1 student1 = SerializableUtil.readSerializableObj(bytes);
            System.out.println(student1.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeObject(age);
        out.writeObject(sex);
    }

    @Override
    public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException {
        name = (String) in.readObject();
        age = (String) in.readObject();
        sex = (String) in.readObject();
    }

  ....//seter、geter方法
    @Override
    public String toString() {
        return "Student1{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                ", sex='" + sex + '\'' +
                '}';
    }
}

而继承Serializable接口的对象如果想自己接管序列化细节,需要自己实现readObject和writeObject方法人,如下代码所示。

public class Student2 implements Serializable {

    private static final long serialVersionUID = 9135534947626580914L;
    private String name;
    private Float score;

    public Student2(String name, Float score) {
        this.name = name;
        this.score = score;
    }

    /**
     * 继承Serializable接口的类如果想自己接管反序列化细节,
     * ObjectInputStream源码中回去判断当前类是否实现了readObject方法,
     * 如果实现了,就调用该对象的readObject来处理反序列化细节,否则执行Serializable默认的反序列化方方案
     * @param inputStream
     * @throws ClassNotFoundException
     * @throws IOException
     */
    private void readObject(ObjectInputStream inputStream) throws ClassNotFoundException, IOException {
        System.out.println("readObject");
        inputStream.defaultReadObject();
        name = (String)inputStream.readObject();
        score = inputStream.readFloat();
    }
    /**
     * 继承Serializable接口的类如果想自己接管序列化细节,
     * ObjectOutputStream源码中回去判断当前类是否实现了writeObject方法,
     * 如果实现了,就调用该对象的writeObject来处理反序列化细节,否则执行Serializable默认的序列化方方案
     * @param outputStream
     * @throws IOException
     */
    private void writeObject(ObjectOutputStream outputStream) throws IOException {
        System.out.println("writeObject");
        outputStream.defaultWriteObject();
        outputStream.writeObject(name);
        outputStream.writeFloat(score);
    }
    public static void main(String ...args){
        Student2 student2 = new Student2("婧儿",100f);
        try {
            byte[] bytes = SerializableUtil.writeSerializableObj(student2);
            System.out.println(Arrays.toString(bytes));

            Student2 student21 = SerializableUtil.readSerializableObj(bytes);
            System.out.println(student21.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return "Student2{" +
                "name='" + name + '\'' +
                ", score=" + score +
                '}';
    }
}

三、Parcelable接口序列化方案

Parcelable是Android为我们提供的序列化的接口,Parcelable相对于Serializable的使用相对复杂一些,但Parcelable的效率相对Serializable也高很多,这一直是Google工程师引以为傲的,有时间的可以看一下Parcelable和Serializable的效率对比 Parcelable vs Serializable 号称快10倍的效率

Parcelable是Android SDK提供的,它是基于内存的,由于内存读写速度高于硬盘,因此Android中的跨进程对象的传递一般使用Parcelable

首先我们来看看如何使用这个Parcelable接口,通过使用,我们再来一步步讲解它的工作原理

/**
 * 创建时间:2019/8/26
 * 创建人:singleCode
 * 功能描述:
 **/
public class PStudent  implements Parcelable {
    private String name;
    private List<String> bookName;
    private String age;

    /**
     *从Parcel对象中读取属性值,将Parcel对象转换成我们需要的实列对象
     * @param in
     */
    protected PStudent(Parcel in) {
        name = in.readString();
        bookName = in.createStringArrayList();
        age = in.readString();
    }

    /**
     * 将对象转换成Parcel对象
     * @param dest  要写入的Parcel对象
     * @param flags 表示这个对象写入方式
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeStringList(bookName);
        dest.writeString(age);
    }
    /**
     * 描述当前 Parcelable 实例的对象类型
     * 比如说,如果对象中有文件描述符,这个方法就会返回上面的 CONTENTS_FILE_DESCRIPTOR
     * 其他情况会返回一个位掩码
     * @return
     */
    @Override
    public int describeContents() {
        return 0;
    }
    /**
     * 实现类必须有一个 Creator 属性,用于反序列化,将 Parcel 对象转换为 Parcelable
     * @param <T>
     */
    public static final Creator<PStudent> CREATOR = new Creator<PStudent>() {
        //反序列化的方法,将Parcel还原成Java对象
        @Override
        public PStudent createFromParcel(Parcel in) {
            return new PStudent(in);
        }
        //提供给外部类反序列化这个数组使用。
        @Override
        public PStudent[] newArray(int size) {
            return new PStudent[size];
        }
    };
}

 

通过上面的的代码可以很容易看出来,Parcelable接口实现序列化和反序列化的工作最后都是交给Parcel对象来完成的。那么什么是Parcel呢?

(1)Parcel的简介

在介绍之前我们需要先了解Parcel是什么?Parcel翻译过来是打包的意思,其实就是包装了我们需要传输的数据,然后在Binder中传输(至于很多朋友弄不清楚Binder是什么没关系,这里我们就先把它理解成一个Android中为了数据传输的通道就行了,后面我会再详细讲解Binder的机制和原理,这里一句两句说不完,因为东西太多太难了。我也还在学习中。),也就是用于跨进程传输数据。简单来说,Parcel提供了一套机制,可以将序列化之后的数据写入到一个共享内存中,其他进程通过Parcel可以从这块共享内存中读出字节流,并反序列化成对象,下图是这个过程的模型。

 

那么Parcel是如何实现这些功能的呢?看看Parcel的源码我们就能一目了然了。

有人说,我完全没看出来Binder在这里体现出来的作用啊。那是因为这里的Binder本来就是针对如果对象是IBinder对象来说的。我们来看一下官方的解释:

而在我们日常的开发中,一般在AIDL开发(进程间通信的一种)中是这里的Binder机制体现最显著的时候。

四、Serializable 序列化与反序列化流程与坑

 

(1)流程

Serializable 序列化与反序列化分别通过 ObjectOutputStream 和 ObjectInputStream 进行

/**

* 创建时间:2019/8/21

* 创建人:singleCode

* 功能描述:

**/

public class SerializableUtil {

/**

* 通过ObjectOutputStream 将对象写入流中,转换成二进制串。

*

**/

public static <T> byte[] writeSerializableObj(T t)throws Exception{

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);

objectOutputStream.writeObject(t);

byte[] bytes = outputStream.toByteArray();

outputStream.close();

objectOutputStream.close();

return bytes;

}

/**

* 通过ObjectInputStream 将二进制串从流中读出来,再转换成对象。

**/

public static <T> T readSerializableObj(byte[] bytes) throws Exception{

ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);

ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);

T t = (T) objectInputStream.readObject();

inputStream.close();

objectInputStream.close();

return t;

}

}

(2)采坑时间到

  • 多引用写入造成的坑

啥意思呢?就是一个对象,写入一次后,被其他地方引用,修改了属性后,再次写入时,并不会写入新的属性值(即后面写入的对象并不会重新分配的内存空间,也不会修改已有内存空间内的值)。这样说好像还是不太清晰,通过代码来看或许会更直接些:

如上图所示,最终执行结果,并不是我们想象的那样得到两个不同name的对象。那么到底是为什么呢?原因就是在默认情况下, 对于一个实例的多个引用,为了节省空间,只会写入一次,再次写入时只会在后面追加几个字节代表某个实例的引用,并不修改已有内容或者重新分配内存。

那么如何解决这个坑呢?有两个方法,如下图:

 

  • 子类实现序列化,但是父类不实现序列化/ 对象引用 带来的坑
/**
 * 创建时间:2019/8/23
 * 创建人:singleCode
 * 功能描述:
 **/
public class Person {
    public String name;

    /**
     * 由于name属性是父类Person的属性,而父类没有继承序列化接口
     *所以父类必须有无参构造器,用来子类进行反序列化时,
     * 通过无参构造器创建一个父类对象来初始化name值
     */
    public Person() {

    }

    public Person(String name) {
        this.name = name;
    }
}
/**
 * 创建时间:2019/8/23
 * 创建人:singleCode
 * 功能描述:
 **/
public class SpStudent extends Person implements Serializable {
    private static final long serialVersionUID = 4918361735692424142L;
    private String age;
    private String sex;

    public SpStudent(String name, String age, String sex) {
        super(name);
        this.age = age;
        this.sex = sex;
    }
    public static void main(String ...args){
        SpStudent spStudent = new SpStudent("婧儿","18","女");
        try {
            byte[] bytes = SerializableUtil.writeSerializableObj(spStudent);
            System.out.println(Arrays.toString(bytes));

            SpStudent spStudent1 = SerializableUtil.readSerializableObj(bytes);

            System.out.println(spStudent1.toString());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
....//seter、geter方法
    @Override
    public String toString() {
        return "SpStudent{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                ", sex='" + sex + '\'' +
                '}';
    }
}

这里子类继承一个没有继承序列化接口的父类,且子类继承了序列化接口,如果父类没有实现无参构造器,那么就会出现下图所示的错误。

  • 反序列化时,类的属性已经发生变化 的坑

前面我们说,在反序列化时如果我们修改了serialVersionUID 的值,会导致反序列化失败,直接报如下错误

那么如果在反序列化之前,我们修改了类的属性(如:增加、删除、修改名称、修改属性变量类型呢?)是否也会出现无法序列化的问题呢?

经过验证,得出的结果如下:

(1)、增加、删除、修改属性名称:反序列化时都不直接报错,依旧能够反序列化成功,只是增加和修改名称后的属性会被置成默认值。而删除的属性依旧保存在流中,不会被读取出来。

(2)、修改属性变量类型:一旦属性的变量类型被修改了,在反序列化时就会直接报错,如下图所示

感兴趣或者不相信这个结果的的朋友可以先序列化一个对象并保存到文件中,然后修改类的属性再读取文件反序列化,看现象。这里我就只贴出序列化保存文件以及从文件中反序列化对象的代码啦。

/**
 * 保存对象到文件
 * @param o
 * @param path 文件路径
 */
public static void saveSerializableObj(Object o,String path){
    ObjectOutputStream objectOutputStream = null;
    try {
        FileOutputStream fileOutputStream = new FileOutputStream(path);
        objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(o);
        objectOutputStream.close();
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        if(objectOutputStream !=null){
            try {
                objectOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 从文件中反序列化出对象实例
 * @param path 文件路径
 * @param <T>
 * @return
 */
synchronized public static <T> T readObject(String path) {
    ObjectInputStream ojs = null;
    try {
        // 创建反序列化对象
        ojs = new ObjectInputStream(new FileInputStream(path));
        // 还原对象
        return (T) ojs.readObject();
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    } finally {
        if(ojs!=null){
            try {
                // 释放资源
                ojs.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    return null;
}
  • 使用Parcelable序列化方案时,需要的序列化的对象内部有枚举变量时

当被序列化的对象有枚举对象,经过Intent传递后,反序列化出来的枚举变量的值变成了null。那么如何解决这个问题呢?狠简单,我们只需要如下图手动添加代码,让枚举参与到序列化过程中来就行啦。

 

五、Serializable和Parcelable对比

(1)、Serializable是Java中的序列化接口,其使用起来简单但开销较大(因为Serializable在序列化过程中使用了反射机制,故而会产生大量的临时变量,从而导致频繁的GC),并且在读写数据过程中,它是通过IO流的形式将数据写入到硬盘或者传输到网络上。

(2)、Parcelable则是以IBinder作为信息载体,在内存上开销比较小,因此在内存之间进行数据传递时,推荐使用Parcelable,而Parcelable对数据进行持久化或者网络传输时操作复杂,一般这个时候推荐使用Serializable。

性能比较总结描述

首先Parcelable的性能要强于Serializable的原因我需要简单的阐述一下

  • 在内存的使用中,前者在性能方面要强于后者

  • 后者在序列化操作的时候会产生大量的临时变量,(原因是使用了反射机制)从而导致GC的频繁调用,因此在性能上会稍微逊色

  • Parcelable是以Ibinder作为信息载体的.在内存上的开销比较小,因此在内存之间进行数据传递的时候,Android推荐使用Parcelable,既然是内存方面比价有优势,那么自然就要优先选择.

  • 在读写数据的时候,Parcelable是在内存中直接进行读写,而Serializable是通过使用IO流的形式将数据读写入在硬盘上. 但是:虽然Parcelable的性能要强于Serializable,但是仍然有特殊的情况需要使用Serializable,而不去使用Parcelable,因为Parcelable无法将数据进行持久化,因此在将数据保存在磁盘的时候,仍然需要使用后者,因为前者无法很好的将数据进行持久化.(原因是在不同的Android版本当中,Parcelable可能会不同,因此数据的持久化方面仍然是使用Serializable)

方案选择时的考量

  • 在使用内存方面,Parcelable比Serializable性能高,所以推荐使用Parcelable。

  • Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC。

  • Parcelable不能使用在要将数据存储在磁盘上的情况,因为Parcelable不能很好的保证数据的持续性,在外界有变化的情况下,建议使用Serializable

 

发布了29 篇原创文章 · 获赞 3 · 访问量 893

猜你喜欢

转载自blog.csdn.net/LVEfrist/article/details/100037934