前言
公园里, 一位仙风鹤骨的老者在打太极, 一招一式都仙气十足, 一个年轻人走过去:"大爷, 太极这玩意儿花拳绣腿, 你练它干啥?" 老者淡淡一笑:"年轻人, 你还没有领悟到太极的真谛, 这样, 你用最大力气打我试试." 于是年轻人用力打了老头一拳, 被讹了八万六.
从段子就能看出来, 今天这篇博客又是一碗炒冷饭. 序列化使用很简单, 但是其中的一些细节并不是所有人都清楚. 在日常的应用开发中, 我们可能需要让某些对象离开内存空间, 存储到物理磁盘, 以便长期保存, 同时也能减少对内存的压力, 而在需要时再将其从磁盘读取到内存, 比如将某个特定的对象保存到文件中, 隔一段时间后再把它读取到内存中使用, 那么该对象就需要实现序列化操作, 在 java 中可以使用 Serializable 接口实现对象的序列化, 而在 Android 中既可以使用 Serializable 接口实现对象序列化也可以使用 Parcelable 接口实现对象序列化, 但是在内存操作时更倾向于实现 Parcelable 接口, 这样会使用传输效率更高效. 接下来我们将分别详细地介绍这样两种序列化操作.
序列化与反序列
首先来了解一下序列化与反序列化.
(1)序列化
由于存在于内存中的对象都是暂时的, 无法长期驻存, 为了把对象的状态保持下来, 这时需要把对象写入到磁盘或者其他介质中, 这个过程就叫做序列化.
(2)反序列化
反序列化恰恰是序列化的反向操作, 也就是说, 把已存在在磁盘或者其他介质中的对象, 反序列化 (读取) 到内存中, 以便后续操作, 而这个过程就叫做反序列化.
概括性来说序列化是指将对象实例的状态存储到存储媒体 (磁盘或者其他介质) 的过程. 在此过程中, 先将对象的公共字段和私有字段以及类的名称 (包括类所在的程序集) 转换为字节流, 然后再把字节流写入数据流. 在随后对对象进行反序列化时, 将创建出与原对象完全相同的副本.
(3)实现序列化的必要条件
一个对象要实现序列化操作, 该类就必须实现了 Serializable 接口或者 Parcelable 接口, 其中 Serializable 接口是在 java 中的序列化抽象类, 而 Parcelable 接口则是 Android 中特有的序列化接口, 在某些情况下, Parcelable 接口实现的序列化更为高效, 关于它们的实现案例我们后续会分析, 这里只要清楚知道实现序列化操作时必须实现 Serializable 接口或者 Parcelable 接口之一即可.
(4)序列化的应用情景
主要有以下情况(但不限于以下情况)
1)内存中的对象写入到硬盘;
2)用套接字在网络上传送对象;
Serializable
Serializable 是 java 提供的一个序列化接口, 它是一个空接口, 专门为对象提供标准的序列化和反序列化操作, 使用 Serializable 实现类的序列化比较简单, 只要在类声明中实现 Serializable 接口即可, 同时强烈建议声明序列化标识. 如下:
- public class User implements Serializable {
- private static final long serialVersionUID = -2083503801443301445L;
- private int id;
- private String name;
- public int getId() {
- return id;
- }
- public void setId(int id) {
- this.id = id;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- }
如上述代码所示, User 类实现的 Serializable 接口并声明了序列化标识 serialVersionUID, 该 ID 由编辑器生成, 当然也可以自定义, 如 1L,5L, 不过还是建议使用编辑器生成唯一标识符. 那么 serialVersionUID 有什么作用呢? 实际上我们不声明 serialVersionUID 也是可以的, 因为在序列化过程中会自动生成一个 serialVersionUID 来标识序列化对象. 既然如此, 那我们还需不需要要指定呢? 原因是 serialVersionUID 是用来辅助序列化和反序列化过程的, 原则上序列化后的对象中 serialVersionUID 只有和当前类的 serialVersionUID 相同才能够正常被反序列化, 也就是说序列化与反序列化的 serialVersionUID 必须相同才能够使序列化操作成功. 具体过程是这样的: 序列化操作的时候系统会把当前类的 serialVersionUID 写入到序列化文件中, 当反序列化时系统会去检测文件中的 serialVersionUID, 判断它是否与当前类的 serialVersionUID 一致, 如果一致就说明序列化类的版本与当前类版本是一样的, 可以反序列化成功, 否则失败. 报出如下 UID 错误:
- Exception in thread "main" java.io.InvalidClassException: com.zejian.test.Client;
- local class incompatible: stream classdesc serialVersionUID = -2083503801443301445,
- local class serialVersionUID = -4083503801443301445
因此强烈建议指定 serialVersionUID, 这样的话即使微小的变化也不会导致 crash 的出现, 如果不指定的话只要这个文件多一个空格, 系统自动生成的 UID 就会截然不同的, 反序列化也就会失败. ok~, 了解这么多, 下面来看一个如何进行对象序列化和反序列化的列子:
- public class Demo {
- public static void main(String[] args) throws Exception {
- // 构造对象
- User user = new User();
- user.setId(1000);
- user.setName("韩梅梅");
- // 把对象序列化到文件
- ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/serializable/user.txt"));
- oos.writeObject(user);
- oos.close();
- // 反序列化到内存
- ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/serializable/user.txt"));
- User userBack = (User) ois.readObject();
- System.out.println("read serializable user:id=" + userBack.getId() + ", name=" + userBack.getName());
- ois.close();
- }
- }
输出结果:
read serializable user:id=1000, name = 韩梅梅
从代码可以看出只需要 ObjectOutputStream 和 ObjectInputStream 就可以实现对象的序列化和反序列化操作, 通过流对象把 user 对象写到文件中, 并在需要时恢复 userBack 对象, 但是两者并不是同一个对象了, 反序列化后的对象是新创建的. 这里有两点特别注意的是如果反序列类的成员变量的类型或者类名, 发生了变化, 那么即使 serialVersionUID 相同也无法正常反序列化成功. 其次是静态成员变量属于类不属于对象, 不会参与序列化过程, 使用 transient 关键字标记的成员变量也不参与序列化过程.
另外, 系统的默认序列化过程是可以改变的, 通过实现如下 4 个方法, 即可以控制系统的默认序列化和反序列过程:
- public class User implements Serializable {
- private static final long serialVersionUID = -4083503801443301445L;
- private int id;
- private String name;
- public int getId() {
- return id;
- }
- public void setId(int id) {
- this.id = id;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- /**
- * 序列化时,
- * 首先系统会先调用 writeReplace 方法, 在这个阶段,
- * 可以进行自己操作, 将需要进行序列化的对象换成我们指定的对象.
- * 一般很少重写该方法
- */
- private Object writeReplace() throws ObjectStreamException {
- System.out.println("writeReplace invoked");
- return this;
- }
- /**
- * 接着系统将调用 writeObject 方法,
- * 来将对象中的属性一个个进行序列化,
- * 我们可以在这个方法中控制住哪些属性需要序列化.
- * 这里只序列化 name 属性
- */
- private void writeObject(java.io.ObjectOutputStream out) throws IOException {
- System.out.println("writeObject invoked");
- out.writeObject(this.name == null ? "默认值" : this.name);
- }
- /**
- * 反序列化时, 系统会调用 readObject 方法, 将我们刚刚在 writeObject 方法序列化好的属性,
- * 反序列化回来. 然后通过 readResolve 方法, 我们也可以指定系统返回给我们特定的对象
- * 可以不是 writeReplace 序列化时的对象, 可以指定其他对象.
- */
- private void readObject(java.io.ObjectInputStream in) throws IOException,
- ClassNotFoundException {
- System.out.println("readObject invoked");
- this.name = (String) in.readObject();
- System.out.println("got name:" + name);
- }
- /**
- * 通过 readResolve 方法, 我们也可以指定系统返回给我们特定的对象
- * 可以不是 writeReplace 序列化时的对象, 可以指定其他对象.
- * 一般很少重写该方法
- */
- private Object readResolve() throws ObjectStreamException {
- System.out.println("readResolve invoked");
- return this;
- }
- }
通过上面的 4 个方法, 我们就可以随意控制序列化的过程了, 由于在大部分情况下我们都没必要重写这 4 个方法, 因此这里我们也不过介绍了, 只要知道有这么一回事就行. ok~, 对于 Serializable 的介绍就先到这里.
Parcelable
鉴于 Serializable 在内存序列化上开销比较大, 而内存资源属于 Android 系统中的稀有资源(Android 系统分配给每个应用的内存开销都是有限的), 为此 Android 中提供了 Parcelable 接口来实现序列化操作, Parcelable 的性能比 Serializable 好, 在内存开销方面较小, 所以在内存间数据传输时推荐使用 Parcelable, 如通过 Intent 在 activity 间传输数据, 而 Parcelable 的缺点就使用起来比较麻烦, 下面给出一个 Parcelable 接口的实现案例, 大家感受一下:
- public class User implements Parcelable {
- public int id;
- public String name;
- public User friend;
- /**
- * 当前对象的内容描述, 一般返回 0 即可
- */
- @Override
- public int describeContents() {
- return 0;
- }
- /**
- * 将当前对象写入序列化结构中
- */
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(this.id);
- dest.writeString(this.name);
- dest.writeParcelable(this.friend, 0);
- }
- public NewClient() {}
- /**
- * 从序列化后的对象中创建原始对象
- */
- protected NewClient(Parcel in) {
- this.id = in.readInt();
- this.name = in.readString();
- //friend 是另一个序列化对象, 此方法序列需要传递当前线程的上下文类加载器, 否则会报无法找到类的错误
- this.friend=in.readParcelable(Thread.currentThread().getContextClassLoader());
- }
- /**
- * public static final 一个都不能少, 内部对象 CREATOR 的名称也不能改变, 必须全部大写.
- * 重写接口中的两个方法:
- * createFromParcel(Parcel in) 实现从 Parcel 容器中读取传递数据值, 封装成 Parcelable 对象返回逻辑层,
- * newArray(int size) 创建一个类型为 T, 长度为 size 的数组, 供外部类反序列化本类数组使用.
- */
- public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
- /**
- * 从序列化后的对象中创建原始对象
- */
- @Override
- public User createFromParcel(Parcel source) {
- return new User(source);
- }
- /**
- * 创建指定长度的原始对象数组
- */
- @Override
- public User[] newArray(int size) {
- return new User[size];
- }
- };
- }
从代码可知, 在序列化的过程中需要实现的功能有序列化和反序列以及内容描述. 其中 writeToParcel 方法实现序列化功能, 其内部是通过 Parcel 的一系列 write 方法来完成的, 接着通过 CREATOR 内部对象来实现反序列化, 其内部通过 createFromParcel 方法来创建序列化对象并通过 newArray 方法创建数组, 最终利用 Parcel 的一系列 read 方法完成反序列化, 最后由 describeContents 完成内容描述功能, 该方法一般返回 0, 仅当对象中存在文件描述符时返回 1. 同时由于 User 是另一个序列化对象, 因此在反序列化方法中需要传递当前线程的上下文类加载器, 否则会报无法找到类的错误.
简单用一句话概括来说就是通过 writeToParcel 将我们的对象映射成 Parcel 对象, 再通过 createFromParcel 将 Parcel 对象映射成我们的对象. 也可以将 Parcel 看成是一个类似 Serliazable 的读写流, 通过 writeToParcel 把对象写到流里面, 在通过 createFromParcel 从流里读取对象, 这个过程需要我们自己来实现并且写的顺序和读的顺序必须一致. ok~, 到此 Parcelable 接口的序列化实现基本介绍完.
那么在哪里会使用到 Parcelable 对象呢? 其实通过 Intent 传递复杂类型 (如自定义引用类型数据) 的数据时就需要使用 Parcelable 对象, 如下是日常应用中 Intent 关于 Parcelable 对象的一些操作方法, 引用类型必须实现 Parcelable 接口才能通过 Intent 传递, 而基本数据类型, String 类型则可直接通过 Intent 传递而且 Intent 本身也实现了 Parcelable 接口, 所以可以轻松地在组件间进行传输.
方法名称 | 含义 |
---|---|
putExtra(String name, Parcelable value) | 设置自定义类型并实现 Parcelable 的对象 |
putExtra(String name, Parcelable[] value) | 设置自定义类型并实现 Parcelable 的对象数组 |
putParcelableArrayListExtra(String name, ArrayList value) | 设置 List 数组,其元素必须是实现了 Parcelable 接口的数据 |
除了以上的 Intent 外系统还为我们提供了其他实现 Parcelable 接口的类, 再如 Bundle,Bitmap, 它们都是可以直接序列化的, 因此我们可以方便地使用它们在组件间进行数据传递, 当然 Bundle 本身也是一个类似键值对的容器, 也可存储 Parcelable 实现类, 其 API 方法跟 Intent 基本相似, 由于这些属于 Android 基础知识点, 这里我们就不过多介绍了.
Parcelable 与 Serializable 区别
(1)两者的实现差异
Serializable 的实现, 只需要实现 Serializable 接口即可. 这只是给对象打了一个标记(UID), 系统会自动将其序列化. 而 Parcelabel 的实现, 不仅需要实现 Parcelabel 接口, 还需要在类中添加一个静态成员变量 CREATOR, 这个变量需要实现 Parcelable.Creator 接口, 并实现读写的抽象方法.
(2)两者的设计初衷
Serializable 的设计初衷是为了序列化对象到本地文件, 数据库, 网络流, RMI 以便数据传输, 当然这种传输可以是程序内的也可以是两个程序间的. 而 Android 的 Parcelable 的设计初衷是由于 Serializable 效率过低, 消耗大, 而 Android 中数据传递主要是在内存环境中(内存属于 Android 中的稀有资源), 因此 Parcelable 的出现为了满足数据在内存中低开销而且高效地传递问题.
(3)两者效率选择
Serializable 使用 IO 读写存储在硬盘上. 序列化过程使用了反射技术, 并且期间产生临时对象, 优点代码少, 在将对象序列化到存储设置中或将对象序列化后通过网络传输时建议选择 Serializable.
Parcelable 是直接在内存中读写, 我们知道内存的读写速度肯定优于硬盘读写速度, 所以 Parcelable 序列化方式性能上要优于 Serializable 方式很多. 所以 Android 应用程序在内存间数据传输时推荐使用 Parcelable, 如 activity 间传输数据和 AIDL 数据传递. 大多数情况下使用 Serializable 也是没什么问题的, 但是针对 Android 应用程序在内存间数据传输还是建议大家使用 Parcelable 方式实现序列化, 毕竟性能好很多, 其实也没多麻烦.
Parcelable 也不是不可以在网络中传输, 只不过实现和操作过程过于麻烦并且为了防止 Android 版本不同而导致 Parcelable 可能不同的情况, 因此在序列化到存储设备或者网络传输方面还是尽量选择 Serializable 接口.
AndroidStudio 中的快捷生成方式
(1)AndroidStudio 快捷生成 Parcelable 代码
在程序开发过程中, 我们实现 Parcelable 接口的代码都是类似的, 如果我们每次实现一个 Parcelable 接口类, 就得去编写一次重复的代码, 这显然是不可取的, 不过幸运的是, Android studio 提供了自动实现 Parcelable 接口的方法的插件, 相当实现, 我们只需要打开 Setting, 找到 plugin 插件, 然后搜索 Parcelable 插件, 最后找到 Android Parcelable code generator 安装即可:
重启 Android studio 后, 我们创建一个 User 类, 如下:
- public class User {
- public int id;
- public int age;
- public String name;
- }
然后使用刚刚安装的插件协助我们生成实现 Parcelable 接口的代码, Windows 快捷键: Alt+Insert,Mac 快捷键: cmd+n, 如下:
最后结果如下:
- public class User implements Parcelable {
- public int id;
- public int age;
- public String name;
- @Override
- public int describeContents() {
- return 0;
- }
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(this.id);
- dest.writeInt(this.age);
- dest.writeString(this.name);
- }
- public User() {
- }
- protected User(Parcel in) {
- this.id = in.readInt();
- this.age = in.readInt();
- this.name = in.readString();
- }
- public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
- @Override
- public User createFromParcel(Parcel source) {
- return new User(source);
- }
- @Override
- public User[] newArray(int size) {
- return new User[size];
- }
- };
- }
(2)AndroidStudio 快捷生成 Serializable 的 UID
在正常情况下, AS 是默认关闭 serialVersionUID 生成提示的, 我们需要打开 setting, 找到检测(Inspections 选项), 开启 Serializable class without serialVersionUID 检测即可, 如下:
image
然后新建 User 类实现 Serializable 接口, 右侧会提示添加 serialVersionUID, 如下:
image
鼠标放在类名上, Alt+Enter(Mac:cmd+Enter), 快捷代码提示, 生成 serialVersionUID 即可:
最终生成结果:
- public class User implements Serializable {
- private static final long serialVersionUID = 6748592377066215128L;
- public int id;
- public int age;
- public String name;
- }
总结
以上就是 Android 序列化的全部内容, 很简单, 但是也有细节. 我有一个想法, 就是后面专门写一些表面很简单但是细节可能不清楚的知识点, 我们不要始终把目光聚集在大框架上, 高端前沿技术什么的, 偶尔研究研究基础的东西也不错.
最后
如果你看到了这里, 觉得文章写得不错就给个喜欢呗? 如果你觉得那里值得改进的, 请给我留言. 一定会认真查询, 修正不足. 谢谢.
来源: http://www.jianshu.com/p/c18a4b593c01