天天看點

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<P_Shop> CREATOR = new Creator<P_Shop>() {
        /**
         * 從序列化對象中建立原始對象
         */
        @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 extends Parcelable> 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<ClassLoader,HashMap<String,Parcelable.Creator<?> 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<String,Parcelable.Creator<?>> 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)總結

資料來自 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序列化