Serializable和Parcelable, 都可以用来做序列化,网上也有很多文章分析它们的优缺点,大部分的结论都是Serializable使用简单但是低效,Parcelable使用麻烦但是高效,em...,也对,但是总感觉缺了点意思,这篇文章带你彻底理解二者,拒绝知识盲区。
先抛出几个问题,带着问题我们一起探索。
- 什么是序列化和反序列化,为什么需要序列化?
- Java中Serializable的序列化是怎么实现的?
- Android中Parcelable的序列化是怎么实现的?
- 有哪些使用场景,实现方式怎么选?
em, 可以先思考一下这几个问题。
(5分钟之后...)
第一个问题:什么是序列化和反序列化?
序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
这里有二个关键字,存储和传输,存储的场景比如对象的持久化,传输的场景比如将对象通过网络传输,然后在需要使用的时候,反序列化,重新创建对象。
第二个问题: Java中Serializable的序列化是怎么实现的?
要弄清楚这个问题,只能去JDK源码里面找答案了(这里基于JDK8)。不过现在,我想通过一个简单的序列化字符串的例子开始,先有个大概的印象。
序列化:(为了简单起见,只贴了关键代码,下面就不再赘述了)
//......
FileOutputStream fileOutputStream = new FileOutputStream(new File("string_file"));
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject("Hello World!");
objectOutputStream.flush();
//......
反序列化:
//......
FileInputStream fileInputStream = new FileInputStream(new File("string_file"));
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
String string = (String) objectInputStream.readObject();
//......
看一下生成的二进制string_file文件的内容。
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的实现
//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类,类定义如下。
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文件的内容。
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的性能不高,是因为它需要反射解析要序列化的对象生成ObjectStreamClass对象,但是使用起来确实很方便。
第三个问题:Android中Parcelable的序列化是怎么实现的?
先来看一下,上面的Person类实现Parcelable接口
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的实现。
/**
* 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的实现
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方法。
//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是没有必要的做序列化的。
全文完!