序列化与反序列化之Parcelable和Serializable浅析

Posted zejian_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了序列化与反序列化之Parcelable和Serializable浅析相关的知识,希望对你有一定的参考价值。

转载请注明出处(万分感谢!):
http://blog.csdn.net/javazejian/article/details/52665164
出自【zejian的博客】

本篇小部分内容摘自android开发艺术探索

  在日常的应用开发中,我们可能需要让某些对象离开内存空间,存储到物理磁盘,以便长期保存,同时也能减少对内存的压力,而在需要时再将其从磁盘读取到内存,比如将某个特定的对象保存到文件中,隔一段时间后再把它读取到内存中使用,那么该对象就需要实现序列化操作,在java中可以使用Serializable接口实现对象的序列化,而在android中既可以使用Serializable接口实现对象序列化也可以使用Parcelable接口实现对象序列化,但是在内存操作时更倾向于实现Parcelable接口,这样会使用传输效率更高效。接下来我们将分别详细地介绍这样两种序列化操作~~~~。

序列化与反序列

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

  • 序列化

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

  • 反序列化

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

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

  • 实现序列化的必要条件

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

  • 序列化的应用情景

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

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

  接下来我们分别介绍两种序列化的实现方式

Serializable

  Serializable是java提供的一个序列化接口,它是一个空接口,专门为对象提供标准的序列化和反序列化操作,使用Serializable实现类的序列化比较简单,只要在类声明中实现Serializable接口即可,同时强烈建议声明序列化标识。如下:

package com.zejian.ipctest;

import java.io.Serializable;

/**
 * Created by zejian
 * Time 2016/9/25.
 * Description:
 */
public class Client implements Serializable{

    /**
     * 生成序列号标识
     */
    private static final long serialVersionUID = -2083503801443301445L;

    private int id;
    private String name;


    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

  如上述代码所示,Client类实现的Serializable接口并声明了序列化标识serialVersionUID,该ID由编辑器生成,当然也可以自定义,如1L,5L,不过还是建议使用编辑器生成唯一标识符。那么serialVersionUID有什么作用呢?实际上我们不声明serialVersionUID也是可以的,因为在序列化过程中会自动生成一个serialVersionUID来标识序列化对象。既然如此,那我们还需不需要要指定呢?由于serialVersionUID是用来辅助序列化和反序列化过程的,原则上序列化后的对象中serialVersionUID只有和当前类的serialVersionUID相同才能够正常被反序列化,也就是说序列化与反序列化的serialVersionUID必须相同才能够使序列化操作成功。具体过程是这样的:序列化操作的时候系统会把当前类的serialVersionUID写入到序列化文件中,当反序列化时系统会去检测文件中的serialVersionUID,判断它是否与当前类的serialVersionUID一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。报出如下UID错误:

Exception in thread "main" java.io.InvalidClassException: com.zejian.test.Client; 
local class incompatible: stream classdesc serialVersionUID = -2083503801443301445, 
local class serialVersionUID = -4083503801443301445

  因此强烈建议指定serialVersionUID,这样的话即使微小的变化也不会导致crash的出现,如果不指定的话只要这个文件多一个空格,系统自动生成的UID就会截然不同的,反序列化也就会失败。ok~,了解这么多,下面来看一个如何进行对象序列化和反序列化的列子:

package com.zejian.ipctest;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
 * Created by zejian
 * Time 2016/9/25.
 * Description:
 */
public class Stest {

    public static void main(String[] args) throws Exception {

        //把对象序列化到文件
        Client client = new Client();
        client.setId(000001);
        client.setName("client");

        ObjectOutputStream oo = new ObjectOutputStream
                (new FileOutputStream("/Users/zejian/Desktop/cache.txt"));
        oo.writeObject(client);
        oo.close();

        //反序列化到内存
        ObjectInputStream oi = new ObjectInputStream
            (new FileInputStream("/Users/zejian/Desktop/cache.txt"));
        Client c_back = (Client) oi.readObject();
        System.out.println("Hi, My name is " + c_back.getName());
        oi.close();

    }
}

  从代码可以看出只需要ObjectOutputStream和ObjectInputStream就可以实现对象的序列化和反序列化操作,通过流对象把client对象写到文件中,并在需要时恢复c_back对象,但是两者并不是同一个对象了,反序列化后的对象是新创建的。这里有两点特别注意的是如果反序列类的成员变量的类型或者类名,发生了变化,那么即使serialVersionUID相同也无法正常反序列化成功。其次是静态成员变量属于类不属于对象,不会参与序列化过程,使用transient关键字标记的成员变量也不参与序列化过程。
  关键字transient,这里简单说明一下,Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。当一个对象被序列化的时候,transient型变量的值不包括在序列化的表示中,然而非transient型的变量是被包括进去的。
  另外,系统的默认序列化过程是可以改变的,通过实现如下4个方法,即可以控制系统的默认序列化和反序列过程:

package com.zejian.ipctest;

import java.io.IOException;
import java.io.ObjectStreamException;
import java.io.Serializable;

/**
 * Created by zejian
 * Time 2016/9/25.
 * Description:
 */
public class Client implements Serializable{

