彻底理解Serializable和Parcelable

2019-03-15 10:48:50 浏览数 (1)

Serializable和Parcelable, 都可以用来做序列化,网上也有很多文章分析它们的优缺点,大部分的结论都是Serializable使用简单但是低效,Parcelable使用麻烦但是高效,em...,也对,但是总感觉缺了点意思,这篇文章带你彻底理解二者,拒绝知识盲区。

先抛出几个问题,带着问题我们一起探索。

  1. 什么是序列化和反序列化,为什么需要序列化?
  2. Java中Serializable的序列化是怎么实现的?
  3. Android中Parcelable的序列化是怎么实现的?
  4. 有哪些使用场景,实现方式怎么选?

em, 可以先思考一下这几个问题。 (5分钟之后...)

第一个问题:什么是序列化和反序列化?

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

这里有二个关键字,存储和传输,存储的场景比如对象的持久化,传输的场景比如将对象通过网络传输,然后在需要使用的时候,反序列化,重新创建对象。

第二个问题: Java中Serializable的序列化是怎么实现的?

要弄清楚这个问题,只能去JDK源码里面找答案了(这里基于JDK8)。不过现在,我想通过一个简单的序列化字符串的例子开始,先有个大概的印象。

序列化:(为了简单起见,只贴了关键代码,下面就不再赘述了)
代码语言:javascript复制
//......
FileOutputStream fileOutputStream = new FileOutputStream(new File("string_file"));
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject("Hello World!");
objectOutputStream.flush();
//......
反序列化:
代码语言:javascript复制
//......
FileInputStream fileInputStream = new FileInputStream(new File("string_file"));
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
String string = (String) objectInputStream.readObject();
//......

看一下生成的二进制string_file文件的内容。

代码语言:javascript复制
ACED0005 74000C48 656C6C6F 20576F72 6C6421
//ACED 是一个stream header魔数,可以类比Java类字节码文件的 0xCAFEBABE 魔数
//0005 是stream header的版本号
//74 是字符串类型的标识,如果字符串长度小于0xFFFF,写入0x74,否则写入0x7C
//000C 是字符串的长度,我们写入的是“Hello world!”,12个字符
//48 656C6C6F 20576F72 6C6421,这一坨就是Hello world!字符串了

上面这个文件的结构还是比较简单的,通过魔数和版本号校验一下文件的合法性,然后就是通过(字段类型 长度 源数据)的规律,写入到文件中。 这里你可能已经有了个疑问,我们都知道如果标记了Serializable接口,一般都要求我们重写serialVersionUID字段(即使不明确指定,编译器也会帮我们根据类字段自动生成一个),我们的经验是,重写了该字段以后,即使类的结构发生变化,还是能序列化成功。但是我们生成的字节码文件中没有看到serialVersionUID?难道我们的经验错了?问题先丢在这里,后面我们再回来解答。

开始看ObjectOutputStream的实现

代码语言:javascript复制
//ObjectOutputStream.java
 public ObjectOutputStream(OutputStream out) throws IOException {
        //...
        //构建一个输出流,后面会用来写文件
        bout = new BlockDataOutputStream(out);
        //...
        //写入stream header魔数和版本号
        writeStreamHeader();
        //...
 }
 //还是贴一下写入魔数的代码,
 protected void writeStreamHeader() throws IOException {
        bout.writeShort(STREAM_MAGIC);
        bout.writeShort(STREAM_VERSION);
 }

 //写入对象的方法
 public final void writeObject(Object obj) throws IOException {
        //...
        try {
            writeObject0(obj, false);
        } catch (IOException ex) {
            //...
        }
 }

 private void writeObject0(Object obj, boolean unshared)
        throws IOException {
        // ...省略已一坨代码
        if (obj instanceof String) {
            //我们例子里面写入就是String,这里重点关注一下
             writeString((String) obj, unshared);
        } else if (cl.isArray()) {
             writeArray(obj, desc, unshared);
        } else if (obj instanceof Enum) {
            writeEnum((Enum<?>) obj, desc, unshared);
        } else if (obj instanceof Serializable) {
            writeOrdinaryObject(obj, desc, unshared);
        } else {
            if (extendedDebugInfo) {
                throw new NotSerializableException(
                        cl.getName()   "n"   debugInfoStack.toString());
            } else {
              throw new NotSerializableException(cl.getName());
            }
        }   
 }

 private void writeString(String str, boolean unshared) throws IOException {
        handles.assign(unshared ? null : str);
        long utflen = bout.getUTFLength(str);
        if (utflen <= 0xFFFF) {
            bout.writeByte(TC_STRING);
            bout.writeUTF(str, utflen);
        } else {
            bout.writeByte(TC_LONGSTRING);
            bout.writeLongUTF(str, utflen);
        }
  }

