背景
在开发插件化App时用到了AIDL实现进程间通信。而AIDL要想传递对象类型的数据就需要将对象序列化。
在 Android 开发中,我们经常需要对对象进行序列化与反序列化操作。 最常见的就是通过 Intent 传输数据时,Intent 只能传输基本数据类型、String 类型和可序列化与反序列化的对象类型, 要想通过 Intent 传递对象类型,我们需要让该对象类型支持序列化和反序列化。
我们知道,Android 给我们提供了两种方式来完成序列化与反序列化过程:
一种是 Serializable 方式
另一种是 Parcelable 方式;
本篇文章将尽可能详细讲述两种方式实现序列化。
我们首先来了解序列化和反序列化。
序列化和反序列化是什么?
除了基本数据类型外的其它类型,如对象、文件、图片都有自己的数据格式,很难统一传输和保存。 基本数据类型提供了转为byte[]的方法,所以数据可以统一成字节流。
为了实现对象、文件、图片数据也能在计算机中传输和保存,因此可以将这些数据格式也转为字节流。
在特定语言语言范畴内将一个实例对象编码成字节流 ,称为序列化 ;将一个字节流中读出一个对象实例 ,称为反序列化 。
PS:基本数据类型
四种整数类型byte、short、int、long
两种浮点数类型float、double
一种字符类型char
一种布尔类型boolean
上面提到的字节和字符,我简要描述一下。
字节流 和 字符流
字节流是由字节组成的流, 与之相关的还有字符流,它是由字符组成的流。
这里的流可以看作是水流,水就是数据,而流是指流入和流出。
那么字节和字符是什么呢?
字节 和 字符
字节(byte):计算机的计量单位,表示数据量的多少。通常情况下 1byte = 8bit
字符(Character):计算机中用于表示字母、数字、字和符号。
一般(ASCII编码)在英文状态下1个字母或字符占用1个字节,1个汉字用2个字节表示。
之所以有编码表是因为计算机在全球各个国家和地区使用,为了支持不同国家的文字在计算机上能统一使用, 因此提供编码表的方式,统一将文字转为字节。
常见的编码表中字符和字节的对应关系如下:
ASCII 码中,1个英文字母(不分大小写)为1个字节,1个中文汉字为2个字节。
GBK 码中,1个英文字母(不分大小写)为1个字节,1个中文汉字为2个字节。
UTF-8 编码中,1个英文字为1个字节,1个中文为3个字节。
Unicode 编码中,1个英文为1个字节,1个中文为2个字节。
符号:英文标点为1个字节,中文标点为2个字节。例如:英文句号.占1个字节的大小,中文句号。占2个字节的大小。
那么我们可以明确序列化的目的就算为了在计算机中传递统一格式的数据。 而计算机传输本质又都是字节,所以本质上就是将数据转为byte[]数据格式,然后在计算机中传递。
Android 中常见的序列化场景
文件的读写是通过字节流的方式,而序列化对象后就可以将对象保存文件中。
网络数据传输也是通过字节流的方式,所以可将对象序列化后用于在网络间传递。
跨进程通信,如使用 AIDL 传递对象时需要进行序列化。
在Intent之间只能传递基本的数据类型, 如需要传递复杂对象,就需要用到序列化。
Android 实现序列化的两种方式
Serializeable:Java提供的序列化方式
Parcelable:Android提供的序列化方式
Serializable
Serializable 是 Java 提供的序列化接口。 要将对象序列化,只需让对象实现java.io.Serializable接口即可。 实现Serializable的类对象的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则使用transient 关键字修饰。
例如:将对象保存到文本文件并读取出来。
写入文件我们会通过ObjectOutputStream的writeObject(Object obj)方法去实现,读取文件我们会通过ObjectInputStream的readObject()去实现。
ObjectOutputStream:是对象输出流,作用是将对象转成字节数据输出到文件中保存.
ObjectInputStream:是对象输入流,作用是从文件中将字节数据转为对象
看个使用示例:
import java. io. Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1 L;
private String name = "" ;
private transient int age = 0 ;
private Child child = null;
}
import java. io. Serializable;
public class Child implements Serializable {
private static final long serialVersionUID = 12 L;
private int age = 0 ;
}
private static void writeUser ( ) {
User user= new User ( "张三" , 20 , new Child ( 2 ) ) ;
try {
ObjectOutputStream out = new ObjectOutputStream ( new FileOutputStream ( "D:\\user.txt" ) ) ;
out. writeObject ( user) ;
out. close ( ) ;
} catch ( IOException e) {
e. printStackTrace ( ) ;
}
}
private static void readUser ( ) {
try {
ObjectInputStream in = new ObjectInputStream ( new FileInputStream ( "D:\\user.txt" ) ) ;
User user= ( User) in. readObject ( ) ;
System. out. println ( "user :" + user. toString ( ) ) ;
in. close ( ) ;
} catch ( Exception e) {
e. printStackTrace ( ) ;
}
}
如果对象没实现Serializable接口就直接调用ObjectOutputStream、ObjectInputStream会报错java.io.NotSerializableException。 而且User对象中的Child对象如果没实现Serializable接口也会报该错,进一步说明了对象中所有要序列化的对象都要实现Serializable接口。 我们在User对象的age属性设置了transient,在写入User数据age=20之后但是读取数据时age=0也说明了transient可以忽略属性的序列化。
关于serialVersionUID最好是用private显式声明,而且必须是static final long类型,不然在反序列化时可能会报错。
这个serialVersionUID是用来辅助序列化和反序列化的。 如不显式声明,编译器会自动去计算出一个值并赋予它。
那么Serialable是如何实现序列化的呢?
Serializable 实现原理
我们在写入文件时调用了writeObject()方法,那么我们就从该方法入手。
参考小缘大佬的源码解读 :地址-https://www.wanandroid.com/wenda/show/9002
借助ObjectStreamClass记录目标对象的类型,类名等信息,这个类里面还有个ObjectStreamField数组,用来记录目标对象的内部变量;
在defaultWriteFields方法中,会先通过ObjectStreamClass的getPrimFieldValues方法,把基本数据类型的值都复制到一个叫primVals的byte数组上;
接着通过getPrimFieldValues方法来获取所有成员变量的值,出乎意料的是:这两个获取值的方法,里面都不是我们常规的反射操作(Field.get),而是通过操作Unsafe类来完成的;
遍历剩下不是基本数据类型的成员变量,然后递归调用writeObject方法(也就是一层层地剥开目标对象,直到找到基本数据类型为止)
UnSafe类的相关的内容可自行搜索,这里就简短描述了,毕竟没用过。
Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,使用Unsafe可用来直接访问系统内存资源并进行自主管理。Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。Unsafe可认为是Java中留下的后门,提供了一些低层次操作,如直接内存访问、线程调度等。但是官方并不建议使用Unsafe。 来源:https://www.jb51.net/article/140726.htm
小结: Serializable实现原理本质是利用UnSafe类去获取对象的数据。为什么不是用反射呢? 其实反射获取数据最后也是通过UnSafe类去获取。
例如:我们通过反射获取上面User中的age属性,getInt的实现如下:
public void getUserAgeByReflection ( ) throws Exception {
Class clazz = Class. forName ( "model.User" ) ;
Object user = clazz. newInstance ( ) ;
Field ageField = clazz. getDeclaredField ( "age" ) ;
ageField. getInt ( user) ;
}
ageField.getInt(user); 的实现在 UnsafeCharacterFieldAccessorImpl 类,我们可以看出反射底层还是调用的 UnSafe 类
public int getInt ( Object var1) throws IllegalArgumentException {
this . ensureObj ( var1) ;
return unsafe. getInt ( var1, this . fieldOffset) ;
}
具体信息可查看源码。
由于Serializable是通过I/O读写存储在磁盘上的数据, 并且使用了UnSafe类去获取数据。 我们知道反射会有性能问题,而反射底层实现就是UnSafe类,所以使用Serializable序列化会有性能问题。 Android的卡顿问题绝对是用户最头疼的问题,因此在Android上通过Parcelable来优化序列化导致的性能问题。
Parcelable
Parcelable 是 Android 提供的序列化接口。
这里举个例子:两个页面之间传递对象
class UserParcelable implements Parcelable {
private String name = "" ;
private int age = 0 ;
public UserParcelable ( String name, int age) {
this . name = name;
this . age = age;
}
protected UserParcelable ( Parcel in) {
name = in. readString ( ) ;
age = in. readInt ( ) ;
}
public static final Creator< UserParcelable> CREATOR = new Creator < UserParcelable> ( ) {
@Override
public UserParcelable createFromParcel ( Parcel in) {
return new UserParcelable ( in) ;
}
@Override
public UserParcelable[ ] newArray ( int size) {
return new UserParcelable [ size] ;
}
} ;
@Override
public int describeContents ( ) {
return 0 ;
}
@Override
public void writeToParcel ( Parcel dest, int flags) {
dest. writeString ( name) ;
dest. writeInt ( age) ;
}
@Override
public String toString ( ) {
return "UserParcelable{" +
"name='" + name + '\'' +
", age=" + age +
'}' ;
}
}
页面FirstAc通过Intent传递给SecondAc
Intent intent = new Intent ( FirstAc. this , SecondAc. class ) ;
intent. putExtra ( "UserParcelable" , new UserParcelable ( "李四" , 18 ) ) ;
startActivity ( intent) ;
Intent intent = getIntent ( ) ;
UserParcelable userParcelable = intent. getParcelableExtra ( "UserParcelable" ) ;
Log. d ( "SecondAc" , userParcelable. toString ( ) ) ;
Parcelable 实现原理
还是参考小缘大佬的源码解读 :地址-https://www.wanandroid.com/wenda/show/9002
它的各种writeXXX方法,在native层都是会调用Parcel.cpp的write方法,它是通过memcpy函数直接复制内存地址由一个叫mData的uint8_t来保存。
read方法同理,它也是通过 memcpy函数来把mData上的某部分数据复制出来。
二者的区别
区别
Serializable
Parcelable
所属API
JAVA API
Android SDK API
原理
序列化和反序列化过程需要在磁盘上进行大量的I/O操作,且通过反射实现有性能问题
序列化和反序列化过程直接在native端操作内存
开销
开销大
开销小
效率
低
很高
使用场景
序列化到本地或者通过网络传输
本地内存序列化
总结
序列化的作用统一数据格式便于数据传输和保存。
由于计算机中数据是以字节流传递,因此大部分的编程语言实现序列化的做法都是转为byte[] 而全球众多文字转为字节是通过特定的编码方式实现,例如GBK编码中,一个英文字符表示一个字节,一个中文字符表示两个字节。
Android中有两种方式实现序列化,一个是实现Serializable,另一个是实现Parcelable。 Serializable是Java提供的序列化方案, Parcelable是Android为了解决Serializable序列化导致的性能问题而提供的方案。
Serializable实现简单,但是会有性能问题,原因是它数据的读写是通过I/O在磁盘上操作,而且获取数据使用的是和反射底层用的是一个类:UnSafe类,该类可以直接操作内存,官方建议不熟悉该类最好别使用。 Parcelable实现略微复杂,通过writeXXX,readXXX方法实现数据的读写,最终通过native的方法去内存中读取数据。
Serializable序列化后的数据可以进行网络传输本地存储 Parcelable是基于Android的,只能在内存间传输。
参考
本文同步分享在 博客“_龙衣”(CSDN)。 如有侵权,请联系 support@oschina.cn 删除。 本文参与“OSC源创计划 ”,欢迎正在阅读的你也加入,一起分享。