

Android - 从浅到懂理解 Serializeable 和 Parcelable 实现的序列化和反序列化
source link: https://my.oschina.net/u/4483532/blog/4836672
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在开发插件化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 = 1L;
private String name = "";
// transient 表示不序列化该属性
private transient int age = 0;
// Child 对象也必须要实现 Serializable 否则得加 transient 禁止该属性序列化
private Child child = null;
// 省略 get set 等方法
}
import java.io.Serializable;
public class Child implements Serializable {
private static final long serialVersionUID = 12L;
private int age = 0;
// 省略 get set 等方法
}
// 写入文件
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());
// 输出
// user :User{name='张三', age=0, child=Child{age=2}}
// 解释
// 由于我们在 User 对象的 age 属性设置了 transient 关键字,所以 age 不会被序列化。
// 也就是写入的 age=20 保存不了,所以读取出来的数据是默认的数字 0
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 {
//获取student类的字节码对象
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
提供的序列化接口。
这里举个例子:两个页面之间传递对象
- 先定义对象并实现
Parcelable
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;
}
/**
* 序列化
* @param dest
* @param flags
*/
@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)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK