Android序列化详解及最佳实践(Serialize&Parcel)

序列化


我们在开发中经常会遇到一些需要序列化对象数据的场景,接下来我们一起来了解一下Java和Android中的序列化。

序列化场景

根据用途,序列化场景有主要有以下几个:

  • 需要永久性保存对象到本地文件中。
  • 在网络中传递对象。
  • 在进程间传递对象。

JAVA中的Serialize机制

java中serialize机制,是能将数据对象存入字节流中,在需要的时候重新生成对象,主要应用是利用外部存储设备保存对象状态,以及通过网络传输对象等。

Android中的新的序列化机制Parcel

在Android系统中,定位为针对内存受限的设备,因此对性能要求更高,另外系统中采用了新的IPC(进程间通信)机制,必然要求使用性能更出色的对象传输方式。在Android系统中parcel设计主要是在IPC中通信,所有的操作都在内存中进行,这样效率更高,它的定位是轻量级的高效的对象序列化和反序列化机制。

Parcel类主要是方便实现上层抽象对象或数据打包做跨进程传输而封装的一个类。其作用,就是将要写入的数据,规整到一个连续的buffer内存中,同时记录一些数据信息属性。远端进程可接收后根据这些属性和读取顺序来克隆还原。连续buffer内存方便驱动实现,同时效率也高。目前Android中,除了service_manager外[太简单了,而且纯C写的…],都是走parcel。

Java中的Serializable是通过存储介质进行传输的,所以速度慢。parcel是基于内存传输的,比磁盘I/O要块,而且更加轻量级。

parcel在内存中的结构是一块连续的内存,会动根据需要自动扩展大小。

parcel传递数据,可以分为3种,传递方式也不一样:

  • 小型数据: 从用户空间(数据源进程)copy到kernel空间(binder 驱动中)再写回用户空间(目标进程)。
  • 大型数据: 使用Andrid的匿名共享内存(Ashmem)传递。
  • binder对象: kernel binder驱动专门处理。

Android中序列化实现


Serializable接口

传统的Java,串行化技术,实现Serializable接口,原理和Parceble差不多,均将自己的类转换为基本的类型,比如说字节数组。但在Android中不是很实用,比Parcelable效率低。

public class Person implements Serializable {
    private static final long serialVersionUID = -7060210544600464482L;
    private String name;
    private int age;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
    public int getAge(){
        return age;
    }
    public void setAge(int age){
        this.age = age;
    }
}

serialVersionUID用于在反序列化时的验证过程,JAVA序列化的机制是通过判断类的serialVersionUID来验证的版本一致的。

在进行反序列化时,JVM会把传来的字节流中的serialVersionUID于本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的,可以进行反序列化,否则会抛出反序列化版本一致的异常:InvalidCastException。

Parcelable 接口

Parcel可以简单理解为一个存放读取数据的容器, Android系统中的binder进程间通信(包括AIDL)都使用了Parcel类来进行客户端与服务端数据的交互。在Android系统中,Java空间和C++都实现了Parcel,由于它在C/C++中,直接使用了内存来读取数据,因此,它更有效率。

我们来看Demo:

public class Book implements Parcelable {
    private String bookName;
    private String author;
    private int publishDate;
    public Book(){
    }
    public String getBookName(){
        return bookName;
    }
    public void setBookName(String bookName){
        this.bookName = bookName;
    }
    public String getAuthor(){
        return author;
    }
    public void setAuthor(String author){
        this.author = author;
    }
    public int getPublishDate(){
        return publishDate;
    }
    public void setPublishDate(int publishDate){
        this.publishDate = publishDate;
    }
    @Override
    public int describeContents(){
        return 0;
    }
    @Override
    public void writeToParcel(Parcel out, int flags){
        out.writeString(bookName);
        out.writeString(author);
        out.writeInt(publishDate);
    }
    public static final Parcelable.Creator CREATOR = new Creator(){
   
     @Override
        public Book[] newArray(int size){
            return new Book[size];
        }
        @Override
        public Book createFromParcel(Parcel in){
            return new Book(in);
        }
    };
    public Book(Parcel in){
        //如果元素数据是list类型的时候需要: lits = new ArrayList in.readList(list);
        //否则会出现空指针异常.并且读出和写入的数据类型必须相同.如果不想对部分关键字进行序列化,可以使用transient关键字来修饰以及static修饰.
        bookName = in.readString();
        author = in.readString();
        publishDate = in.readInt();
    }
}

