前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >彻底理解Serializable和Parcelable

彻底理解Serializable和Parcelable

作者头像
三好码农
发布2019-03-15 10:48:50
1.1K0
发布2019-03-15 10:48:50
举报
文章被收录于专栏:三好码农的三亩自留地

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

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

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

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

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

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

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

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

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

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

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

代码语言:javascript
复制
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的实现

代码语言:javascript
复制
//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类,类定义如下。

代码语言:javascript
复制
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文件的内容。

代码语言:javascript
复制
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接口

代码语言:javascript
复制
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的实现。

代码语言:javascript
复制
/**
* 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的实现

代码语言:javascript
复制
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方法。

代码语言:javascript
复制
//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是没有必要的做序列化的。 全文完!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019.03.05 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 第一个问题:什么是序列化和反序列化?
  • 第二个问题: Java中Serializable的序列化是怎么实现的?
    • 序列化:(为了简单起见,只贴了关键代码,下面就不再赘述了)
      • 反序列化:
        • 通过上面的流程,我们大概能看出,之所以Serializable的性能不高,是因为它需要反射解析要序列化的对象生成ObjectStreamClass对象,但是使用起来确实很方便。
        • 第三个问题:Android中Parcelable的序列化是怎么实现的?
        • 4. 有哪些使用场景,实现方式怎么选?
          • 先看第一种情况,将数据保存到文件。
            • 再看第二种情况,页面间的传值
            • 总结一下:
              • 小彩蛋:
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档