Android-序列化和反序列化(Serializable和Parcelable)

一、为什么使用序列化

由于在系统底层,数据的传输形式是简单的字节序列形式传输,在系统底层,并不认识Java对象,只知道字节序列,所以想要达到进程通讯的目的,需要先将数据进行序列化,即将对象转化为字节序列的过程。而字节序列被响应的进程使用的时候,进程为了识别这些字节序列,就需要对这些字节序列进行反序列化操作,把字节序列转换成Java对象。

1.序列化

将数据结果或者对象转化为二进制串的过程

2.反序列化

将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程

序列化/反序列化的目的

序列化:主要用于网络传输,数据持久化,也可以叫做编码
反序列化:主要用于从网络、磁盘上读取字节数组还原成原始对象,也可以叫做解码
(1)永久的保存对象数据(将对象数据保存在文件或者磁盘中)
(2)通过序列化操作将对象数据在网络上进行传输(由于网络传输是以字节流的方式对数据进行传输的,因此序列化的目的是将对象数据转换成字节流的形式)
(3)将对象数据在进程间进行传输(Activity之间传递对象数据时,需要在当前的Activity中对对象数据进行序列化操作,在另一个Activity中需要进行反序列化操作将数据取出)
(4)Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长(即每个对象都在JVM中)但在现实应用中,就可能要停止JVM运行,但有要保存某些指定的对象,并在将来重新读取被保存的对象。这是Java对象序列化就能实现该功能(可以选择入数据库、或者文件的形式保存)
(5)序列化对象的时候只是针对变量进行序列化,不针对方法进行序列化
(6)在Intent之间,基本的数据类型直接进行相关传递即可,但是一旦数据类型比较复杂的时候,就需要进行序列化操作。

二、序列化协议特性

1.通用性

2.强健性/鲁棒性

3.可调适性/可读性

4.性能

(1)空间开销:序列化需要在原有的数据上加上描述字段,为反序列化解析使用。如果序列化过程引入的额外开销过高,可能会导致过大的网络、磁盘等各方面压力。对于海量分布式存储系统,数据量往往以TB为单位,巨大的额外空间开销意味着高昂的成本
(2)时间开销:复杂的序列化协议会导致较长的解析时间,这可能会使得序列化和反序列化阶段成为整个系统的瓶颈

5.可扩展性/兼容性

业务系统需求的更新周期变快,老的系统也需要维护。如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度。

6.安全性/访问限制

在序列化选型的过程中,安全性的考虑往往发生在跨局域网的场景。当通讯发生在公司之间或者跨机房的时候,处于安全的考虑,对于跨局域网的访问往往被限制为基于HTTP/HTTPS的80和433端口。如果使用的序列化协议没有兼容而成熟的HTTP传输层框架支持,可能会导致以下三种结果:
(1)因为访问限制而降低服务可用性
(2)被迫重新实现安全协议而导致实施成本大大提高
(3)开放更多的防火墙端口和协议访问,而牺牲了安全性

三、Serializable接口

Serializable接口可以用来标识当前类可以被ObjectOutputStream序列化,以及被ObjectInputStream反序列化,Serializable是一个空接口,就是起一个标识的作用。

1.Serializable的简单使用和注意事项

public class Student implements Serializable {
    //serialVersionUID唯一标识了一个可序列化的类
    private static final long serialVersionUID = -2100492893943893602L;
    private String name;
    private String sax;
    private Integer age;
    //Course也需要实现Serializable接口
    private List<Course> courses;
    //用transient关键字标记的成员变量不参与序列化(在被反序列化后,transient变量的值
    //被设为初始值,如 int 型的是 0,对象型的是 null)
    private transient Date createTime;
    //静态成员变量属于类不属于对象,所以不会参与序列化(对象序列化保存的是对象的“状态”,
    //也就是它的成员变量,因此序列化不会关注静态变量)
    private static SimpleDateFormat simpleDateFormat = newSimpleDateFormat();
    // 一个可序列化的类中,存在不可序列化的属性,那么这个类的无参构造函数一定要可以访问
    // 否则在反序列化过程中,会因为无法调用无参构造函数初始化不可序列化的属性而报错
    public Student() {
        System.out.println("Student: empty");
    }
    public Student(String name, String sax, Integer age) {
        System.out.println("Student: " + name + " " + sax + " " + age);
        this.name = name;
        this.sax = sax;
        this.age = age;
        courses = new ArrayList<>();
        createTime = new Date();
    }
    ...
}
////Course也需要实现Serializable接口
public class Course implements Serializable {
    private static final long serialVersionUID = 667279791530738499L;
    private String name;
    private float score;
    ...
}

