彻底理解Serializable和Parcelable

Serializable和Parcelable, 都可以用来做序列化,网上也有很多文章分析它们的优缺点,大部分的结论都是Serializable使用简单但是低效,Parcelable使用麻烦但是高效,em...,也对,但是总感觉缺了点意思,这篇文章带你彻底理解二者,拒绝知识盲区。

先抛出几个问题,带着问题我们一起探索。

  1. 什么是序列化和反序列化,为什么需要序列化?
  2. Java中Serializable的序列化是怎么实现的?
  3. Android中Parcelable的序列化是怎么实现的?
  4. 有哪些使用场景,实现方式怎么选?

em, 可以先思考一下这几个问题。
(5分钟之后...)

第一个问题:什么是序列化和反序列化?

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

这里有二个关键字,存储和传输,存储的场景比如对象的持久化,传输的场景比如将对象通过网络传输,然后在需要使用的时候,反序列化,重新创建对象。

第二个问题: Java中Serializable的序列化是怎么实现的?

要弄清楚这个问题,只能去JDK源码里面找答案了(这里基于JDK8)。不过现在,我想通过一个简单的序列化字符串的例子开始,先有个大概的印象。

序列化:(为了简单起见,只贴了关键代码,下面就不再赘述了)
//......
FileOutputStream fileOutputStream = new FileOutputStream(new File("string_file"));
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject("Hello World!");
objectOutputStream.flush();
//......
反序列化:
//......
FileInputStream fileInputStream = new FileInputStream(new File("string_file"));
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
String string = (String) objectInputStream.readObject();
//......

看一下生成的二进制string_file文件的内容。

ACED0005 74000C48 656C6C6F 20576F72 6C6421
//ACED 是一个stream header魔数,可以类比Java类字节码文件的 0xCAFEBABE 魔数
//0005 是stream header的版本号
//74 是字符串类型的标识,如果字符串长度小于0xFFFF,写入0x74,否则写入0x7C
//000C 是字符串的长度,我们写入的是“Hello world!”,12个字符
//48 656C6C6F 20576F72 6C6421,这一坨就是Hello world!字符串了

上面这个文件的结构还是比较简单的,通过魔数和版本号校验一下文件的合法性,然后就是通过(字段类型+长度+源数据)的规律,写入到文件中。
这里你可能已经有了个疑问,我们都知道如果标记了Serializable接口,一般都要求我们重写serialVersionUID字段(即使不明确指定,编译器也会帮我们根据类字段自动生成一个),我们的经验是,重写了该字段以后,即使类的结构发生变化,还是能序列化成功。但是我们生成的字节码文件中没有看到serialVersionUID?难道我们的经验错了?问题先丢在这里,后面我们再回来解答。

开始看ObjectOutputStream的实现

//ObjectOutputStream.java
 public ObjectOutputStream(OutputStream out) throws IOException {
        //...
        //构建一个输出流,后面会用来写文件
        bout = new BlockDataOutputStream(out);
        //...
        //写入stream header魔数和版本号
        writeStreamHeader();
        //...
 }
 //还是贴一下写入魔数的代码,
 protected void writeStreamHeader() throws IOException {
        bout.writeShort(STREAM_MAGIC);
        bout.writeShort(STREAM_VERSION);
 }

 //写入对象的方法
 public final void writeObject(Object obj) throws IOException {
        //...
        try {
            writeObject0(obj, false);
        } catch (IOException ex) {
            //...
        }
 }

 private void writeObject0(Object obj, boolean unshared)
        throws IOException {
        // ...省略已一坨代码
        if (obj instanceof String) {
            //我们例子里面写入就是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) {
            writeOrdinaryObject(obj, desc, unshared);
        } else {
            if (extendedDebugInfo) {
                throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
            } else {
              throw new NotSerializableException(cl.getName());
            }
        }   
 }

 private void writeString(String str, boolean unshared) throws IOException {
        handles.assign(unshared ? null : str);
        long utflen = bout.getUTFLength(str);
        if (utflen <= 0xFFFF) {
            bout.writeByte(TC_STRING);
            bout.writeUTF(str, utflen);
        } else {
            bout.writeByte(TC_LONGSTRING);
            bout.writeLongUTF(str, utflen);
        }
  }

上面的代码都比较简单,通过查看源码,我们看到JDK对String、Array、Enum、Serializable这几种类型,分别有一套序列化逻辑,我们再做一个实验,这一次我们写入一个自定义的Person类,类定义如下。

public class Person implements Serializable {

    //定义成有规律的数字,方便查看
    private static final long serialVersionUID = 0x87654321;

    private int age;
    private String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

}

//...
Person person = new Person(0x18, "Rose");
try {
      FileOutputStream fileOutputStream = new FileOutputStream(new File("person_test"));
      ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
      objectOutputStream.writeObject(person);
      objectOutputStream.flush();
} catch (FileNotFoundException e) {
      e.printStackTrace();
} catch (IOException e) {
      e.printStackTrace();
}

