IT干货网

Android-序列化的人生三问

luoye 2022年03月15日 编程设计 157 0

何为三问?

广为流传的人生三问是:

我是谁?我从哪里来?我要到哪里去?

就像西游记中唐僧每次都这样介绍自己:

贫僧唐三藏,从东土大唐而来,去往西天取经。

不过,今天我不是来回答自己的人生问题,而是想通过类似的三个思考角度,What-Why-How三部曲来解释序列化的人生意义。

What?序列化是什么?

序列化是指将对象转换为对象流的一种机制。我们可以把它看做对象数据的两种存在方式的转换。

Why?为什么需要序列化?

当理解概念之后,可以思路很清晰的深入想到,既然是对象的两种存在方式,那么必定是存在互补关系,对象无法做到的事情,让对象流去做。那么对象流能做什么呢?

先问一个问题:

流是什么?

“ 流 ” 是一个抽象概念,它代表任何有能力产出数据的数据源对象或者是有能力接收数据的接收端对象。 “ 流 ” 屏蔽了实际的输入 / 输出设备中处理数据的细节。一个程序可以打开一个数据源上的流,然后按顺序读取这个流中的数据到程序中,这样的流称为输入流。一个程序也可以打开一个目的地的流,然后按顺序的把程序中的数据写入到这个目地中,这样的流称为输出流。

我们可以把它想象成一股水流,不管这个水是从苹果汁,还是橘子汁,变成液体之后,都是流。关于输入和输出的方向,流出去的叫输出流,流进来的是输入流,但是这些讲的话,参考物不同,同一个流的方向可能也不同,所以需要规定一个参考物。对于Android 中这个参考物就是内存,从内存出去的都是Out,进来的是In。

另外,流也有两种分类,从流本身的数据类型角度,分为字节流和字符流。从流存在的意义角度,分缓冲流和节点流。字节流和字符流,顾名思义,流中的数据分别以字符和字节存在。缓冲流和节点流,节点流是指可以从或者向一个特定的地方(节点)读写数据,处理流:是对一个已经存在的流的连接和封装,通过所封装的流的功能调用实现数据读、写功能。用水流来比喻,有水源头和流入源头的流,被称为节点流,中间途径一些地方,改变流速的那部分,被称为缓冲流,例如三门峡大水坝。

而且,流不能单独存在,总要有输入方和输出方。就像河流一样,有源头和入海口才能被称为流,在一个地方静止不动的,那是湖泊不是河流。

流能干嘛?

这个问题,流的概念中就有解释:屏蔽实际的输入/输出方处理数据的细节。因为这个特性,流被用来做数据传输。
例如:

  • 服务端和客户端之间的数据传输
  • 本地文件和客户端间的数据传输
  • 客户端内部中关联页面间的数据传输

回答完以上问题,你知道为什么要序列化了吗?

答案是因为某种用途,需要将数据转换成流。

How?如何实现序列化?

对于 Android 开发者来说,有两种序列化方式选择:

  • Serializable
  • Parcelale

其中 Serializable 是 Java 提供的序列化接口,Parcelale 是 Android 提供的新的序列化方式。两者的区别稍后再说,现在
先体验两者实现序列化的过程。

Serializable

Serializable 接口是一个空接口,唯一的目的就是声明实现它的类是可以被序列化的。

public class UserBean implements Serializable{ 
  public int id; 
  public String name; 
 
    public UserBean(int id, String name) { 
        this.id = id; 
        this.name = name; 
    } 
} 

将一个Serializable 对象序列化,非常简单,只需要简单几个步骤。

//创建一个要序列化的对象 
UserBean userBean = new UserBean(666,"小明"); 
 
//创建文件输出流 
FileOutputStream fileOutputStream = new FileOutputStream(fileName); 
 
//创建对象输出流 
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); 
 
//写入对象 
objectOutputStream.writeObject(userBean); 
 
//关闭流 
objectOutputStream.close(); 
 
 

反序列化也非常简单,反向将文件转换为文件输入流,文件输入流转化为对象流,最后转换成对象即可。

//创建文件输入流 
FileInputStream fileInputStream = new FileInputStream(fileName); 
 
//创建对象输入流 
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); 
 
//读出对象 
UserBean userBean = (UserBean)objectInputStream.readObject(); 
 
//关闭流 
objectInputStream.close(); 

这里存在些许疑问:

对象流转化成对象的时候,是通过什么识别的呢?

以上情况下,如果序列化之后, UserBean 类新增了 address 字段,还可以成功反序列转换吗?

答案是 NO !

原因是当实现 Serializable 接口时,Java 会默认创建 serialVersionUID 字段。当类中的字段修改之后,Java 类默认的 serialVersionUID 会自动发生变化,因此出现了异常。