Parcelable接口实现序列化较为复杂一些,不仅仅需要声明,还需要实现相应的方法。

需要实现内部的方法:

  • writeToParcel将对象数据序列化成一个Parcel对象,序列化之后成为Parcel对象,以便Parcel容器取出数据。
  • 重写内容接口描述方法describeContents,默认值为0就可以了。
  • Public static final Parcelable.CreatorCREATOR (将Parcel容器中的数据转换成对象数据) 同时需要实现两个方法:
    • newArray(int size)返回对象数据的大小。
    • CreateFromParcel(从Parcel容器中取出数据并进行转换)。

也就是说我们先利用writeToParcel方法写入对象,再利用createFromParcel方法读取对象,因此这两个方法中的读写顺序必须一致,否则会出现数据紊乱。

Parcelable的工作原理

无论是对数据的读还是写都需要使用Parcel作为中间层将数据进行传递。在Java应用层是先创建Parcel对象,然后再调用相关的读写操作。

例如readInt()方法,调用的方法过程:首先是将Parcel(Java)对象转换成Parcel(C++)对象,然后被封装在Parcel中的相关数据由C++底层来完成数据的序列化操作。

  • 整个读写全是在内存中进行,主要是通过malloc()、realloc()、memcpy()等内存操作进行,所以效率比JAVA序列化中使用外部存储器会高很多。
  • 读写时是4字节对齐的,可以看到#define PAD_SIZE(s) (((s)+3)&~3)这句宏定义就是在做这件事情。
  • 如果预分配的空间不够时newSize = ((mDataSize+len)*3) / 2;会一次多分配50%。
  • 对于普通数据,使用的是mData内存地址,对于IBinder类型的数据以及FileDescriptor使用的是mObjects内存地址。后者是通过flatten_binder()和unflatten_binder()实现的,目的是反序列化时读出的对象就是原对象而不用重新new一个新对象。

总结


  1. 序列化场景有主要有:需要永久性保存对象到本地文件中、在网络中传递对象、在进程间传递对象。
  2. java中serialize机制,是能将数据对象存入字节流中,在需要的时候重新生成对象,主要应用是利用外部存储设备保存对象状态,以及通过网络传输对象等。
  3. 在Android系统中parcel设计主要是在IPC中通信,所有的操作都在内存中进行,这样效率更高,它的定位是轻量级的高效的对象序列化和反序列化机制。
  4. 在Android系统中,Java空间和C++都实现了Parcel,由于它在C/C++中,直接使用了内存来读取数据,因此,它更有效率。
  5. 无论是对数据的读还是写都需要使用Parcel作为中间层将数据进行传递。在Java应用层是先创建Parcel对象,然后再调用相关的读写操作。
  6. Serializable实现简单,不需要任何额外的序列化操作。而Parcelable实现较为复杂,有特定的接口和对象句柄需要实现。
  7. Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC,而相比之下Parcelable的性能更高(号称10倍优于Serializable),所以当在使用内存时(如:序列化对象在网络中传递对象或序列化在进程间传递对象),更推荐使用Parcelable接口。
  8. Parcelable不能使用在要将数据存储在磁盘上的情况,因为Parcelable不能很好的保证数据的持续性在外界有变化的情况下。尽管Serializable效率低点,也不提倡用,但在这种情况下,还是建议你用Serializable 。

你可能感兴趣的:(Android开发,android,java,序列化,Parcel,Serialize)