Android序列化(Serializable/Parcelable)总结

文章目录

    • 一、什么是序列化?为什么要序列化?怎么进行序列化?
    • 二、Serializable
      • 2.1 序列化举例
      • 2.2 重写readObject、writeObject、readResolve、writeReplace
      • 2.3 serialVersionUID
      • 2.4 实现原理
      • 2.5 Externalizable
    • 三、Parcelable
      • 3.1 序列化举例
      • 3.2 实现原理
    • 四、Parcelable、Serializable比较
      • 4.1 效率对比
      • 4.2 容错率对比
    • 五、总结
    • 六、参考

一、什么是序列化?为什么要序列化?怎么进行序列化?

序列化定义:将一个类对象转换成可存储、可传输状态的过程。序列化有两个过程:

1、序列化:将对象编码成字节流(serializing)
2、反序列化:从字节流编码中重新构建对象(deserializing)。对象序列化后,可以在进程内/进程间、网络间进行传输,也可以做本地持久化存储。

为什么要序列化: 系统底层并不认识对象,数据传输是以字节序列形式传递,以进程间通信为例,需要将对象转化为字节序列(字节序列中包括该对象的类型,成员信息等),然后在目标进程里通过反序列化字节序列,将字节序列转换成对象。

序列化方式:

  • Serializable(Java提供 后面简称为S)
  • Parcelable(Android特有 下面简称为P)

二、Serializable

S是Java API,是一个通用的序列化机制,可以将对象序列化并保存在本地或内存中。S是一个空接口:

public interface Serializable {}

S只起到了一个标识的作用,用于告知程序实现了Serializable的对象是可以被序列化的,但真正进行序列化和反序列化的操作是通过ObjectOutputStream及ObjectInputStream实现的。

2.1 序列化举例

S_Shop.java:

public class S_Shop implements Serializable {

    private static final long serialVersionUID = -1399695071515887643L;
    public String mShopName;
    public int mShopId;
    public String mShopPhone;
    public static int STATIC_VALUE = 100;//静态值
    public transient int TRANSIENT_VALUE;//被transient修饰 不能序列化

    @NonNull
    @Override
    public String toString() {
        return "Serializable: mShopName is " + mShopName
                + ",mShopId is " + mShopId
                + ",mShopPhone is " + mShopPhone
                + ",STATIC_VALUE is " + STATIC_VALUE
                + ",TRANSIENT_VALUE is " + TRANSIENT_VALUE;
    }
}

