Android序列化
是什么?为啥用?怎么用?——灵魂三连问
1、序列化和反序列化是什么?
- 序列化:把对象转变为字节序列的过程称为对象的序列化。
- 反序列化:把字节序列恢复为对象的过程称为对象的反序列化。
2、对象序列化的用途
- 永久的保存对象数据(将对象数据保存在文件或者磁盘中)
- 是对象数据能够在网络上传输(由于网络传输是以字节流的方式来完成对数据的传输的,因此序列化的目的是将对象数据转换成字节流的形式)。
- 使对象能够在进程间进行传递(基础类型数据除外,对象类型数据必须进行序列化操作后才能进行传输)。
- 在Android intent之间,基础数据类型可以直接传递,但是传递复杂数据类型的时候,必须进行序列化。
序列化对象的时候只针对属性进行序列化,不针对方法序列化。
3、Android实现序列化的两种方式
3.1、实现Serializable接口
Serializable是java提供的一个序列化接口,它是一个空接口,专门为对象提供标准的序列化和反序列化操作,使用Serializable实现类的序列化比较简单,只要在类声明中实现Serializable接口即可,同时强烈建议声明序列化标识。
3.1.1 序列化举例
public class S_Shop implements Serializable {
private static final long serialVersionUID = -1399695071515887643L;
public String mShopName;
public int mShopId;
public String mShopPhone;
public static int STATIC_VALUE = 100;//静态值
public transient int TRANSIENT_VALUE;//被transient修饰 不能序列化
@NonNull
@Override
public String toString() {
return "Serializable: mShopName is " + mShopName
+ ",mShopId is " + mShopId
+ ",mShopPhone is " + mShopPhone
+ ",STATIC_VALUE is " + STATIC_VALUE
+ ",TRANSIENT_VALUE is " + TRANSIENT_VALUE;
}
}
执行序列化和反序列化过程:
public static void main(String[] args) throws IOException {
//------------------Serializable------------------
S_Shop shop = new S_Shop();
shop.mShopName = "商品名";
shop.mShopId = 2022;
shop.mShopPhone = "15700000000";
shop.TRANSIENT_VALUE = 1000;
saveObject(shop); //序列化
readObject();//反序列化
}
//序列化
private static void saveObject(S_Shop shop) {
ObjectOutputStream outputStream = null;
try {
outputStream = new ObjectOutputStream(new FileOutputStream("shop.obj"));
outputStream.writeObject(shop);
System.out.println("write-hashCode: " + shop.hashCode());
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private static void readObject() {
//反序列化
ObjectInputStream inputStream = null;
try {
inputStream = new ObjectInputStream(new FileInputStream("shop.obj"));
S_Shop shop = (S_Shop) inputStream.readObject();
System.out.println(shop.toString());
System.out.println("read-hashCode: " + shop.hashCode());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
执行结果:
Serializable: mShopName is 商品名,mShopId is 2022,mShopPhone is 15700000000,STATIC_VALUE is 100,TRANSIENT_VALUE is 0
结果看到反序列化成功,从序列化结构中又重新生成了对象,这里注意一点,类中的变量TRANSIENT_VALUE是由transient修饰的,不能被序列化,所以反序列化时得到的是默认值。另外STATIC_VALUE由static修饰,也不参与序列化过程
3.1.2 特殊变量序列化
- 静态变量的序列化
序列化并不保存静态变量,序列化保存的是对象的状态,而静态变量是类的状态。 - Transient关键字
transient
关键字的作用就是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量序列化到文件中,在反序列化后,transient
变量会被设为初始值,如int型的为0,对象型的为null。 - 父类的序列化特性
如果子类实现了Serializable
接口而父类没有实现,那么父类不会被序列化,但是**父类必须有默认的无参构造方法,否则会抛出InvalidClassException
异常。如下图所示
序列化异常
解决方案:想要将父类对象也序列化,就需要让父类也实现Serializable接口;如果父类不实现的话,就需要有默认的无参构造函数,并且父类的变量值都是默认声明的值。
在父类没有实现Serializable接口时,虚拟机不会序列化父对象,而一个Java对象的初始化必须先初始化父对象,再初始化子对象,反序列化也不例外。所以在反序列化时,为了构造父对象,只能调用父类对象的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值
3.1.3 序列化举例序列化步骤:
- 将对象实例相关的类元数据输出
- 递归地输出类的超类描述直到不再有超类
- 类元数据完了之后,开始从最顶层的超类开始输出对象实例的实际数据值
- 从上至下递归输出实例的数据
3.2 、实现Parcelable接口
Parcelable是Android SDK API,其序列化操作完全由底层实现,可以在进程内、进程间(AIDL)高效传输数据。不同版本的API实现方式可能不同,不宜做本地持久化存储。
3.2.1 序列化举例
public class P_Shop implements Parcelable {
public P_Shop(){}
public String mShopName;
public int mShopId;
public String mShopPhone;
public static int STATIC_VALUE = 100;//静态值
public transient int TRANSIENT_VALUE;//被transient修饰 不能序列化
/**
* 从序列化结构中创建原始对象
*/
protected P_Shop(Parcel in) {
mShopName = in.readString();
mShopId = in.readInt();
mShopPhone = in.readString();
}
/**
* 反序列化
*/
public static final Creator<P_Shop> CREATOR = new Creator<P_Shop>() {
/**
* 从序列化对象中创建原始对象
*/
@Override
public P_Shop createFromParcel(Parcel in) {
return new P_Shop(in);
}
/**
* 创建指定长度的原始对象数组
*/
@Override
public P_Shop[] newArray(int size) {
return new P_Shop[size];
}
};
/**
* 序列化:将当前对象写入序列化结构中
*/
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mShopName);
dest.writeInt(mShopId);
dest.writeString(mShopPhone);
}
/**
* 当前对象的内容描述,存在文件描述符时返回1 其余全返回0
*/
@Override
public int describeContents() {
return 0;
}
@NonNull
@Override
public String toString() {
return "Parcelable: mShopName is " + mShopName
+ ",mShopId is " + mShopId
+ ",mShopPhone is " + mShopPhone
+ ",STATIC_VALUE is " + STATIC_VALUE
+ ",TRANSIENT_VALUE is " + TRANSIENT_VALUE;
}
}
执行序列化/反序列化:
//------------------Parcelable------------------
Context context = this;
P_Shop shopP = new P_Shop();
shopP.mShopName = "商品名";
shopP.mShopId = 2022;
shopP.mShopPhone = "15700000000";
shopP.TRANSIENT_VALUE = 1000;
//序列化过程
byte[] bytes = PUtil.marshall(shopP);//Parcel->bytes[]
PUtil.save(context, bytes);//保存bytes[]
//反序列化过程
Object object = PUtil.getParcel(context);//bytes[]->Parcel->Object
if (object == null) return;
if (object instanceof P_Shop) {
P_Shop shop = (P_Shop) object;
Log.e("TTT", shop.toString());
}
执行结果:
Parcelable: mShopName is 商品名,mShopId is 2022,mShopPhone is 15700000000,STATIC_VALUE is 100,TRANSIENT_VALUE is 0
3.2.2 实现原理
Parcelable 序列化过程中会用到Parcel,Parcel可以被认为是一个包含数据或者对象引用的容器,能够支持序列化及在跨进程之后的反序列化。Parcelable 的序列化操作在Native层实现,通过write内存写入及read读内存数据重新生成对象。Parcelable 将对象进行分解,且分解后每一部分都是支持可传递的数据类型。
4、对象序列化的用途Parcelable、Serializable比较
Serializable序列化和反序列化会经过大量的I/O操作,产生大量的临时变量引起GC;Parcelable是基于内存实现的封装和解封(marshalled& unmarshalled),效率比Serializable快很多
下面的测试来自非官方测试,通过Parcelable和Serializable分别执行序列化/反序列化过程,循环1000次取平均值,实验结果如下:
image.png
数据来自 parcelable-vs-serializable,实验结果对比Parcelable的效率比Serializable快10倍以上。
5、总结
对比 | Serializable | Parcelable |
---|---|---|
所属API | Java API | Android SDK API |
特点 | 序列化和反序列化会经过大量的I/O操作,产生大量的临时变量引起GC,且反序列化时需要反射 | 基于内存拷贝实现的封装和解封(marshalled& unmarshalled),序列化基于Native层实现,不同版本的API实现可能不同 |
开销 | 相对高 | 相对低 |
效率 | 相对低 | 相对高 |
适用场景 | 序列化到本地、网络传输 | 主要内存序列化 |
另外序列化过程中的几个注意点:
下面两种成员变量不会参与到默认序列化过程中:
1、static静态变量属于类而不属于对象
2、transient标记的成员变量
参与序列化的成员变量本身也是需要可序列化的
反序列化时,非可序列化的(如被transient修饰)变量将会调用自身的无参构造函数重新创建,因此也要求此成员变量的构造函数必须是可访问的,否则会报错。