Serializable接口在使用的时候,可以配合transient关键字,使用transient关键字修饰的对象,不能被序列化;静态成员也不能被序列化。

2.serialVersionUID与兼容性

(1)serialVersionUID的作用

serialVersionUID用来表明类的不同版本间的兼容性。如果修改了此类,则需要修改这个值。否则以前用老版本的类序列化的类恢复时会报错:InvalidClassException

(2)兼容性问题

为了在反序列化时,确保类版本的兼容性,最好在每个要序列化的类中加入private static final
long serialVersionUID这个属性,具体数值自定义。这样做的目的,即使某个类在与之对应的对象已经序列化出去后做了修改,该对象依然可以被正确的反序列化。否则,如果不显示定义该属性,这个属性值将由JVM根据类的相关信息计算,而修改后的类的计算结果与修改前的类的计算结果往往不同,从而造成对象的反序列化因为类版本不兼容而失败。
不显示定义这个属性值的另一个坏处是,不利于程序在不同的JVM之间的移植。因为不同的编译器实现该属性值的计算策略可能不同,从而造成虽然类没有改变,但是可能因为JVM不同,出现因为类版本不兼容而无法正确反序列化的现象

3.Externalizable接口

Externalizable接口继承自Serializable接口,通过覆写其writeExternal和readExternal方法,针对类的部分属性做序列化和反序列化。实现了Externalizable接口,一定要有无参构造函数,如果没有无参构造函数,则反序列化的时候会报错;并且实现了Externalizable接口之后,并不需要serialVersionUID;

4.ObjectOutputStream.writeObject原理分析

ObjectOutputStream构造函数
public ObjectOutputStream(OutputStream out) throws IOException {
    verifySubclass();
    bout = new BlockDataOutputStream(out);
    handles = new HandleTable(10, (float) 3.00);
    subs = new ReplaceTable(10, (float) 3.00);
    // 这设置为false,是在writeObject执行时判断执行的分支
    enableOverride = false;//enableOverride = false
    ...
}
ObjectOutputStream.writeObject
public final void writeObject(Object obj) throws IOException {
    //enableOverride=false,不走这里
    if (enableOverride) {
        writeObjectOverride(obj);
        return;
    }
    try {//一般情况都走这里
        writeObject0(obj, false);
    ...
}
ObjectOutputStream.writeObject0

在这里,判断传入的Object对象的具体类型,根据类型选择相对应的操作,如果不满足这四种类型条件,则抛出异常
/**

  • Underlying writeObject/writeUnshared implementation.
    */
    private void writeObject0(Object obj, boolean unshared)
    throws IOException{
    ...
    // remaining cases
    if (obj instanceof String) {
    writeString((String) obj, unshared);
    } else if (cl.isArray()) {
    writeArray(obj, desc, unshared);
    } else if (obj instanceof Enum) {
    writeEnum((Enum<?>) obj, desc, unshared);
    } else if (obj instanceof Serializable) {
    //如果实现了Serializable接口
    writeOrdinaryObject(obj, desc, unshared);
    } else {//如果没有实现Serializable接口,会报NotSerializableException
    if (extendedDebugInfo) {
    throw new NotSerializableException(
    cl.getName() + "\n" + debugInfoStack.toString());
    } else {
    throw new NotSerializableException(cl.getName());
    ...
    }
ObjectOutputStream.writeOrdinaryObject

因为Java序列化和反序列化有可能会实现Serializable或者是Externalizable,所以在这里会有两个不同的输出情况

private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared)
    ...
    if (desc.isExternalizable() && !desc.isProxy()) {
        //如果对象实现了Externalizable接口,那么执行writeExternalData((Externalizable) obj)方法
        writeExternalData((Externalizable) obj);
    } else {
        //如果对象实现的是Serializable接口,那么执行的是writeSerialData(obj, desc)
        writeSerialData(obj, desc);
    }
    ...
}
ObjectOutputStream.writeSerialData
/**
* Writes instance data for each serializable class of given object,
from
* superclass to subclass.
* 最终写序列化的方法
*/
private void writeSerialData(Object obj, ObjectStreamClass desc)
throws IOException{
    ...
    if (slotDesc.hasWriteObjectMethod()) {
        //如果writeObjectMethod != null(目标类中定义了私有的writeObject
        //方法),那么将调用目标类中的writeObject方法
        // 其实就是需要序列化的类重写了writeObject方法,由开发者自己实现序列化的功能
        ...
            slotDesc.invokeWriteObject(obj, this);
        ...
    } else {
        //如果如果writeObjectMethod == null, 
        //那么将调用默认的defaultWriteFields方法来读取目标类中的属性
        // 调用BlockDataOutputStream.write将二进制流序列化写入
            defaultWriteFields(obj, slotDesc);
        }
    }
}
ObjectStreamClass

