安卓 Android Java 序列化
Serializable 是 Java 原生的序列化机制,在 Android 中也有被广泛使用。我们可以通过 Serializable 将对象持久化存储,也可以通过 Bundle 传递 Serializable 的序列化数据。
Serializable 原理
Serializable 的原理是通过 ObjectInputStream 和 ObjectOutputStream 来实现的,我们以 Android 6.0 的源码为例,你可以看到:
private void writeFieldValues(Object obj, ObjectStreamClass classDesc) {
for (ObjectStreamField fieldDesc : classDesc.fields()) {
...
Field field = classDesc.checkAndGetReflectionField(fieldDesc);
...
整个序列化过程使用了大量的反射和临时变量,而且在序列化对象的时候,不仅会序列化当前对象本身,还需要递归序列化对象引用的其他对象。
整个过程计算非常复杂,而且因为存在大量反射和 GC 的影响,序列化的性能会比较差。另外一方面因为序列化文件需要包含的信息非常多,导致它的大小比 Class 文件本身还要大很多,这样又会导致 I/O 读写上的性能问题。
Serializable 进阶
- writeObject 和 readObject 方法。Serializable 序列化支持替代默认流程,它会先反射判断是否存在我们自己实现的序列化方法 writeObject 或反序列化方法 readObject。 通过这两个方法,我们可以对某些字段做一些特殊修改,也可以实现序列化的加密功能。
- writeReplace 和 readResolve 方法。这两个方法代理序列化的对象,可以实现自定义返回的序列化实例。那它有什么用呢?我们可以通过它们实现对象序列化的版本兼容,例如通过 readResolve 方法可以把老版本的序列化对象转换成新版本的对象类型。
Serializable 的序列化与反序列化的调用流程如下。
// 序列化
E/test:SerializableTestData writeReplace
E/test:SerializableTestData writeObject
// 反序列化
E/test:SerializableTestData readObject
E/test:SerializableTestData readResolve
Serializable 注意事项
- 不被序列化的字段。类的 static 变量以及被声明为 transient 的字段,默认的序列化机制都会忽略该字段,不会进行序列化存储。 当然我们也可以使用进阶的 writeReplace 和 readResolve 方法做自定义的序列化存储。
- serialVersionUID。在类实现了 Serializable 接口后,我们需要添加一个 Serial Version ID,它相当于类的版本号。这个 ID 我们可以显式声明也可以让编译器自己计算。 显示声明更加稳妥 因为隐式声明假如类发生了一点点变化,进行反序列化都会由于 serialVersionUID 改变而导致 InvalidClassException 异常。
- 构造方法。Serializable 的反序列默认是不会执行构造函数的,它是根据数据流中对 Object 的描述信息创建对象的。如果一些逻辑依赖构造函数,就可能会出现问题,例如一个静态变量只在构造函数中赋值,当然我们也可以通过进阶方法做自定义的反序列化修改。
Parcelable
你可以发现 Parcel 序列化和 Java 的 Serializable 序列化差别还是比较大的,Parcelable 只会在内存中进行序列化操作,并不会将数据存储到磁盘里。
也可以通过 Parcel.java
的 marshall 接口获取 byte 数组,然后存在文件中从而实现 Parcelable 的永久存储。
// Returns the raw bytes of the parcel.
public final byte[] marshall() {
return nativeMarshall(mNativePtr);
}
// Set the bytes in data to be the raw bytes of this Parcel.
public final void unmarshall(byte[] data, int offset, int length) {
nativeUnmarshall(mNativePtr, data, offset, length);
}
Parcelable 注意事项
虽然通过取巧的方法可以实现 Parcelable 的永久存储,但是它也存在两个问题。
- 系统版本的兼容性。由于 Parcelable 设计本意是在内存中使用的,我们无法保证所有 Android 版本的
Parcel.java
实现都完全一致。如果不同系统版本实现有所差异,或者有厂商修改了实现,可能会存在问题。 - 数据前后兼容性。Parcelable 并没有版本管理的设计,如果我们类的版本出现升级,写入的顺序及字段类型的兼容都需要格外注意,这也带来了很大的维护成本。
数据序列化
JSON
Protocol Buffers
相比对象序列化方案,JSON 的确速度更快、体积更小。不过为了保证 JSON 的中间结果是可读的,它并没有做二进制的压缩,也因此 JSON 的性能还没有达到极致。
- 性能。使用了二进制编码压缩,相比 JSON 体积更小,编解码速度也更快,感兴趣的同学可以参考
- 兼容性。跨语言和前后兼容性都不错,也支持基本类型的自动转换,但是不支持继承与引用类型。
- 使用成本。Protocol Buffers 的开发成本很高,需要定义.proto 文件,并用工具生成对应的辅助类。辅助类特有一些序列化的辅助方法,所有要序列化的对象,都需要先转化为辅助类的对象,这让序列化代码跟业务代码大量耦合,是侵入性较强的一种方式。
Google 后面还推出了压缩率更高的 FlatBuffers,对于它的使用你可以参考 《FlatBuffers 体验》
相比 JSON、Protocol Buffers 的最佳选择
Serial
从实现原理上看,Serial 就像是把 Parcelable 和 Serializable 的优点集合在一起的方案。
- 由于没有使用反射,相比起传统的反射序列化方案更加高效
- 开发者对于序列化过程的控制较强,可定义哪些 Object、Field 需要被序列化。
- 有很强的 debug 能力,可以调试序列化的过程。
- 有很强的版本管理能力,可以通过版本号和 OptionalFieldException 做兼容。
Rom 监控
ROM 监控的两个核心指标是文件总大小与总文件数,例如我们可以将文件总大小超过 400MB 的用户比例定义为 空间异常率 ,将文件数超过 1000 个的用户比例定义为 数量异常率
文件遍历的耗时跟文件夹下的文件数量有关。曾经我们也出现过一次 bug 导致某个文件夹下面有几万个文件,在遍历这个文件夹时,用户手机直接重启了。需要注意的是文件遍历在 API level 26 之后建议使用 FileVisitor 提到 ListFiles,整体的性能会好很多。
参考
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 安卓 编译插桩:AspectJ、ASM、ReDex
下一篇: MyBatis 介绍和使用
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论