上面的代码都比较简单,通过查看源码,我们看到JDK对String、Array、Enum、Serializable这几种类型,分别有一套序列化逻辑,我们再做一个实验,这一次我们写入一个自定义的Person类,类定义如下。

代码语言:javascript复制
public class Person implements Serializable {

    //定义成有规律的数字,方便查看
    private static final long serialVersionUID = 0x87654321;

    private int age;
    private String name;

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

}

//...
Person person = new Person(0x18, "Rose");
try {
      FileOutputStream fileOutputStream = new FileOutputStream(new File("person_test"));
      ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
      objectOutputStream.writeObject(person);
      objectOutputStream.flush();
} catch (FileNotFoundException e) {
      e.printStackTrace();
} catch (IOException e) {
      e.printStackTrace();
}

看下person_test文件的内容。

代码语言:javascript复制
ACED0005 7372001C 636F6D2E 616C696F 75737761 6E672E62 696E6465 722E5065 72736F6E FFFFFFFF 87654321 

02000249 00036167 654C0004 6E616D65 7400124C 6A617661 2F6C616E 672F5374 72696E67 3B787000 00001874 

0004526F 7365

//ACED0005 魔数和版本号
//73  表示写入是一个Object对象
//72  new Class Descriptor
//001C 对象字段的总长度
//636F6D2E 616C696F 75737761 6E672E62 696E6465 722E5065 72736F6E "Person类完整类名"
//FFFFFFFF 87654321  这就是我们重写的serialVersionUID值啦!
//后面的一坨就是我们person对象的非transient成员变量,写的过程跟写person对象一模一样,当做对象来做递归写入

到这里我们可以回答上面的问题了,JDK对String的序列化做了优化,所有不用写入serialVersionUID标识,解答了上面的问题,所以不是我们的经验错了,而是了解的还不够全面。下面是JDK序列化的流程图

Serializable的序列化流程.png

通过上面的流程,我们大概能看出,之所以Serializable的性能不高,是因为它需要反射解析要序列化的对象生成ObjectStreamClass对象,但是使用起来确实很方便。
第三个问题:Android中Parcelable的序列化是怎么实现的?

先来看一下,上面的Person类实现Parcelable接口

代码语言:javascript复制
public class Person implements Parcelable {

    public int age;
    public String name;


    protected Person(Parcel in) {
        age = in.readInt();
        name = in.readString();
    }