在ObjectStreamClass中,ObjectOutputStream(ObjectInputStream)会寻找目标类中的私有的writeObject(readObject)方法,赋值给变量writeObjectMethod(readObjectMethod)

/**
* Creates local class descriptor representing given class.
*/
private ObjectStreamClass(final Class<?> cl) {
    ...
                if (externalizable) {
                    cons = getExternalizableConstructor(cl);
                } else {
                    //,在序列化(反序列化)的时候,ObjectOutputStream(ObjectInputStream)
                    // 会寻找目标类中的私有的writeObject(readObject)方法,
                    // 赋值给变量writeObjectMethod(readObjectMethod)
                    cons = getSerializableConstructor(cl);
                    writeObjectMethod = getPrivateMethod(cl, "writeObject", 
                        new Class<?>[] { ObjectOutputStream.class },Void.TYPE);
                    readObjectMethod = getPrivateMethod(cl, "readObject",
                        new Class<?>[] { ObjectInputStream.class }, Void.TYPE);
                    readObjectNoDataMethod = getPrivateMethod(cl, "readObjectNoData", 
                        null, Void.TYPE);
                    hasWriteObjectData = (writeObjectMethod != null);
                }
                domains = getProtectionDomains(cons, cl);
                writeReplaceMethod = getInheritableMethod(
                    cl, "writeReplace", null, Object.class);
                readResolveMethod = getInheritableMethod(
                    cl, "readResolve", null, Object.class);
                return null;
            }
        });
    ...
}

5.重写readObject、writeObject、writeReplace、readResolve

如果是重写了readObject和writeObject,则会通过反射进行序列化。
在调用顺序中,writeReplace会优先于writeObject调用;readResolve会在readObject之后调用。
如果想要对某个字段做特殊处理,则可以在writeReplace和readResolve中进行。即在序列化之前,和反序列化之后。
如果重写了writeReplace方法,则会在ObjectOutputStream中的writeObject0方法中先调用writeReplace方法。
在ObjectOutputStream类中的writeSerialData方法,会先判断是否重写了writeObject方法。
而且writeObject0方法会调用writeSerialData方法,所以writeReplace会优先于writeObject方法。
readResolve方法的调用顺序其实类似。即在ObjectInputStream中的readOrdinaryObject方法中,会优先调用readSerialData方法,在readSerialData方法中会判断是否重写了readObject,如果重写了,则通过readObject进行反序列化,然后再readSerialData方法执行完成之后,在readOrdinaryObject中接着调用了readResolve方法对反序列化之后的结果进行处理。

6.Serializable接口实现序列化需要注意

(1)多引用写入

在默认情况下, 对于一个实例的多个引用,为了节省空间,只会写入一次,后面会追加几个字节代表某个实例的引用。
当一个引用对象已经被序列化,那么修改了这个引用对象之后,想要再次对这个引用对象进行序列化,有两种方式解决,一种就是调用ObjectOutputStream.reset()方法,然后再写;还有一种方案就是oos.writeUnshared(course);通过writeUnshared来进行第二次序列化。

(2)子类实现序列化,父类不实现序列化/ 对象引用

在readObject时抛出java.io.NotSerializableException异常。子类实现了Serializable序列化的时候,其父类也是需要实现的,或者父类有无参构造函数,否则就会报错。

(3)单例模式的序列化问题/反射问题

单例模式的序列化和反序列化,需要重写readResolve方法,返回单例对象。如果不重写,则会导致单例模式在序列化->反序列化后失败。
序列化会导致单例模式失效。
因为单例实现Serializable将单例序列化,则并不会是同一个对象,单例就不是单例了。而重写readResolve方法,直接将单例对象返回。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335