注册

Parcelable为什么速度优于 Serializable ?

在Android开发中,我们有时需要在组件之间传递对象,比如Activity之间、Service之间以及进程之间。
传递对象的方式有三种:

  • 将对象转换为Json字符串
  • 通过Serializable序列化
  • 通过Parcelable序列化 

1、什么是序列化

微信截图_20230619105720.png

  序列化:简单来说,就是将实例的状态转化为可以存储或者传输的形式的过程
  反序列化:反过来再把这种形式还原成实例的过程就叫做反序列化

  这种可以传输或者存储的形式,可以是二进制流,也可以是字符串,可以被存储到文件,也可以通过各种协议被传输。

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();

序列化写入文件的结果:
image.png

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这个描述信息中就包括原始类的属性结构和属性的值。接着再根据实例的类型调用对应的方法将实例的类名和属性信息写入到输出流;字符串、数组、枚举和一般的实例写入的逻辑也是不同的。


image.png

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的使用相对复杂一些。
微信截图_20230629112841.png

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
    1. 对象自行实现出入口方法,避免使用反射的情况。
    2. 二进制流存储在连续内存中,占用空间更小。
    3. 牺牲易用性(kotlin的Parcelize 可以弥补),换取性能。
  • Serializable
    1. 用反射获取类的结构和属性信息,过程中会产生中间信息。
    2. 有缓存结构,在解析相同类型的情况下,能复用缓存。
    3. 性能在可接受的范围内,易用性较好。

  不能抛开应用场景谈技术方案,在大多数场景下Parcelable确实存在性能优势,而Serializable的性能缺陷主要来自反射构建ObjectStreamClass类型的描述信息。在构建ObjectStreamClass类型的描述信息的过程中,是有缓存机制的。所以能够大量复用缓存的场景下,Serializable反而会存在性能优势。 Parcelable原本在易用性上是存在短板的,但是kotlin的Parcelize 很好的弥补了这个缺点。


作者:大神仙
链接:https://juejin.cn/post/7250012486992101433
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册