执行序列化和反序列化过程:

    public static void main(String[] args) throws IOException {
        //------------------Serializable------------------
        S_Shop shop = new S_Shop();
        shop.mShopName = "便利蜂";
        shop.mShopId = 2020;
        shop.mShopPhone = "18888888888";
        shop.TRANSIENT_VALUE = 1000;
      
        saveObject(shop); //序列化
        readObject();//反序列化
    }

    //序列化
    private static void saveObject(S_Shop shop) {
        ObjectOutputStream outputStream = null;
        try {
            outputStream = new ObjectOutputStream(new FileOutputStream("shop.obj"));
            outputStream.writeObject(shop);
            System.out.println("write-hashCode: " + shop.hashCode());
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static void readObject() {
        //反序列化
        ObjectInputStream inputStream = null;
        try {
            inputStream = new ObjectInputStream(new FileInputStream("shop.obj"));
            S_Shop shop = (S_Shop) inputStream.readObject();
            System.out.println(shop.toString());
            System.out.println("read-hashCode: " + shop.hashCode());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

执行结果:

Serializable: mShopName is 便利蜂,mShopId is 2020,mShopPhone is 18888888888,STATIC_VALUE is 100,TRANSIENT_VALUE is 0

结果看到反序列化成功,从序列化结构中又重新生成了对象,这里注意一点,类中的变量TRANSIENT_VALUE是由transient修饰的,不能被序列化,所以反序列化时得到的是默认值。另外STATIC_VALUE由static修饰,也不参与序列化过程。

2.2 重写readObject、writeObject、readResolve、writeReplace

一般来讲,只有当你自行设计的自定义序列化形式与默认的序列化形式基本相同时,才能接受默认的序列化形式。否则就要设计一个自定义的序列化形式,通过它合理地描述对象的状态。——《Effective Java》

Serializable实现自定义序列化必须重写readObject、writeObject方法,readResolve、writeReplace方法是可选的,看下面的例子:

public class S_Shop implements Serializable{

    private static final long serialVersionUID = -1399695071515887643L;
    public transient String mShopName;//注意mShopName是瞬态的
    public int mShopId;
    public String mShopPhone;

    /**
     * 序列化时执行 执行顺序早于writeObject 可以在此方法中做一些替换
     */
    private Object writeReplace() {
        System.out.println("-----writeReplace() start-----");
        S_Shop shop = new S_Shop();
        shop.mShopName = "物美超市";//将mShopName替换
        shop.mShopId = mShopId;
        shop.mShopPhone = mShopPhone;
        return shop;
    }

    /**
     * 序列化时执行 通过defaultWriteObject将非transient字段序列化 也可以自定义序列化字段
     */
    private void writeObject(ObjectOutputStream outputStream) throws IOException {
        System.out.println("-----writeObject() start-----");
        outputStream.defaultWriteObject();
        outputStream.writeObject(mShopName);
    }

    /**
     * 反序列化时执行 通过defaultReadObject将非transient字段反序列化 也可以将自定义字段反序列化
     */
    private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
        System.out.println("-----readObject() start-----");
        inputStream.defaultReadObject();
        mShopName = (String) inputStream.readObject();
    }

    /**
     * 反序列化时执行,执行顺序在readObject之后 可以在此方法中重新生成一个新对象
     */
    private Object readResolve() {
        System.out.println("-----readResolve() start-----");
        S_Shop shop = new S_Shop();
        shop.mShopName = mShopName;
        shop.mShopId = mShopId;
        shop.mShopPhone = "12345678";//将mShopPhone替换
        return shop;
    }

    @NonNull
    @Override
    public String toString() {
        return "Serializable: mShopName is " + mShopName
                + ",mShopId is " + mShopId
                + ",mShopPhone is " + mShopPhone;
    }
}

执行结果:

修改前:Serializable: mShopName is 便利蜂,mShopId is 2020,mShopPhone is 18888888888
-----writeReplace() start-----
-----writeObject() start-----
-----readObject() start-----
-----readResolve() start-----
修改后:Serializable: mShopName is 物美超市,mShopId is 2020,mShopPhone is 12345678

序列化过程的执行顺序:writeReplace->writeObject;反序列化过程的执行顺序:readObject->readResolve 通过上面四个方法,可以实现Serializable的自定义序列化。

:虽然上述的四个方法都是private级别的,但在反序列化过程中是通过反射执行的。

2.3 serialVersionUID

序列化会导致类的演变收到限制。这种限制与序列化唯一标识符serialVersionUID(后面简称sUID)有关,每个可序列化的类都有一个唯一标识号与它相关,sUID用来辅助序列化和反序列化的,序列化过程中会把类中的sUID写入序列化文件中。在反序列化时,检测序列化文件中sUID和当前类中的sUID是否一致,如果一致,才可以继续进行反序列化操作,否则说明序列化后类发生了一些改变,比如成员变量的类型发生改变等,此时是不能反序列化的。

是否需要指定serialVersionUID? 答案是肯定的,如果不指定sUID,在序列化时系统也会经过一个复杂运算过程,自动帮我们生成一个并写入序列化文件中。sUID的值受当前类名称、当前类实现的接口名称、以及所有公有、受保护的成员名称等所影响,此时即使当前类发生了微小的变化(如添加/删除一个不重要的方法)也会导致sUID改变,进而反序列化失败;如果指定了sUID,上述操作依然可以进行反序列化,但一些类结构发生改变,如类名改变、成员变量的类型发生了改变,此时即使sUID验证通过了,反序列化依然会失败。

2.4 实现原理

使用hexdump命令来查看上述生成的shop.obj二进制文件:

0000000 ac ed 00 05 73 72 00 1a 63 6f 6d 2e 65 78 61 6d
0000010 70 6c 65 2e 64 65 6d 6f 61 70 70 2e 53 5f 53 68
0000020 6f 70 ec 93 48 bf 94 6e 37 e5 02 00 03 49 00 07
0000030 6d 53 68 6f 70 49 64 4c 00 09 6d 53 68 6f 70 4e
0000040 61 6d 65 74 00 12 4c 6a 61 76 61 2f 6c 61 6e 67
0000050 2f 53 74 72 69 6e 67 3b 4c 00 0a 6d 53 68 6f 70
0000060 50 68 6f 6e 65 71 00 7e 00 01 78 70 00 00 07 e4
0000070 74 00 09 e4 be bf e5 88 a9 e8 9c 82 74 00 0b 31
0000080 38 38 38 38 38 38 38 38 38 38                  
000008a
  • AC ED: STREAM_MAGIC. 声明使用了序列化协议.
  • 00 05: STREAM_VERSION. 序列化协议版本.
  • 0x73: TC_OBJECT. 声明这是一个新的对象.
  • 0x72: TC_CLASSDESC. 声明这里开始一个新Class。
  • 00 1a: Class名字的长度.

序列化步骤:

  • 将对象实例相关的类元数据输出
  • 递归地输出类的超类描述直到不再有超类
  • 类元数据完了之后,开始从最顶层的超类开始输出对象实例的实际数据值
  • 从上至下递归输出实例的数据

Serializable 的序列化与反序列化分别通过 ObjectOutputStream 和 ObjectInputStream 进行,都是在Java层实现的。两个相关概念:
ObjectStreamClass: 序列化类的描述符。它包含类的名称和serialVersionUID。它由Java VM加载,可以使用lookup方法找到或创建。
ObjectStreamField: 类(可序列化)的可序列化字段的描述。ObjectStreamFields数组用于声明类的可序列化字段。

1、序列化过程(writeObject方法)

  • 通过ObjectStreamClass记录目标对象的类型、类名等信息,内部有个ObjectStreamFields数组,用来记录目标对象的内部变量(内部变量可以是基本类型,也可以是自定义类型,但是必须都支持序列化—必须是S不能是P)。

  • 首先通过ObjectStreamClass.lookup()找到或创建ObjectStreamClass,然后调用defaultWriteFields方法,在方法中通过getPrimFieldValues()获取基本数据类型并赋值到primVals(byte[]类型)中,再通过getObjFieldValues()获取到自定义对象(通过Unsafe类实现而不是反射)并赋值到objVals(Object[]类型)中,接着遍历objVals数组,然后递归调用writeObject方法重复上述操作。

  • 调用过程:writeObject() -> writeObject0()-> writeOrdinaryObject() -> writeSerialData() -> invokeWriteObject() -> defaultWriteFields()

2、反序列化过程(readObject方法)

  • 通过readClassDescriptor()读取InputStream里的数据并初始化ObjectStreamClass类,再根据这个实例通过反射创建目标对象实例。

  • 调用过程:readObject() -> readObject0() -> readOrdinaryObject() -> readSerialData() -> defaultReadFields()

Serializable常见异常

异常名称 异常起因
java.io.InvalidClassException 1、序列化时自动生成serialVersionUID,此时改变类名、类实现的接口名、内部成员变化或添加/删除方法,都会导致serialVersionUID改变,反序列化时就会抛出此异常
java.io.NotSerializableException 当前类或类中成员变量未实现序列化

2.5 Externalizable

Externalizable 继承自Serializable,并在其基础上添加了两个方法:writeExternal()和readExternal()。这两个方法在序列化和反序列化时会被执行,从而可以实现一些特殊的需求(如指定哪些元素不参与序列化,作用等同于transient)。如果说默认的Serializable序列化方式是自动序列化,那么Externalizable就是手动序列化了,通过writeExternal()指定参与序列化的内部变量个数,然后通过readExternal()反序列化重新生成对象。

public class S_Shop_External implements Externalizable {

    private static final long serialVersionUID = -61368254488136487L;
    public String mShopName;
    public int mShopId;
    public String mShopPhone;
    
    public S_Shop_External() {
        System.out.println("S_Shop_External()构造方法");
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(mShopName);
        out.writeInt(mShopId);
        out.writeObject(mShopPhone);
    }

    @Override
    public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException {
        mShopName = (String) in.readObject();
        mShopId = in.readInt();
        mShopPhone = (String) in.readObject();
    }

    @NonNull
    @Override
    public String toString() {
        return "Serializable: mShopName is " + mShopName
                + ",mShopId is " + mShopId
                + ",mShopPhone is " + mShopPhone;
    }
}

执行代码跟Serializable一样,只是将对象变成了S_Shop_External,执行结果:

Externalizable: mShopName is 便利蜂,mShopId is 2020,mShopPhone is 18888888888

readExternal()中得到的数据都是在writeExternal()中写入的数据。

Externalizable常见异常

异常名称 异常起因
java.io.InvalidClassException: no valid constructor 反序列化时,必须要有修饰符为public的默认构造参数
java.io.OptionalDataException:readExternal 反序列化时readExternal添加元素、删除末尾之外的元素、修改元素类型

三、Parcelable

P是Android SDK API,其序列化操作完全由底层实现,可以在进程内、进程间(AIDL)高效传输数据。不同版本的API实现方式可能不同,不宜做本地持久化存储。

3.1 序列化举例

P_Shop.java:

public class P_Shop implements Parcelable {
    public P_Shop(){}
    public String mShopName;
    public int mShopId;
    public String mShopPhone;
    public static int STATIC_VALUE = 100;//静态值
    public transient int TRANSIENT_VALUE;//被transient修饰 不能序列化

    /**
     * 从序列化结构中创建原始对象
     */
    protected P_Shop(Parcel in) {
        mShopName = in.readString();
        mShopId = in.readInt();
        mShopPhone = in.readString();
    }

    /**
     * 反序列化
     */
    public static final Creator CREATOR = new Creator() {
        /**
         * 从序列化对象中创建原始对象
         */
        @Override
        public P_Shop createFromParcel(Parcel in) {
            return new P_Shop(in);
        }

        /**
         * 创建指定长度的原始对象数组
         */
        @Override
        public P_Shop[] newArray(int size) {
            return new P_Shop[size];
        }
    };

    /**
     * 序列化:将当前对象写入序列化结构中
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mShopName);
        dest.writeInt(mShopId);
        dest.writeString(mShopPhone);
    }

    /**
     * 当前对象的内容描述,存在文件描述符时返回1  其余全返回0
     */
    @Override
    public int describeContents() {
        return 0;
    }

    @NonNull
    @Override
    public String toString() {
        return "Parcelable: mShopName is " + mShopName
                + ",mShopId is " + mShopId
                + ",mShopPhone is " + mShopPhone
                + ",STATIC_VALUE is " + STATIC_VALUE
                + ",TRANSIENT_VALUE is " + TRANSIENT_VALUE;
    }
}

注意:createFromParcel()和writeToParcel()方法中对应变量读写的顺序必须是一致的,否则序列化会失败。

Parcel处理工具:

public class PUtil {

    private static final String SP_NAME = "sp_parcel";
    private static final String PARCEL_KEY = "parcel_key";

    //marshall Parcel将自身保存的数据以byte数组形式返回
    public static byte[] marshall(Parcelable parcelable) {
        Parcel parcel = Parcel.obtain();
        parcel.setDataPosition(0);
        parcel.writeValue(parcelable);
        byte[] bytes = parcel.marshall();
        parcel.recycle();
        return bytes;
    }

    //将bytes经过base64转换成字符串并存储到sp中
    public static void save(Context context, byte[] bytes) {
        SharedPreferences preferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = preferences.edit();
        String saveStr = Base64.encodeToString(bytes, 0);
        editor.putString(PARCEL_KEY, saveStr);
        editor.apply();
    }

    //从sp中取出字符串并转换成bytes 然后bytes->Parcel->Object
    public static Object getParcel(Context context) {
        SharedPreferences preferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
        byte[] bytes = Base64.decode(preferences
                .getString(PARCEL_KEY, "").getBytes(), Base64.DEFAULT);
        //从bytes中获取Parcel
        Parcel parcel = unmarshall(bytes);
        return parcel.readValue(context.getClassLoader());
    }

    //从byte数组中获取数据,存入自身的Parcel中
    private static Parcel unmarshall(byte[] bytes) {
        Parcel parcel = Parcel.obtain();
        parcel.unmarshall(bytes, 0, bytes.length);
        parcel.setDataPosition(0);
        return parcel;
    }
}

执行序列化/反序列化:

        //------------------Parcelable------------------
        Context context = this;
        P_Shop shopP = new P_Shop();
        shopP.mShopName = "便利蜂";
        shopP.mShopId = 2020;
        shopP.mShopPhone = "18888888888";
        shopP.TRANSIENT_VALUE = 1000;

        //序列化过程
        byte[] bytes = PUtil.marshall(shopP);//Parcel->bytes[]
        PUtil.save(context, bytes);//保存bytes[]
        //反序列化过程
        Object object = PUtil.getParcel(context);//bytes[]->Parcel->Object
        if (object == null) return;
        if (object instanceof P_Shop) {
            P_Shop shop = (P_Shop) object;
            Log.e("TTT", shop.toString());
        }

执行结果:

Parcelable: mShopName is 便利蜂,mShopId is 2020,mShopPhone is 18888888888,STATIC_VALUE is 100,TRANSIENT_VALUE is 0

3.2 实现原理

P序列化过程中会用到Parcel,Parcel可以被认为是一个包含数据或者对象引用的容器,能够支持序列化及在跨进程之后的反序列化。P的序列化操作在Native层实现,通过write内存写入及read读内存数据重新生成对象。P将对象进行分解,且分解后每一部分都是支持可传递的数据类型。

序列化过程(Parcelable的写过程)

调用过程Parcel.writeValue()->writeParcelable(),下面主要来看下此方法:

 public final void writeParcelable(@Nullable Parcelable p, int parcelableFlags) {
        if (p == null) {
            writeString(null);
            return;
        }
        //1、先写入序列化类名
        writeParcelableCreator(p);
        //2、调用类中复写的writeToParcel方法按顺序写入
        p.writeToParcel(this, parcelableFlags);
    }
 //写入序列化类名
 public final void writeParcelableCreator(@NonNull Parcelable p) {
        String name = p.getClass().getName();
        writeString(name);
    }

序列化过程中,首先写入序列化类名,然后调用类中复写的writeToParcel()方法依次写入

反序列化过程(Parcelable的读过程)
调用过程:Pacel.readValue()->readParcelable()

    public final  T readParcelable(@Nullable ClassLoader loader) {
        //1、通过反射或缓存获取序列化类中的CREATOR
        Parcelable.Creator creator = readParcelableCreator(loader);
        if (creator == null) {
            return null;
        }
        if (creator instanceof Parcelable.ClassLoaderCreator) {
          Parcelable.ClassLoaderCreator classLoaderCreator =
              (Parcelable.ClassLoaderCreator) creator;
          return (T) classLoaderCreator.createFromParcel(this, loader);
        }
        //2、调用CREATOR中的createFromParcel进行反序列化
        return (T) creator.createFromParcel(this);
    }

    private static final HashMap mCreators = new HashMap<>();

    public final Parcelable.Creator readParcelableCreator(@Nullable ClassLoader loader) {
       //1、首先读取之前写入的类名
        String name = readString();
        if (name == null) {
            return null;
        }
        Parcelable.Creator creator;
        synchronized (mCreators) {
            //如果之前某个classLoader缓存过Parcelable的Creator,然后通过mCreators缓存过,
            //那么直接从缓存取;否则通过反射去加载并加入缓存中
            HashMap> map = mCreators.get(loader);
            if (map == null) {
                map = new HashMap<>();
                mCreators.put(loader, map);
            }
            creator = map.get(name);
            if (creator == null) {
                try {
                    ClassLoader parcelableClassLoader =
                            (loader == null ? getClass().getClassLoader() : loader);
                      //通过反射去获取Parcelable中的CREATOR
                    Class parcelableClass = Class.forName(name, false /* initialize */,
                            parcelableClassLoader);
                    Field f = parcelableClass.getField("CREATOR");
                    Class creatorType = f.getType();
                    creator = (Parcelable.Creator) f.get(null);
                }
                map.put(name, creator);
            }
        }
        return creator;
    }

四、Parcelable、Serializable比较

4.1 效率对比

S序列化和反序列化会经过大量的I/O操作,产生大量的临时变量引起GC;P是基于内存实现的封装和解封(marshalled& unmarshalled),效率比S快很多。

下面的测试来自非官方测试,通过Parcelable和Serializable分别执行序列化/反序列化过程,循环1000次取平均值,实验结果如下:

Android序列化(Serializable/Parcelable)总结_第1张图片

数据来自 parcelable-vs-serializable,实验结果对比Parcelable的效率比Serializable快10倍以上。

4.2 容错率对比

序列化到本地时,新版本字段改变对旧版本反序列化的影响

改变字段 默认的Serializable序列化方式 Externalizable Parcelable
增加字段 ✔️ 追加到末尾:✔️ 其他:❌
删除字段 ✔️ 删除末尾:✔️ 其他:❌
修改字段类型

总结:
Externalizable中,writeExternal参与序列化,readExternal参与的是反序列化。readExternal()中读入的元素一定是writeExternal()中写入过的,且读写的顺序、字段类型要一致。另外,readExternal中的元素可以少于writeExternal中的,但是注意少的元素一定是在末尾的元素(即不能删除前面的元素),否则反序列化就会失败。

对于Parcelable来说,如果新版本中修改字段类型,那么该字段反序列化时会失败;如果是添加字段,那么反序列化时在添加字段位置到末尾位置都会失败;同样删除字段,反序列化时在删除字段的位置到末尾位置都会失败。

五、总结

对比 Serializable Parcelable
所属API Java API Android SDK API
特点 序列化和反序列化会经过大量的I/O操作,产生大量的临时变量引起GC,且反序列化时需要反射 基于内存拷贝实现的封装和解封(marshalled& unmarshalled),序列化基于Native层实现,不同版本的API实现可能不同
开销 相对高 相对低
效率 相对低 相对高
适用场景 序列化到本地、网络传输 主要内存序列化

另外序列化过程中的几个注意点:

  • 下面两种成员变量不会参与到默认序列化过程中:
    1、static静态变量属于类而不属于对象
    2、transient标记的成员变量
  • 参与序列化的成员变量本身也是需要可序列化的
  • 反序列化时,非可序列化的(如被transient修饰)变量将会调用自身的无参构造函数重新创建,因此也要求此成员变量的构造函数必须是可访问的,否则会报错。

六、参考

【1】Android 面试(七):Serializable 这么牛逼,Parcelable 要你何用?
【2】每日一问 Parcelable 为什么效率高于 Serializable ?
【3】Android中两种序列化方式的比较Serializable和Parcelable
【4】Android之序列化详解
【5】Android 开发之漫漫长途 X——Android序列化

你可能感兴趣的:(Android本地存储)