我正在参加「启航计划」
在Android开发中,咱们有时需求在组件之间传递目标,比方Activity之间、Service之间以及进程之间。
传递目标的方式有三种:
- 将目标转换为Json字符串
- 经过Serializable序列化
- 经过Parcelable序列化
1、什么是序列化
序列化:简单来说,便是将实例的状况转化为能够存储或许传输的方式的进程。
反序列化:反过来再把这种方式还原成实例的进程就叫做反序列化。
这种能够传输或许存储的方式,能够是二进制流,也能够是字符串,能够被存储到文件,也能够经过各种协议被传输。
2、Serializable 的完成原理
Serializable 是 Java 渠道中用于目标序列化和反序列化的接口。,它是一个空接口,没有界说任何办法,它仅仅只起到了标记作用。经过完成它,Java 虚拟机能够辨认该类是能够进行序列化和反序列化操作的。
2.1 Serializable 序列化的运用
将一个目标序列化写入文件:
public class User implements Serializable {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
/***** get set办法省略 *****/
}
File file = new File("write.txt");
//序列化写入文件
FileOutputStream fileOutputStream = new FileOutputStream(file);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(new User("李四", "lisi@qq.com"));
objectOutputStream.flush();
//读取文件反序列化
FileInputStream fileInputStream = new FileInputStream(file);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
User user = (User) objectInputStream.readObject();
序列化写入文件的结果:
2.2 Serializable 的关键办法ObjectOutputStream() 和 writeObject()
那么关于一个仅仅完成了一个空接口的实例,ObjectOutputStream是怎么做到知道这个类中都有哪些特点结构的呢?而且是怎么获取它们的值的呢?
咱们来看一下 ObjectOutputStream的源码完成,在它的构造办法中主要做两件事:
- 创立一个用于写入文件的Stream
- 写入魔数和版本号
public ObjectOutputStream(OutputStream out) throws IOException {
verifySubclass();
bout = new BlockDataOutputStream(out);//创立Stream
...
writeStreamHeader();//写入魔数和版本号
...
}
再来看 writeObject() 办法,writeObject的核心是调用 writeObject0()办法,在writeObject0中经过 ObjectStreamClass desc = ObjectStreamClass.lookup(cl, true)
创立出一个原始实例的描绘信息的实例,即desc。desc这个描绘信息中就包括原始类的特点结构和特点的值。接着再依据实例的类型调用对应的办法将实例的类名和特点信息写入到输出流;字符串、数组、枚举和一般的实例写入的逻辑也是不同的。
2.3 功能剖析
很明显在序列化的进程中,写输出流的进程必定不存在输出瓶颈,复杂操作集中在怎么去解析原始目标的结构,怎么读取它的特点。所以要把要点放在ObjectStreamClass这个类是怎么的被创立出来的。
咱们剖析lookup办法,发现创立进程会先去读取缓存。假如发现现已解析而且加载过相同的类,那么就直接返回。在没有缓存的状况下,才会依据class去创立新的ObjectStreamClass实例。
static ObjectStreamClass lookup(Class<?> cl, boolean all) {
...
Reference<?> ref = Caches.localDescs.get(key);//读取缓存
if (ref != null) {
entry = ref.get();
}
if (entry instanceof ObjectStreamClass) {
return (ObjectStreamClass) entry;
}
...
if (entry == null) {
entry = new ObjectStreamClass(cl);//没有缓存
}
if (entry instanceof ObjectStreamClass) {
return (ObjectStreamClass) entry;
}
}
在创立进程中,类名name是经过class实例调用反射API来获取的。再经过getDeclaredSUID 办法提取到serialVersionUID 字段信息。假如没有装备,getSerialVersionUID 办法会经过 computeDefaultSUID 生成一个默许的序列号。
接下来就会去获取特点以及计算特点值的偏移量。
private ObjectStreamClass(final Class<?> cl) {
name = cl.getName();//类名
suid = getDeclaredSUID(cl);//提取 serialVersionUID 字段信息
fields = getSerialFields(cl);//获取特点 即经过反射获取该类一切需求被序列化的Field
computeFieldOffsets();//计算特点值的偏移量
}
咱们再来看一下读取特点信息的代码 getSerialFields(),首要体系会判断咱们是否自行完成了字段序列化 serialPersistentFields 特点,否则走默许序列化流程,既忽律 static、transient 字段。
private static ObjectStreamField[] getSerialFields(Class<?> cl)
throws InvalidClassException
{
ObjectStreamField[] fields;
if (Serializable.class.isAssignableFrom(cl) &&
!Externalizable.class.isAssignableFrom(cl) &&
!Proxy.isProxyClass(cl) &&
!cl.isInterface())
{
if ((fields = getDeclaredSerialFields(cl)) == null) {
fields = getDefaultSerialFields(cl);//默许序列化字段规则
}
Arrays.sort(fields);
} else {
fields = NO_FIELDS;
}
return fields;
}
然后在getDefaultSerialFields 中运用了许多的反射API,最终把特点信息构建成了ObjectStreamField的实例。
private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
Field[] clFields = cl.getDeclaredFields();//获取当前类的一切字段
ArrayList<ObjectStreamField> list = new ArrayList<>();
int mask = Modifier.STATIC | Modifier.TRANSIENT;
for (int i = 0; i < clFields.length; i++) {
if ((clFields[i].getModifiers() & mask) == 0) {
//将其封装在ObjectStreamField中
list.add(new ObjectStreamField(clFields[i], false, true));
}
}
int size = list.size();
return (size == 0) ? NO_FIELDS :
list.toArray(new ObjectStreamField[size]);
}
到这里咱们会发现Serializable 整个计算进程非常复杂,而且由于存在许多反射和 GC 的影响,序列化的功能会比较差。别的一方面由于序列化文件需求包含的信息非常多,导致它的巨细比 Class 文件本身还要大许多,这样又会导致 I/O 读写上的功能问题。
总结,完成Serializable接口后 ,Java运行时将运用反射来确认怎么编组和解组目标。所以咱们能够认定这些反射操作便是影响 Serializable 功能的一个重要的因素,同时会创立许多临时目标并导致相当多的垃圾收集。可是由于这些反射,所以Serializable的运用非常简单。
3、Parcelable的完成原理
在Android中供给了一套机制,能够将序列化之后的数据写入到一个共享内存。其他进程就能够经过Parcel来读取这块共享内存中的字节流,而且反序列化成实例。Parcelable相关于Serializable的运用相对复杂一些。
3.1 Parcelable 序列化的运用
public class User implements Parcelable {
private String name;
private String email;
//反序列化
protected User(Parcel in) {
name = in.readString();
email = in.readString();
}
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
@Override
public int describeContents() {
return 0;
}
// 用于序列化
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeString(email);
}
}
User user = new User();
Bundle params = new Bundle();
params.putParcelable("user", user);
Bundle arguments = getArguments();
personBean = arguments.getParcelable("user");
完成 Parcelable 接口,需求完成writeToParcel的办法以供给序列化时,将数据写入Parcel的代码。除此之外还要供给一个Creator以及一个参数是Parcel类型的构造办法,用来反序列化。
序列化和反序列化每个字段的代码统一由运用者自己来完成。这样一来在序列化和反序列化的进程中,就不用再去关怀实例的特点结构和访问权限。这些都由开发者自己来完成。所以能够防止大面积的运用反射的状况,算是牺牲了必定的易用性来提升运行时的功率。当然了这个易用性咱们也能够经过parcelize的方式来弥补。此外,Parcelable还有一个长处,便是它能够手动控制序列化和反序列化的进程。这意味着咱们能够挑选只序列化目标的部分字段,或许在反序列化的时分对字段进行一些额外的处理。这种灵活性使得Parcelable在某些特定的场景下更加有用。
尽管Parcelable的设计初衷并不是像Serializable那样,根据输入流和输出流的操作,而是根据共享内存的概念。但Parcelable是支撑让咱们获取到序列化之后的data数组的。这样一来,咱们就能够同样把序列化后的信息写入到文件中。
//序列化写入byte[]
Parcel parcel = Parcel.obtain();
parcel.setDataPosition(0);
new User().writeToParcel(parcel, 0);
byte[] bytes = parcel.marshall();
parcel.recycle();
//从byte数组反序列化
Parcel parcel = Parcel.obtain();
parcel.unmarshall(bytes, 0, bytes.length);
parcel.setDataPosition(0);
new User(parcel);
3.2 Intent、Bundle中传值对比
public class Intent implements Parcelable, Cloneable { }
public final class Bundle extends BaseBundle implements Cloneable, Parcelable { }
在安卓渠道最经常用到序列化的状况,是经过Intent传值。咱们能够看到,无论是Intent仍是Bundle,其实都是Parcelable的完成类。
那么当Intent或许Bundle被序列化的时分,它们内部的Serializable是怎么被处理的呢?
经过代码能够看到,在Parcel的writeSerializable办法中,仍是会先把Serializable转化成Byte数组。然后再经过writeByteArray去写入到Parcel中。
public final void writeSerializable(@Nullable 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();
writeBytea
所以在Intent传值的场景下,Parcelable也是存在速度优势的。由于Parcelable便是正常的根据writeToParcel的办法中的逻辑去进行序列化的。而Serializable要先经过ObjectOutputStream把目标转换成ByteArray,再经过writeByteArray写入Parcel。
- Parcelable
调用目标内部完成的writeToParcel 办法,经过一些write办法直接写入Parcel。
- Serializable
经过ObjectOutputStream把目标转换成ByteArray,再经过writeByteArray写入Parcel。
可是有些状况下不必定Parcelable更快。
之前在看Serializable源码的时分,咱们发现ObjectStreamClass是存在缓存机制的。所以在一次序列化的进程中,假如涉及到许多相同类型的不同实例序列化,比方一个实例反复嵌套自己的类型,或许是在序列化大数组的状况下。Serializable的功能仍是存在优势的。
4、Parcelable为什么速度优于Serializable?
- Parcelable
- 目标自行完成出入口办法,防止运用反射的状况。
- 二进制流存储在连续内存中,占用空间更小。
- 牺牲易用性(kotlin的Parcelize 能够弥补),换取功能。
- Serializable
- 用反射获取类的结构和特点信息,进程中会发生中心信息。
- 有缓存结构,在解析相同类型的状况下,能复用缓存。
- 功能在可接受的范围内,易用性较好。
不能抛开使用场景谈技术方案,在大多数场景下Parcelable确实存在功能优势,而Serializable的功能缺陷主要来自反射构建ObjectStreamClass类型的描绘信息。在构建ObjectStreamClass类型的描绘信息的进程中,是有缓存机制的。所以能够许多复用缓存的场景下,Serializable反而会存在功能优势。 Parcelable原本在易用性上是存在短板的,可是kotlin的Parcelize 很好的弥补了这个缺点。
参阅:
Java序列化与反序列化(三)源码剖析
Android 目标序列化之你不知道的 Serializable