看下person_test文件的内容。

ACED0005 7372001C 636F6D2E 616C696F 75737761 6E672E62 696E6465 722E5065 72736F6E FFFFFFFF 87654321 

02000249 00036167 654C0004 6E616D65 7400124C 6A617661 2F6C616E 672F5374 72696E67 3B787000 00001874 

0004526F 7365

//ACED0005 魔数和版本号
//73  表示写入是一个Object对象
//72  new Class Descriptor
//001C 对象字段的总长度
//636F6D2E 616C696F 75737761 6E672E62 696E6465 722E5065 72736F6E "Person类完整类名"
//FFFFFFFF 87654321  这就是我们重写的serialVersionUID值啦!
//后面的一坨就是我们person对象的非transient成员变量,写的过程跟写person对象一模一样,当做对象来做递归写入

到这里我们可以回答上面的问题了,JDK对String的序列化做了优化,所有不用写入serialVersionUID标识,解答了上面的问题,所以不是我们的经验错了,而是了解的还不够全面。下面是JDK序列化的流程图


Serializable的序列化流程.png
通过上面的流程,我们大概能看出,之所以Serializable的性能不高,是因为它需要反射解析要序列化的对象生成ObjectStreamClass对象,但是使用起来确实很方便。

第三个问题:Android中Parcelable的序列化是怎么实现的?

先来看一下,上面的Person类实现Parcelable接口

public class Person implements Parcelable {

    public int age;
    public String name;


    protected Person(Parcel in) {
        age = in.readInt();
        name = in.readString();
    }