    /**
     * 返序列化的时候会被调用,注意,Parcel读写字段的顺序必须一致,
     */
    public static final Creator<Person> CREATOR = new Creator<Person>() {
        @Override
        public Person createFromParcel(Parcel in) {
            return new Person(in);
        }

        @Override
        public Person[] newArray(int size) {
            return new Person[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * 序列化的时候被调用,有哪些字段参与序列化由你决定
     * @param dest
     * @param flags
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(age);
        dest.writeString(name);
    }
}

我们重点关注的二个地方,一个是writeToParcel方法,一个是CREATOR对象,很容易联想到,前者在序列化的时候会被调用,方法参数里有一个Parcel对象dest,我们将需要序列化的字段逐个写入dest即可,而CREATOR对象是在反序列化的时候被调用,createFromParcel方法参数有一个Parcel对象in,我们只需要逐个从in中读取需要恢复的字段即可,这里要注意,读写的顺序要保持一致。

所有对Parcelable对象的所有操作都是Parcel这个类来处理的。看一下WriteInt和WriteString的实现。

代码语言:javascript复制
/**
* Write an integer value into the parcel at the current dataPosition(),
* growing dataCapacity() if needed.
*/
public final void writeInt(int val) {
     nativeWriteInt(mNativePtr, val);
}

/**
* Called when writing a string to a parcel. Subclasses wanting to write a string
* must use {@link #writeStringNoHelper(String)} to avoid
* infinity recursive calls.
*/
public void writeString(Parcel p, String s) {
      nativeWriteString(p.mNativePtr, s);
}

最终写入的操作是C 实现的,最终的套路跟Serializable基本是一致的,将数据转为二进制写入,因为Parcel要求严格的按顺序读写,所以这里的数据类型和数据长度是不需要写入的,对比Serializable写入的数据量要少一些,更深入的研究感兴趣的同学可以自行研究,这里就不再去深入了。

我们继续最后一个问题。

4. 有哪些使用场景,实现方式怎么选?

我们日常用到的有二种场景。

  • 数据的持久化保存,这里主要是指保存到文件
  • Android页面间数据的传递
先看第一种情况,将数据保存到文件。

根据我们前面的分析,Serializable用到了大量的反射调用,还需要生成很多辅助对象,执行效率应该会比Parcelable低,到底真是情况是不是如我们所想呢?我们可以测试一下。

为了使结果尽可能的准确一些,我分别使用Serializable和Parcelable写文件100次,每次写1000个对象,运行时间取平均值。运行结果:Serializable平均每次写1000个对象的耗时大约30ms,Parcelable平均每次耗时大约4ms。

Parcelable的速度是有一点优势的,但是Serializable的性能也不是不能接受,毕竟Android实际项目中,一般也不会有这么高的IO并发需求。Serializable使用起来简便,能够自动将父类的可序列化字段一并序列化,所以这里该怎么选,见仁见智,但是使用的时候知道底层原理,会更自如一点,如果场景要求极致的性能可以使用Parcelable,一般的场景使用Serializable即可。

测试的代码地址在这里,很简单,给有需要的同学参考吧。测试代码

再看第二种情况,页面间的传值

Android页面间传值当然要用到Intent了,我们知道启动一个Activity是需要我们的Application跟ActivityManagerService(AMS)进行IPC的,那么Intent里面携带的信息就需要IPC传给AMS,看下Intent的实现

代码语言:javascript复制
public class Intent implements Parcelable, Cloneable {
//...
//保存我们需要传递的数据的Bundle
private Bundle mExtras;
//...

public @NonNull Intent putExtra(String name, Parcelable value) {
     if (mExtras == null) {
        mExtras = new Bundle();
     }
     mExtras.putParcelable(name, value);
     return this;
}

public @NonNull Intent putExtra(String name, Serializable value) {
    if (mExtras == null) {
        mExtras = new Bundle();
    }
     mExtras.putSerializable(name, value);
     return this;
}

//Intent的writeToParcel方法
public void writeToParcel(Parcel out, int flags) {
     //...
    //写入bundle
    out.writeBundle(mExtras);
}

//Intent的CREATOR对象
public static final Parcelable.Creator<Intent> CREATOR
        = new Parcelable.Creator<Intent>() {
   public Intent createFromParcel(Parcel in) {
        return new Intent(in);
   }
   public Intent[] newArray(int size) {
       return new Intent[size];
   }
};

//BaseBundle.class, bundle写入的实现
 void writeToParcelInner(Parcel parcel, int flags) {

      //...
      int startPos = parcel.dataPosition();
      parcel.writeArrayMapInternal(map);
      int endPos = parcel.dataPosition();
      //...
}

//BaseBundle中写入ArrayMap的实现,重点关注writeValue
void writeArrayMapInternal(ArrayMap<String, Object> val) {
     int startPos;
     for (int i=0; i<N; i  ) {
        if (DEBUG_ARRAY_MAP) startPos = dataPosition();
        writeString(val.keyAt(i));
        writeValue(val.valueAt(i));
        if (DEBUG_ARRAY_MAP) Log.d(TAG, "  Write #"   i   " "
                  (dataPosition()-startPos)   " bytes: key=0x"
                  Integer.toHexString(val.keyAt(i) != null ? val.keyAt(i).hashCode() : 0)
                  " "   val.keyAt(i));
     }
 }

public final void writeValue(Object v) {
       //...
       else if (v instanceof Serializable) {
           // Must be last
            writeInt(VAL_SERIALIZABLE);
                writeSerializable((Serializable) v);
       } else {
             throw new RuntimeException("Parcel: unable to marshal value "   v);
       }
}

//writeSerializable的实现
public final void writeSerializable(Serializable s) {
        if (s == null) {
            writeString(null);
            return;
        }
        String name = s.getClass().getName();
        writeString(name);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(s);
            oos.close();

            writeByteArray(baos.toByteArray());
        } catch (IOException ioe) {
            throw new RuntimeException("Parcelable encountered "  
                "IOException writing serializable object (name = "   name  
                ")", ioe);
        }
}

可以看到Intent本身实现了Parcelable接口,虽然我们可以在putExtra中添加实现了Serializable接口的对象,但是通过我们上面的扒源码发现,最终Parcel会将Serializable先序列化为字节数组,然后写入,所以这中间就进行了二次序列化,性能肯定比Parcelable要低很多。所以如果我们的场景是界间传值的话,Parcelable是首选,我们可以自行决定哪些需要字段需要序列化,效率和自由度都很高。

总结一下:
  • 数据本地持久化,推荐Serializable
  • 界面传值 推荐Parcelable
小彩蛋:

通过上面分析,Parcelable我们可以自由决定哪些字段参与序列化,那么Serializable可不可以呢,答案当然是可以,我们都知道可以用transient关键字来忽略一些不需要参与序列化的字段,而且Java还提供了writeObject和readObject二个方法,Serializable在序列化时,如果检测到我们的类重写了writeObject方法,就执行该方法来替代默认的序列化调用。JDK中有很多这样的类,比如ArrayList,HashMap,都是重写 了writeObject方法。

代码语言:javascript复制
//HashMap.java
transient Node<K,V>[] table;

private void writeObject(java.io.ObjectOutputStream s)
     throws IOException {
     int buckets = capacity();
     // Write out the threshold, loadfactor, and any hidden stuff
     s.defaultWriteObject();
     s.writeInt(buckets);
     s.writeInt(size);
     internalWriteEntries(s);
}

// Called only from writeObject, to ensure compatible ordering.
void internalWriteEntries(java.io.ObjectOutputStream s) throws 
  IOException {
     Node<K,V>[] tab;
     if (size > 0 && (tab = table) != null) {
         for (int i = 0; i < tab.length;   i) {
              for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                 s.writeObject(e.key);
                 s.writeObject(e.value);
              }
          }
     }
 }

HashMap将存储数据的Node数组添加了 transient修饰符,然后重写了writeObject方法,用一个双层循环将key和value写入ObjectOutputStream。 再进一步想一下,为什么HashMap要自定义序列化逻辑呢?我想可能的原因是,存储数据的数组table,一般都是不满的(因为HashMap的负载因子默认0.75,超过就会扩容),里面肯定会有很多null,如果是默认的序列化,这些null也会被被序列化,显然这些null是没有必要的做序列化的。 全文完!

0 人点赞