    /**
     * 生成序列号标识
     */
    private static final long serialVersionUID = -4083503801443301445L;


    private int id;
    private String name;


    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }


    /**
     * 序列化时,
     * 首先系统会先调用writeReplace方法,在这个阶段,
     * 可以进行自己操作,将需要进行序列化的对象换成我们指定的对象.
     * 一般很少重写该方法
     * @return
     * @throws ObjectStreamException
     */
    private Object writeReplace() throws ObjectStreamException {
        System.out.println("writeReplace invoked");
        return this;
    }
    /**
     *接着系统将调用writeObject方法,
     * 来将对象中的属性一个个进行序列化,
     * 我们可以在这个方法中控制住哪些属性需要序列化.
     * 这里只序列化name属性
     * @param out
     * @throws IOException
     */
    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        System.out.println("writeObject invoked");
        out.writeObject(this.name == null ? "zejian" : this.name);
    }

    /**
     * 反序列化时,系统会调用readObject方法,将我们刚刚在writeObject方法序列化好的属性,
     * 反序列化回来.然后通过readResolve方法,我们也可以指定系统返回给我们特定的对象
     * 可以不是writeReplace序列化时的对象,可以指定其他对象.
     * @param in
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private void readObject(java.io.ObjectInputStream in) throws IOException,
            ClassNotFoundException {
        System.out.println("readObject invoked");
        this.name = (String) in.readObject();
        System.out.println("got name:" + name);
    }


    /**
     * 通过readResolve方法,我们也可以指定系统返回给我们特定的对象
     * 可以不是writeReplace序列化时的对象,可以指定其他对象.
     * 一般很少重写该方法
     * @return
     * @throws ObjectStreamException
     */
    private Object readResolve() throws ObjectStreamException {
        System.out.println("readResolve invoked");
        return this;
    }
}

  通过上面的4个方法,我们就可以随意控制序列化的过程了,由于在大部分情况下我们都没必要重写这4个方法,因此这里我们也不过介绍了,只要知道有这么一回事就行。ok~,对于Serializable的介绍就先到这里。

Parcelable

  鉴于Serializable在内存序列化上开销比较大,而内存资源属于android系统中的稀有资源(android系统分配给每个应用的内存开销都是有限的),为此android中提供了Parcelable接口来实现序列化操作,Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如通过Intent在activity间传输数据,而Parcelable的缺点就使用起来比较麻烦,下面给出一个Parcelable接口的实现案例,大家感受一下:

package com.zejian.ipctest;
import android.os.Parcel;
import android.os.Parcelable;

/**
 * Created by zejian
 * Time 2016/9/25.
 * Description:
 */
public class NewClient implements Parcelable {

    public int id;
    public String name;
    public User user;

    /**
     * 当前对象的内容描述,一般返回0即可
     * @return
     */
    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * 将当前对象写入序列化结构中
     * @param dest
     * @param flags
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(this.id);
        dest.writeString(this.name);
        dest.writeParcelable(this.user,0);
    }

    public NewClient() {
    }

    /**
     * 从序列化后的对象中创建原始对象
     * @param in
     */
    protected NewClient(Parcel in) {
        this.id = in.readInt();
        this.name = in.readString();
       //User是另一个序列化对象,此方法序列需要传递当前线程的上下文类加载器,否则会报无法找到类的错误
       this.user=in.readParcelable(Thread.currentThread().getContextClassLoader());
    }

    /**
     * public static final一个都不能少,内部对象CREATOR的名称也不能改变,必须全部大写。
     * 重写接口中的两个方法:
     * createFromParcel(Parcel in) 实现从Parcel容器中读取传递数据值,封装成Parcelable对象返回逻辑层,
     * newArray(int size) 创建一个类型为T,长度为size的数组,供外部类反序列化本类数组使用。
     */
    public static final Parcelable.Creator<NewClient> CREATOR = new Parcelable.Creator<NewClient>() {
        /**
         * 从序列化后的对象中创建原始对象
         */
        @Override
        public NewClient createFromParcel(Parcel source) {
            return new NewClient(source);
        }

        /**
         * 创建指定长度的原始对象数组
         * @param size
         * @return
         */
        @Override
        public NewClient[] newArray(int size) {
            return new NewClient[size];
        }
    };
}

  从代码可知,在序列化的过程中需要实现的功能有序列化和反序列以及内容描述。其中writeToParcel方法实现序列化功能,其内部是通过Parcel的一系列write方法来完成的,接着通过CREATOR内部对象来实现反序列化,其内部通过createFromParcel方法来创建序列化对象并通过newArray方法创建数组,最终利用Parcel的一系列read方法完成反序列化,最后由describeContents完成内容描述功能,该方法一般返回0,仅当对象中存在文件描述符时返回1。同时由于User是另一个序列化对象,因此在反序列化方法中需要传递当前线程的上下文类加载器,否则会报无法找到类的错误。
  简单用一句话概括来说就是通过writeToParcel将我们的对象映射成Parcel对象,再通过createFromParcel将Parcel对象映射成我们的对象。也可以将Parcel看成是一个类似Serliazable的读写流,通过writeToParcel把对象写到流里面,在通过createFromParcel从流里读取对象,这个过程需要我们自己来实现并且写的顺序和读的顺序必须一致。ok~,到此Parcelable接口的序列化实现基本介绍完。
  那么在哪里会使用到Parcelable对象呢?其实通过Intent传递复杂类型(如自定义引用类型数据)的数据时就需要使用Parcelable对象,如下是日常应用中Intent关于Parcelable对象的一些操作方法,引用类型必须实现Parcelable接口才能通过Intent传递,而基本数据类型,String类型则可直接通过Intent传递而且Intent本身也实现了Parcelable接口,所以可以轻松地在组件间进行传输。