    /**
     * 返序列化的时候会被调用,注意,Parcel读写字段的顺序必须一致,
     */
    public static final Creator<Person> CREATOR = new Creator<Person>() {
        @Override
        public Person createFromParcel(Parcel in) {
            return new Person(in);
        }

        @Override
        public Person[] newArray(int size) {
            return new Person[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * 序列化的时候被调用,有哪些字段参与序列化由你决定
     * @param dest
     * @param flags
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(age);
        dest.writeString(name);
    }
}

我们重点关注的二个地方,一个是writeToParcel方法,一个是CREATOR对象,很容易联想到,前者在序列化的时候会被调用,方法参数里有一个Parcel对象dest,我们将需要序列化的字段逐个写入dest即可,而CREATOR对象是在反序列化的时候被调用,createFromParcel方法参数有一个Parcel对象in,我们只需要逐个从in中读取需要恢复的字段即可,这里要注意,读写的顺序要保持一致。

所有对Parcelable对象的所有操作都是Parcel这个类来处理的。看一下WriteInt和WriteString的实现。

/**
* Write an integer value into the parcel at the current dataPosition(),
* growing dataCapacity() if needed.
*/
public final void writeInt(int val) {
     nativeWriteInt(mNativePtr, val);
}

/**
* Called when writing a string to a parcel. Subclasses wanting to write a string
* must use {@link #writeStringNoHelper(String)} to avoid
* infinity recursive calls.
*/
public void writeString(Parcel p, String s) {
      nativeWriteString(p.mNativePtr, s);
}

最终写入的操作是C++实现的,最终的套路跟Serializable基本是一致的,将数据转为二进制写入,因为Parcel要求严格的按顺序读写,所以这里的数据类型和数据长度是不需要写入的,对比Serializable写入的数据量要少一些,更深入的研究感兴趣的同学可以自行研究,这里就不再去深入了。

我们继续最后一个问题。

4. 有哪些使用场景,实现方式怎么选?

我们日常用到的有二种场景。

  • 数据的持久化保存,这里主要是指保存到文件
  • Android页面间数据的传递
先看第一种情况,将数据保存到文件。

根据我们前面的分析,Serializable用到了大量的反射调用,还需要生成很多辅助对象,执行效率应该会比Parcelable低,到底真是情况是不是如我们所想呢?我们可以测试一下。

为了使结果尽可能的准确一些,我分别使用Serializable和Parcelable写文件100次,每次写1000个对象,运行时间取平均值。运行结果:Serializable平均每次写1000个对象的耗时大约30ms,Parcelable平均每次耗时大约4ms。

Parcelable的速度是有一点优势的,但是Serializable的性能也不是不能接受,毕竟Android实际项目中,一般也不会有这么高的IO并发需求。Serializable使用起来简便,能够自动将父类的可序列化字段一并序列化,所以这里该怎么选,见仁见智,但是使用的时候知道底层原理,会更自如一点,如果场景要求极致的性能可以使用Parcelable,一般的场景使用Serializable即可。

测试的代码地址在这里,很简单,给有需要的同学参考吧。测试代码

再看第二种情况,页面间的传值

Android页面间传值当然要用到Intent了,我们知道启动一个Activity是需要我们的Application跟ActivityManagerService(AMS)进行IPC的,那么Intent里面携带的信息就需要IPC传给AMS,看下Intent的实现

public class Intent implements Parcelable, Cloneable {
//...
//保存我们需要传递的数据的Bundle
private Bundle mExtras;
//...

public @NonNull Intent putExtra(String name, Parcelable value) {
     if (mExtras == null) {
        mExtras = new Bundle();
     }
     mExtras.putParcelable(name, value);
     return this;
}

public @NonNull Intent putExtra(String name, Serializable value) {
    if (mExtras == null) {
        mExtras = new Bundle();
    }
     mExtras.putSerializable(name, value);
     return this;
}

//Intent的writeToParcel方法
public void writeToParcel(Parcel out, int flags) {
     //...
    //写入bundle
    out.writeBundle(mExtras);
}

//Intent的CREATOR对象
public static final Parcelable.Creator<Intent> CREATOR
        = new Parcelable.Creator<Intent>() {
   public Intent createFromParcel(Parcel in) {
        return new Intent(in);
   }
   public Intent[] newArray(int size) {
       return new Intent[size];
   }
};

//BaseBundle.class, bundle写入的实现
 void writeToParcelInner(Parcel parcel, int flags) {

      //...
      int startPos = parcel.dataPosition();
      parcel.writeArrayMapInternal(map);
      int endPos = parcel.dataPosition();
      //...
}

//BaseBundle中写入ArrayMap的实现,重点关注writeValue
void writeArrayMapInternal(ArrayMap<String, Object> val) {
     int startPos;
     for (int i=0; i<N; i++) {
        if (DEBUG_ARRAY_MAP) startPos = dataPosition();
        writeString(val.keyAt(i));
        writeValue(val.valueAt(i));
        if (DEBUG_ARRAY_MAP) Log.d(TAG, "  Write #" + i + " "
                + (dataPosition()-startPos) + " bytes: key=0x"
                + Integer.toHexString(val.keyAt(i) != null ? val.keyAt(i).hashCode() : 0)
                + " " + val.keyAt(i));
     }
 }

public final void writeValue(Object v) {
       //...
       else if (v instanceof Serializable) {
           // Must be last
            writeInt(VAL_SERIALIZABLE);
                writeSerializable((Serializable) v);
       } else {
             throw new RuntimeException("Parcel: unable to marshal value " + v);
       }
}

//writeSerializable的实现
public final void writeSerializable(Serializable s) {
        if (s == null) {
            writeString(null);
            return;
        }
        String name = s.getClass().getName();
        writeString(name);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(s);
            oos.close();

            writeByteArray(baos.toByteArray());
        } catch (IOException ioe) {
            throw new RuntimeException("Parcelable encountered " +
                "IOException writing serializable object (name = " + name +
                ")", ioe);
        }
}

可以看到Intent本身实现了Parcelable接口,虽然我们可以在putExtra中添加实现了Serializable接口的对象,但是通过我们上面的扒源码发现,最终Parcel会将Serializable先序列化为字节数组,然后写入,所以这中间就进行了二次序列化,性能肯定比Parcelable要低很多。所以如果我们的场景是界间传值的话,Parcelable是首选,我们可以自行决定哪些需要字段需要序列化,效率和自由度都很高。

总结一下:

  • 数据本地持久化,推荐Serializable
  • 界面传值 推荐Parcelable
小彩蛋:

通过上面分析,Parcelable我们可以自由决定哪些字段参与序列化,那么Serializable可不可以呢,答案当然是可以,我们都知道可以用transient关键字来忽略一些不需要参与序列化的字段,而且Java还提供了writeObject和readObject二个方法,Serializable在序列化时,如果检测到我们的类重写了writeObject方法,就执行该方法来替代默认的序列化调用。JDK中有很多这样的类,比如ArrayList,HashMap,都是重写 了writeObject方法。

//HashMap.java
transient Node<K,V>[] table;

private void writeObject(java.io.ObjectOutputStream s)
     throws IOException {
     int buckets = capacity();
     // Write out the threshold, loadfactor, and any hidden stuff
     s.defaultWriteObject();
     s.writeInt(buckets);
     s.writeInt(size);
     internalWriteEntries(s);
}

// Called only from writeObject, to ensure compatible ordering.
void internalWriteEntries(java.io.ObjectOutputStream s) throws 
  IOException {
     Node<K,V>[] tab;
     if (size > 0 && (tab = table) != null) {
         for (int i = 0; i < tab.length; ++i) {
              for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                 s.writeObject(e.key);
                 s.writeObject(e.value);
              }
          }
     }
 }

HashMap将存储数据的Node数组添加了 transient修饰符,然后重写了writeObject方法,用一个双层循环将key和value写入ObjectOutputStream。
再进一步想一下,为什么HashMap要自定义序列化逻辑呢?我想可能的原因是,存储数据的数组table,一般都是不满的(因为HashMap的负载因子默认0.75,超过就会扩容),里面肯定会有很多null,如果是默认的序列化,这些null也会被被序列化,显然这些null是没有必要的做序列化的。
全文完!

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