更具体的原因是,Java 的序列化操作的时候系统会把当前类的 serialVersionUID 写入到序列化文件中,当反序列化时系统会去检测文件中的 serialVersionUID,判断它是否与当前类的 serialVersionUID 一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。

实际项目中,实体的字段变化挺常见的,为了避免以上情况,最好是手动创建 serialVersionUID 。

//序列化标识 
private static final long serialVersionUID = 64893744941884305L; 

很长一串手写也不现实,不过不要慌,借助开发工具,可以帮我们自动生成。

  • 第一步,开启检查,在 Serializable 实现类中没有创建该字段的出现警告。Android Studio 的操作路径:Setting->Inspections->Java->Serialization issues->Serializable class without ’serialVersionUID’

  • 第二步,选上以后,在出现警告的类中,类名上 Alt+Enter 就会提示自动创建 serialVersionUID了。

Parcelale

下面介绍另外一种序列化方式 Parcelale。Parcelale 也是一个接口,只要实现了这个接口,就可以实现对象的序列化和反序列化了。

Parcelale 不像 Serializable 是一个标记作用的空接口,内部需要实现几个方法。源码如下,序列化方式由 writeToParcel 方法完成,反序列化由 Creator 完成。

public interface Parcelable { 
    int CONTENTS_FILE_DESCRIPTOR = 1; 
    int PARCELABLE_WRITE_RETURN_VALUE = 1; 
 
    int describeContents(); 
 
    void writeToParcel(android.os.Parcel parcel, int i); 
 
    static interface ClassLoaderCreator <T> extends android.os.Parcelable.Creator<T> { 
        T createFromParcel(android.os.Parcel parcel, java.lang.ClassLoader classLoader); 
    } 
 
    static interface Creator <T> { 
        T createFromParcel(android.os.Parcel parcel); 
 
        T[] newArray(int i); 
    } 
} 
 

下面给一个对比的小例子:

 
//序列化前 
public class BookBean implements Parcelable{ 
    public int id; 
    public String name; 
 
    public BookBean(int id, String name) { 
        this.id = id; 
        this.name = name; 
    } 
} 
 
//序列化后 
public class BookBean implements Parcelable{ 
    public int id; 
    public String name; 
 
    public BookBean(int id, String name) { 
        this.id = id; 
        this.name = name; 
    } 
 
    protected BookBean(Parcel in) { 
        id = in.readInt(); 
        name = in.readString(); 
    } 
 
    @Override 
    public String toString() { 
        return "BookBean{" + 
                "id=" + id + 
                ", name='" + name + '\'' + 
                '}'; 
    } 
 
    @Override 
    public void writeToParcel(Parcel dest, int flags) { 
        dest.writeInt(id); 
        dest.writeString(name); 
    } 
 
    @Override 
    public int describeContents() { 
        return 0; 
    } 
 
    public static final Creator<BookBean> CREATOR = new Creator<BookBean>() { 
        @Override 
        public BookBean createFromParcel(Parcel in) { 
            return new BookBean(in); 
        } 
 
        @Override 
        public BookBean[] newArray(int size) { 
            return new BookBean[size]; 
        } 
    }; 
} 

代码变好多,字段如何很多,写起来估计要抓狂,但是不要慌,Android Studio 来帮你,Alt + Enter 可以选择自动代码生成,简直不要太爽。

Serializable VS Parcelale

首先两种方式都能实现序列化并且都可以被用来 Intent 做数据传递,那两者之前应该怎么选择呢?

你以为是这样:

  • A:Serializable
  • B:Parcelale

其实还可以这样:

  • C:Serializable and Parcelale

Serializable 是 Java 中的序列化解救,使用起来简单但是开销很大,序列化和反序列化都需要大量的 IO 操作。而 Parcelable 是 Android 中的序列化方式,因此更适合用在 Android 平台上,它的缺点是使用起来稍微麻烦一点,但是它的效率很高。因此在对象序列化存储到网络传输或者硬件存储设备中时,推荐使用 Serializable。在内存中操作传递数据时,推荐使用 Parcelable。

最后

两种序列化方式的使用选择建议内容来源于 《Android 开发艺术探索》,至于为什么 Serializable 内存开销大,Parcelable 效率高,需要去分析更深处的原理,有时间会研究一下,另外出篇文章。如果有知道的小伙伴,欢迎在文章下面留言讨论交流。


欢迎关注个人微信公众号,最新的博客,好玩的事情,都会在上面分享,期待与你共同成长。

在这里插入图片描述


评论关闭
IT干货网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!

Android-组件化改造项目之概述