方法名称含义
putExtra(String name, Parcelable value)设置自定义类型并实现Parcelable的对象
putExtra(String name, Parcelable[] value)设置自定义类型并实现Parcelable的对象数组
public Intent putParcelableArrayListExtra(String name, ArrayList value)设置List数组,其元素必须是实现了Parcelable接口的数据

  除了以上的Intent外系统还为我们提供了其他实现Parcelable接口的类,再如Bundle、Bitmap,它们都是可以直接序列化的,因此我们可以方便地使用它们在组件间进行数据传递,当然Bundle本身也是一个类似键值对的容器,也可存储Parcelable实现类,其API方法跟Intent基本相似,由于这些属于android基础知识点,这里我们就不过多介绍了。

Parcelable 与 Serializable 区别

  • 两者的实现差异
      Serializable的实现,只需要实现Serializable接口即可。这只是给对象打了一个标记(UID),系统会自动将其序列化。而Parcelabel的实现,不仅需要实现Parcelabel接口,还需要在类中添加一个静态成员变量CREATOR,这个变量需要实现 Parcelable.Creator 接口,并实现读写的抽象方法。

  • 两者的设计初衷
      Serializable的设计初衷是为了序列化对象到本地文件、数据库、网络流、RMI以便数据传输,当然这种传输可以是程序内的也可以是两个程序间的。而Android的Parcelable的设计初衷是由于Serializable效率过低,消耗大,而android中数据传递主要是在内存环境中(内存属于android中的稀有资源),因此Parcelable的出现为了满足数据在内存中低开销而且高效地传递问题。

  • 两者效率选择
      Parcelable的性能比Serializable好,在内存开销方面较小,所以Android应用程序在内存间数据传输时推荐使用Parcelable,如activity间传输数据和AIDL数据传递,而Serializable将数据持久化的操作方便,因此在将对象序列化到存储设置中或将对象序列化后通过网络传输时建议选择Serializable(Parcelable也是可以,只不过实现和操作过程过于麻烦并且为了防止android版本不同而导致Parcelable可能不同的情况,因此在序列化到存储设备或者网络传输方面还是尽量选择Serializable接口)。

  • 两者需要注意的共同点
      无论是Parcelable还是Serializable,执行反序列操作后的对象都是新创建的,与原来的对象并不相同,只不过内容一样罢了。

Android studio 中的快捷生成方式

  • Android studio 快捷生成Parcelable代码

  在程序开发过程中,我们实现Parcelable接口的代码都是类似的,如果我们每次实现一个Parcelable接口类,就得去编写一次重复的代码,这显然是不可取的,不过幸运的是,android studio 提供了自动实现Parcelable接口的方法的插件,相当实现,我们只需要打开Setting,找到plugin插件,然后搜索Parcelable插件,最后找到android Parcelable code generator 安装即可:
这里写图片描述

重启android studio后,我们创建一个User类,如下:

这里写图片描述

然后使用刚刚安装的插件协助我们生成实现Parcelable接口的代码,window快捷键:Alt+Insert,Mac快捷键:cmd+n,如下:

这里写图片描述

最后结果如下:

这里写图片描述

  • Android studio 快捷生成Serializable的UID
      在正常情况下,AS是默认关闭serialVersionUID生成提示的,我们需要打开setting,找到检测(Inspections选项),开启 Serializable class without serialVersionUID 检测即可,如下:

这里写图片描述

然后新建User类实现Serializable接口,右侧会提示添加serialVersionUID,如下:

这里写图片描述

最后在类名上,Alt+Enter(Mac:cmd+Enter),快捷代码提示,生成serialVersionUID即可:

这里写图片描述

最终结果如下:
这里写图片描述

ok~,以上便是Parcelable与Serializable接口的全部内容,本篇结束。

以上是关于序列化与反序列化之Parcelable和Serializable浅析的主要内容,如果未能解决你的问题,请参考以下文章

Serialzable和Parcelable的区别?Bunder传递对象为什么需要序列化?

序列化--Serializable与Parcelable

Android Parcelable反序列化漏洞分析与利用

Android Parcelable反序列化漏洞分析与利用

Android Parcelable反序列化漏洞分析与利用

Android :安卓学习笔记之 通过Intent传递类对象(实现Serializable和Parcelable接口)