Android 你不可不知的对象序列化 Serializable和Parcelable
目录结构
1. 序列化的目的
2. Android中序列化的两种方式Parcelable与Serializable
3. Parcelable与Serializable的性能比较
4. Parcelable与Serializable使用场景
5. Serializable接口实现序列化
6. Parcelable接口实现序列化
1.序列化的目的
(1)永久的保存对象数据(将对象数据保存在文件当中,或者是磁盘中)
(2)通过序列化操作将对象数据在网络上进行传输(由于网络传输是以字节流的方式对数据进行传输的.因此序列化的目的是将对象数据转换成字节流的形式)
(3)将对象数据在进程之间进行传递(Activity之间传递对象数据时,需要在当前的Activity中对对象数据进行序列化操作.在另一个Activity中需要进行反序列化操作讲数据取出)
(4)Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长(即每个对象都在JVM中)但在现实应用中,就可能要停止JVM运行,但有要保存某些指定的对象,并在将来重新读取被保存的对象。这是Java对象序列化就能够实现该功能。(可选择入数据库、或文件的形式保存)
(5)序列化对象的时候只是针对变量进行序列化,不针对方法进行序列化.
(6)在Intent之间,基本的数据类型直接进行相关传递即可,但是一旦数据类型比较复杂的时候,就需要进行序列化操作了.
2. Android中序列化的两种方式Parcelable与Serializable
两者区别
Serializable是java中自带的技术,因其资源消耗大后期Google对其进行优化推出了Parcelable,可以说Parcelable 是android上独有的序列化方式,性能得到大幅的优化,也因为这种独特性使得这种方式不可以在网络上进行传输。
Serializable使用IO读写存储在硬盘上。序列化过程使用了反射技术,并且期间产生临时对象。Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC。优点代码少。
Parcelable是直接在内存中读写,我们知道内存的读写速度肯定优于硬盘读写速度,所以Parcelable序列化方式性能上要优于Serializable方式很多。但是代码写起来相比Serializable方式麻烦一些。
3. Parcelable与Serializable的性能比较
对比 | Parcelable | Serializable |
---|---|---|
实现方式 | 实现Parcelable接口 | 实现Serializable接口 |
属于 | android 专用 | Java自带 |
内存消耗 | 优秀 | 一般 |
读写数据 | 内存中直接进行读写 | 通过使用IO流的形式将数据读写入在硬盘上 |
持久化 | 不可以 | 可以 |
速度 | 优秀 | 一般 |
4. Parcelable与Serializable使用场景
-
在内存间存储数据时, Parcelable比Serializable性能高,所以推荐使用Parcelable。但是书写相对麻烦数据量不大的前提下可以使用Serializable,但是不推荐。
-
Parcelable无法要将数据持久化在磁盘介质,也无法在网络间进行传输。这两种情况必须使用Serializable.其他推荐使用Parcelable
5.Serializable接口实现序列化
Serializable是Java所提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。使用Serializable来实现序列化相当简单,只需要在类的声明中指定一个类似下面的标识即可自动实现默认的序列化过程。
private static final long serialVersionUID = 8711368828010083044L
在Android中也提供了新的序列化方式,那就是Parcelable接口,使用Parcelable来实现对象的序列号,其过程要稍微复杂一些,本节先介绍Serializable接口。上面提到,想让一个对象实现序列化,只需要这个类实现Serializable接口并声明一个serialVersionUID即可,实际上,甚至这个serialVersionUID也不是必需的,我们不声明这个serialVersionUID同样也可以实现序列化,但是这将会对反序列化过程产生影响,具体什么影响后面再介绍。
User类就是一个实现了Serializable接口的类,它是可以被序列化和反序列化的,如下所示。
public class UserBean implements Serializable {
private static final long serialVersionUID = 2226350951019985L;
private String userName;
private int age;
private String address;
private HomeTown homeTown;
public UserBean(String userName, int age, String address, HomeTown homeTown) {
this.userName = userName;
this.age = age;
this.address = address;
this.homeTown = homeTown;
}
public static class HomeTown implements Serializable {
private static final long serialVersionUID = 22263509510199845L;
private String townName;
private String x;
private String y;
public HomeTown(String townName, String x, String y) {
this.townName = townName;
this.x = x;
this.y = y;
}
public String getTownName() {
return townName;
}
public void setTownName(String townName) {
this.townName = townName;
}
public String getX() {
return x;
}
public void setX(String x) {
this.x = x;
}
public String getY() {
return y;
}
public void setY(String y) {
this.y = y;
}
@Override
public String toString() {
return "HomeTown{" +
"townName='" + townName + '\'' +
", x='" + x + '\'' +
", y='" + y + '\'' +
'}';
}
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public HomeTown getHomeTown() {
return homeTown;
}
public void setHomeTown(HomeTown homeTown) {
this.homeTown = homeTown;
}
@Override
public String toString() {
return "UserBean{" +
"userName='" + userName + '\'' +
", age=" + age +
", address='" + address + '\'' +
", homeTown=" + homeTown.toString() +
'}';
}
}
通过Serializable方式来实现对象的序列化,实现起来非常简单,几乎所有工作都被系统自动完成了。如何进行对象的序列化和反序列化也非常简单,只需要采用ObjectOutputStream和ObjectInputStream即可轻松实现。下面举个简单的例子。
//序列化过程
try {
UserBean userBean = new UserBean("婉君", 24, "山西", new UserBean.HomeTown("山西·大同", "35.154145", "114.155"));
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(FileUtils.getPrimaryDirPath()+"/"+"userMsg.text"));
outputStream.writeObject(userBean);
outputStream.close();
tvMsg.setText("数据写入成功\n" + userBean.toString());
} catch (IOException e) {
e.printStackTrace();
}
//反序列化过程
try {
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(FileUtils.getPrimaryDirPath()+"/"+"userMsg.text"));
UserBean userBean2 = (UserBean) inputStream.readObject();
tvMsg.setText("数据读取成功\n" + userBean2.toString());
} catch (Exception e) {
e.printStackTrace();
}
上述代码演示了采用Serializable方式序列化对象的典型过程,很简单,只需要把实现
了Serializable接口的User对象写到文件中就可以快速恢复了,恢复后的对象newUser和user
的内容完全一样,但是两者并不是同一个对象。
刚开始提到,即使不指定serialVersionUID也可以实现序列化,那到底要不要指定
呢?如果指定的话,serialVersionUID后面那一长串数字又是什么含义呢?我们要明白,
系统既然提供了这个serialVersionUID,那么它必须是有用的。这个serialVersionUID是用
来辅助序列化和反序列化过程的,原则上序列化后的数据中的serialVersionUID只有和当
前类的serialVersionUID相同才能够正常地被反序列化。serialVersionUID的详细工作机制
是这样的:序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中(也可
能是其他中介),当反序列化的时候系统会去检测文件中的serialVersionUID,看它是否
和当前类的serialVersionUID一致,如果一致就说明序列化的类的版本和当前类的版本是
相同的,这个时候可以成功反序列化;否则就说明当前类和序列化的类相比发生了某些变
换,比如成员变量的数量、类型可能发生了改变,这个时候是无法正常反序列化的,因此
会报如下错误:
java.io.InvalidClassException: Main; local class incompatible: stream classdesc serialVersionUID = 8711368828010083044,local class serial-VersionUID = 8711368828010083043。
一般来说,我们应该手动指定serialVersionUID的值,比如102145454154152L,也可以让 AndroidStudio 根据
当前类的结构自动去生成它的hash值,这样序列化和反序列化时两者的serialVersionUID是
相同的,因此可以正常进行反序列化。如果不手动指定serialVersionUID的值,反序列化
时当前类有所改变,比如增加或者删除了某些成员变量,那么系统就会重新计算当前类的
hash值并把它赋值给serialVersionUID,这个时候当前类的serialVersionUID就和序列化的
数据中的serialVersionUID不一致,于是反序列化失败,程序就会出现crash。所以,我们
可以明显感觉到serialVersionUID的作用,当我们手动指定了它以后,就可以在很大程度
上避免反序列化过程的失败。比如当版本升级后,我们可能删除了某个成员变量也可能增
加了一些新的成员变量,这个时候我们的反向序列化过程仍然能够成功,程序仍然能够最
大限度地恢复数据,相反,如果不指定serialVersionUID的话,程序则会挂掉。当然我们
还要考虑另外一种情况,如果类结构发生了非常规性改变,比如修改了类名,修改了成员
变量的类型,这个时候尽管serialVersionUID验证通过了,但是反序列化过程还是会失
败,因为类结构有了毁灭性的改变,根本无法从老版本的数据中还原出一个新的类结构的
对象。
根据上面的分析,我们可以知道,给serialVersionUID指定为1L或者采用Eclipse根据
当前类结构去生成的hash值,这两者并没有本质区别,效果完全一样。以下两点需要特别
提一下,首先静态成员变量属于类不属于对象,所以不会参与序列化过程;其次用
transient关键字标记的成员变量不参与序列化过程。
下面通过例子来说明:
-
创建一个
UserBean
类,在未实现Serializable
接口的前提下,调用上面提到的方法序列化这个对象会怎么样
会报错未实现Serializable接口
-
接着上面的步骤,实现
Serializable
接口后,序列化和反序列化成功。 -
接着上面的实验,在类
UserBean
中添加内部类HomeTown
再序列化
会报错未实现Serializable接口
,结论变序列化的对象,每个类都要实现序列化接口包括他的内部类 -
内部类实现
Serializable
接口后,序列化和反序列化将会成功,这时候我们增加或删除外部类UserBean
的局部变量age
并增加变量String height
,完成后直接进行反序列化,运行之后报错java.io.InvalidClassException: Main; local class incompatible: stream classdesc serialVersionUID = 8711368828010083044,local class serial-VersionUID = 8711368828010083043
APP崩溃退出。 -
执行步骤4 的时候如果我们在
UserBean
中添加private static final long serialVersionUID = 2226350951019985L;
之后直接调用反序列化,程序正常执行并输出,结论: 被序列化的对象若没有为当前bean及其内部bean指定 serialVersionUID 的值,bean类结构改变后,编译器检测发现bean类的改变后将重新根据bean类计算新的hashcode值,造成bean类的hashCode值改变,先前序列化的数据无法反序列化(serialVersionUID
不相同将造成无法反序列化) -
接着上面的步骤,如果创建的 UserBean 中含有另一个 HomeTown 内部类,我们仅仅在外层的bean类中添加了
serialVersionUID = 2226350951019985L
会怎样
报错 未实现Serializable接口(每一层bean都需指定serialVersionUID) -
接着上面的步骤,对内部也添加
private static final long serialVersionUID = 22263509510199845L;
,这个时候调用序列化和反序列化均可成功。 -
经过实际的测试,被序列化的对象的数据结构发生大的改变的时(比如任意一层bean名称改变或局部成员类型改变),反序列化将无法进行.
APP报错闪退
java.io.InvalidClassException: Main; local class incompatible: stream classdesc serialVersionUID = 8711368828010083044,local class serial-VersionUID = 8711368828010083043。
6. Parcellable 接口实现序列化
Parcelable也是一个接口,只要实现这个接口并重写以下几个必要方法,一个类的对象就可
以实现序列化并可以通过Intent和Binder传递。下面的示例是一个典型的用法。
以 SystemMsgBean 对象的传输为例:
-
对象类实现Parcelable接口是完成对象传递的前提
public class SystemMsgBean implements Parcelable { private String msgId; private String msgBody; private long time; private int number; public SystemMsgBean(String msgId, String msgBody, long time, int number) { this.msgId = msgId; this.msgBody = msgBody; this.time = time; this.number = number; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(msgId); dest.writeString(msgBody); dest.writeLong(time); dest.writeInt(number); } public static final Creator<SystemMsgBean> CREATOR = new Creator<SystemMsgBean>() { @Override public SystemMsgBean createFromParcel(Parcel in) { return new SystemMsgBean(in); } @Override public SystemMsgBean[] newArray(int size) { return new SystemMsgBean[size]; } }; public SystemMsgBean(Parcel source) { this.msgId = source.readString(); this.msgBody = source.readString(); this.time = source.readLong(); this.number = source.readInt(); } @Override public String toString() { return "SystemMsgBean{" + "msgId='" + msgId + '\'' + ", msgBody='" + msgBody + '\'' + ", time=" + time + ", number=" + number + '}'; } }
注意:
SystemMsgBean(Parcel source) 参数的顺序一定需要与 writeToParcel(Parcel dest, int flags)参数顺序
这里先说一下Parcel,Parcel内部包装了可序列化的数据,可以在Binder中自由传输。从上述代码中可以看出,在序列化过程中需要实现的功能有序列化、反序列化和内容描述。序列化功能由writeToParcel方法来完成,最终是通过Parcel中的一系列write方法来完成的;反序列化功能由CREATOR来完成,其内部标明了如何创建序列化对象和数组,并通过Parcel的一系列read方法来完成反序列化过程;内容描述功能由describeContents方法来完成,几乎在所有情况下这个方法都应该返回0,仅当当前对象中存在文件描述符时,此方法返回1。需要注意的是,在User(Parcel in)方法中,由于book是另一个可序列化对象,所以它的反序列化过程需要传递当前线程的上下文类加载器,否则会报无法找到类的错误。详细的方法说明请参看表2-1。
方法 | 功能 | 标记位 |
---|---|---|
createFromParcel(Parcel in) | 从序列化后的对象中创建原始对象 | |
newArray(int size) | 创建指定长度的原始对象数组 | |
User(Parcel in) | 从序列化后的对象中创建原始对象 | |
writeToParce (Parcel out, int flags) | 将当前对象写入序列化结构中,其中flags标识有两种值: 0或者1 (参见右侧标记位)。 为1时标识当前对象需要作为返回值返回,不能立即释放资源,几乎所有情况都为0 | PARCELABLE_WRITE_RETURN_VALUE |
describeContents | 返回当前对象的内容描述。如果含有文件描述符,返回1(参见右侧标记位),否则返回0, 几乎所有情况都返回0 | ONTENTS_FILE_DESCRIPTOR |
-
将这个数据放到 intent 或者 Bundle 中完成传递
Intent intent = new Intent(MainActivity.this, ServerService.class); intent.setExtrasClassLoader(SystemMsgBean.class.getClassLoader()); Bundle bundle = new Bundle(); bundle.putParcelable("systemMsgBean", new SystemMsgBean("a00001" , "您有新的版本需要更新" , System.currentTimeMillis() , 0 )); intent.putExtras(bundle); bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
通过上面的例子可以看出,在Messenger中进行数据传递必须将数据放入Message中,而Messenger和Message都实现了Parcelable接口,因此可以跨进程传输。简单来说,Message中所支持的数据类型就是Messenger所支持的传输类型。实际上,通过Messenger来传输Message,Message中能使用的载体只有what、arg1、arg2、Bundle以及replyTo。Message中的另一个字段object在同一个进程中是很实用的,但是在进程间通信的时候,在Android 2.2以前object字段不支持跨进程传输,即便是2.2以后,也仅仅是系统提供的实现了Parcelable接口的对象才能通过它来传输。这就意味着我们自定义的Parcelable对象是无法通过object字段来传输的,读者可以试一下。非系统的Parcelable对象的确无法通过object字段来传输,这也导致了object字段的实用性大大降低,所幸我们还有Bundle,Bundle中可以支持大量的数据类型。
-
在需要接收目标组件中完成接收操作
Bundle bundle = intent.getExtras(); SystemMsgBean systemMsgBean = bundle.getParcelable("systemMsgBean"); LogUtil.e("onBind 接收到的消息为————————\n" + systemMsgBean.toString());
可得到如下输出日志即表明序列化后的数据已经成功完成通讯:
当然上面的过程仅仅是初级版本,仅仅完成了进程创建时的单向的通信过程,很多时候我们的需求是可以在两个进程之间可以自由并实时的进行双向完成通信,这个时候就引出了另一个进程间通讯的方案Messenger。