Android 對象序列化之你不知道的 Serializable

閃存
Android 存儲優(yōu)化系列專題
  • SharedPreferences 系列

Android 之不要濫用 SharedPreferences
Android 之不要濫用 SharedPreferences(2)— 數(shù)據(jù)丟失

  • ContentProvider 系列(待更)

《Android 存儲選項之 ContentProvider 啟動存在的暗坑》
《Android 存儲選項之 ContentProvider 深入分析》

  • 對象序列化系列

Android 對象序列化之你不知道的 Serializable
Android 對象序列化之 Parcelable 取代 Serializable ?
Android 對象序列化之追求性能完美的 Serial

  • 數(shù)據(jù)序列化系列(待更)

《Android 數(shù)據(jù)序列化之 JSON》
《Android 數(shù)據(jù)序列化之 Protocol Buffer 使用》
《Android 數(shù)據(jù)序列化之 Protocol Buffer 源碼分析》

  • SQLite 存儲系列

Android 存儲選項之 SQLiteDatabase 創(chuàng)建過程源碼分析
Android 存儲選項之 SQLiteDatabase 源碼分析
數(shù)據(jù)庫連接池 SQLiteConnectionPool 源碼分析
SQLiteDatabase 啟用事務源碼分析
SQLite 數(shù)據(jù)庫 WAL 模式工作原理簡介
SQLite 數(shù)據(jù)庫鎖機制與事務簡介
SQLite 數(shù)據(jù)庫優(yōu)化那些事兒


前言

對象序列化系列,主要內容包括:Java 原生提供的 Serializable 渺贤,更加適合 Android 平臺輕量且高效的 Parcelable雏胃,以及追求性能完美的 Serial。該系列內容主要結合源碼的角度分析它們各自的優(yōu)缺點以及合適的使用場景志鞍。

今天先來聊聊 Java 原生提供的 Serializable 對象序列化機制瞭亮,本文不是與大家一起探討關于 Serializable 的具體使用,相信關于 Serializable 大家肯定也不會感到陌生固棚,Serializable 是 Java 原生的序列化機制统翩,在 Android 中也有被廣泛使用仙蚜。我們可以通過 Serializable 將對象持久化存儲,也可以通過 Bundle 傳遞 Serializable 的序列化數(shù)據(jù)厂汗。

Serializable 簡單實現(xiàn)背后包含著復雜的計算邏輯委粉,本文也將結合源碼角度深入分析 Serializable 背后實現(xiàn)原理。

序列化/反序列化
  • 什么是對象序列化

大家是否有思考過娶桦,什么是序列化贾节?應用程序中的對象存儲在內存中,如果此時我們想把對象存儲下來或者在網絡上傳輸衷畦,這個時候就需要用到對象的序列化和反序列化栗涂。

對象序列化就是把一個 Object 對象的所有信息表示成一個字節(jié)序列,這包括 Class 信息祈争、繼承關系斤程、訪問權限、變量類型以及數(shù)值信息等菩混。

數(shù)據(jù)存儲不一定就是將數(shù)據(jù)存放到磁盤中忿墅,比如放到內存中、通過網絡傳輸也可以算是存儲的一種形式墨吓∏蜇埃或者我們也可以把這個過程叫做對象或者數(shù)據(jù)的序列化。

Serializable 簡單使用

“好像我們只需要簡單實現(xiàn) Serializable 接口帖烘,它就可以完成序列化/反序列化工作了”亮曹, 那它是如何工作的呢?實際上 Serializalbe 這種簡單機制是通過犧牲掉執(zhí)行性能為代價換來的秘症,也就是在時間開銷和使用成本的權衡上照卦,Serializable 機制選擇是使用成本優(yōu)先。

public final class Person implements Serializable {

    private String name;
    private int age;
    private String like;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getLike() {
        return like;
    }

    public void setLike(String like) {
        this.like = like;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", like='" + like + '\'' +
                '}';
    }
}

使用 ObjectOutputStream 將 Person 對象序列化到磁盤:

public void testWritePersonObject() {
    Person person;
    ObjectOutputStream out = null;
    try {
        out = new ObjectOutputStream(openFileOutput("person", Context.MODE_PRIVATE));
        person = new Person();
        out.writeObject(person);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        IOUtil.close(out);
    }
}

整個使用過程是不是非常簡單乡摹?雖然 Serializable 使用真的非常簡單役耕,但是也有一些需要注意的事項。另外它還有一些進階的使用技巧聪廉。文中也會詳細介紹到瞬痘,并且最后也會給大家總結出該部分內容。

Serializable 背后實現(xiàn)原理
  • ObjectOutputStream 和 ObjectInputStream

不知道大家是否跟我一樣板熊,在最初使用 Serializable 時是否會有這樣的疑問:“我們只需要實現(xiàn)這個接口框全,好像什么都不用管了,那它是如何實現(xiàn)序列化的呢干签?”

實際上 Serializable 的實現(xiàn)原理分別在 ObjectOutputStream 和 ObjectInputStream 中(本文主要給大家介紹序列化過程)津辩,整個序列化過程使用了大量反射和臨時變量,而且在序列化對象的時候,不僅會序列化當前對象本身喘沿,還需要遞歸序列化對應引用的其他對象闸度。

先來看下 ObjectObjectStream 的構造方法:

    public ObjectOutputStream(OutputStream out) throws IOException {
    //檢查繼承權限
    verifySubclass();
    //構造一個BlockDataOutputStream用于向out寫入序列化數(shù)據(jù)
    bout = new BlockDataOutputStream(out);
    //構造一個大小為10,負載因子為3的HandleTable和ReplaceTable
    handles = new HandleTable(10, (float) 3.00);
    subs = new ReplaceTable(10, (float) 3.00);
    //有參構造方法默認為false,
    enableOverride = false;
    writeStreamHeader();
    //將緩存模式打開蚜印,寫入數(shù)據(jù)時先寫入緩沖區(qū)
    bout.setBlockDataMode(true);
    if (extendedDebugInfo) {
        debugInfoStack = new DebugTraceInfoStack();
    } else {
        debugInfoStack = null;
    }
}

我們從上面例子中 ObjectOutputStream writeObject 方法開始說起:

public final void writeObject(Object obj) throws IOException {
    if (enableOverride) {
        //這里判斷是否需要重寫writeObject()
        //如果使用無參構造方法創(chuàng)建莺禁,該變量默認為true,此時必須重寫writeObject()
        //此時實際需要重寫的方法為:writeObjectOverride晒哄,否則拋出異常
        writeObjectOverride(obj);
        return;
    }
    try {
        //執(zhí)行writeObject0()
        writeObject0(obj, false);
    } catch (IOException ex) {
        if (depth == 0) {
            try {
                writeFatalException(ex);
            } catch (IOException ex2) {}
        }
        throw ex;
    }
}

在 writeObject 方法中首先判斷當前是否重寫了 writeObject 方法睁宰,如果我們使用無參構造方法創(chuàng)建 ObjectOutStream 實例,此時必須重寫該方法(實際重寫的是 writeObjectOverride 方法)柒傻,否則會拋出異常。

接著跟蹤 writeObject0 方法:

private void writeObject0(Object obj, boolean unshared) throws IOException {
    boolean oldMode = bout.setBlockDataMode(false);
    depth++;
    try {
        int h;
        //處理以前寫過和不可替換的對象
        //如果obj為null,此時一定返回null
        if ((obj = subs.lookup(obj)) == null) {
            writeNull();
            return;
        } else if (!unshared && (h = handles.lookup(obj)) != -1) {
            writeHandle(h);
            return;
        }

        Object orig = obj;
        Class<?> cl = obj.getClass();
        ObjectStreamClass desc;

        Class repCl;
        //創(chuàng)建ObjectStreamClass實例
        //ObjectStreamClass表示序列化對象的class詳細信息
        desc = ObjectStreamClass.lookup(cl, true);

        if (desc.hasWriteReplaceMethod()
                && (obj = desc.invokeWriteReplace(obj)) != null
                && (repCl = obj.getClass()) != cl) {
            //如果實現(xiàn)了writeReplace方法替換掉原寫入對象 與
            //該方法返回對象不為null 與
            //返回對象類型不是當前序列化類型
            cl = repCl;
            //生成替換對象類型的ObjectStreamClass
            desc = ObjectStreamClass.lookup(cl, true);
        }

        if (enableReplace) {
            //如果通過enableReplaceObject()方法設置enablReplace
            //此時回調replaceObject较木,需要子類重寫红符,否則返回與上述obj一致。
            //這里是繼承ObjectOutputStream之后重寫了replaceObject方法
            Object rep = replaceObject(obj);
            if (rep != obj && rep != null) {
                cl = rep.getClass();
                desc = ObjectStreamClass.lookup(cl, true);
            }
            //重新賦值要序列化的對象
            obj = rep;
        }

        if (obj != orig) {
            //如果替換了原序列化對象
            subs.assign(orig, obj);
            if (obj == null) {
                writeNull();
                return;
            } else if (!unshared && (h = handles.lookup(obj)) != -1) {
                writeHandle(h);
                return;
            }
        }

        // remaining cases
        // BEGIN Android-changed
        if (obj instanceof Class) {
            writeClass((Class) obj, unshared);
        } else if (obj instanceof ObjectStreamClass) {
            writeClassDesc((ObjectStreamClass) obj, unshared);
        // END Android-changed
        } else 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) {
            //我們只看實現(xiàn) Serializable 序列化過程
            writeOrdinaryObject(obj, desc, unshared);
        } else {
            if (extendedDebugInfo) {
                throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
            } else {
                throw new NotSerializableException(cl.getName());
            }
        }
    } finally {
        depth--;
        bout.setBlockDataMode(oldMode);
    }
}

該方法中我們重點關心 desc = ObjectStreamClass.lookup 和
writeOrdinaryObject 兩個方法伐债。

首先看 ObjectStreamClass.lookup 方法:它返回 ObjectStreamClass 實例预侯,ObjectStreamClass 保存了要序列化對象的 Class 相關信息:Class 名稱、serialVersionUID峰锁、實現(xiàn)了 Serializable 還是 Externalizable 接口萎馅,是否自行實現(xiàn)了 writeObject 和 readObject 方法等內容。每個序列化對象類型對應一個 ObjectStreamClass 實例虹蒋。

static ObjectStreamClass lookup(Class<?> cl, boolean all) {
    if (!(all || Serializable.class.isAssignableFrom(cl))) {
        //all在ObjectOutStream中默認傳遞true
        //判斷對象類型是否實現(xiàn)了Serializable接口
        //如果all=false 與 未實現(xiàn)Serializable接口則直接return糜芳。
        return null;
    }
    //清除失去Class引用的對應ObjectStreamClass實例緩存
    //不過這在Android好像并不能生效,因為在Android JVM 中類是不可以被卸載的
    processQueue(Caches.localDescsQueue, Caches.localDescs);
    //將寫入對象類交給弱引用持有魄衅,WeakClassKey繼承自WeakReference
    WeakClassKey key = new WeakClassKey(cl, Caches.localDescsQueue);
    //根據(jù)類對象獲取是否存在緩存的EntryFutrue或ObjectStreamClass
    //這里可能大家會有疑問郭厌,每次new WeakClassKey() 作為key摇天?實際上它內部使用
    //Class作為hash值的計算
    Reference<?> ref = Caches.localDescs.get(key);
    Object entry = null;
    if (ref != null) {
        //如果引用不為null,獲取引用的實際類型
        //在Android中只要是之前緩存過聘萨,則該ref永遠不為null(Class 不可以被卸載)
        //獲取該ref引用的對象
        entry = ref.get();
    }
    EntryFuture future = null;
    //如果引用的對象已經被回收
    if (entry == null) {
        //創(chuàng)建EntryFuture华弓,并使用軟引用關聯(lián)族跛。
        EntryFuture newEntry = new EntryFuture();
        Reference<?> newRef = new SoftReference<>(newEntry);
        do {
            if (ref != null) {
                //此時實際的引用對象已經被回收了鞍爱,故ref引用就沒有用了
                //從緩存中刪除該鍵值對
                Caches.localDescs.remove(key, ref);
            }
            //localDescs是ConcurrentHashMap,
            //putIfAbsent()方法初肉,不存在該key就添加進去,返回null
            //如果存在就返回之前的Reference荆责。
            ref = Caches.localDescs.putIfAbsent(key, newRef);
            if (ref != null) {
                entry = ref.get();
            }
            //這里是要保證該key(Class)對應的Reference中持有的對象沒有被回收
        } while (ref != null && entry == null);

        if (entry == null) {
            //如果entry為null喻粹,此時future就是新創(chuàng)建的EntryFuture
            future = newEntry;
        }
    }
    if (entry instanceof ObjectStreamClass) {
        //如果拿到的是ObjectStreamClass,直接返回
        return (ObjectStreamClass) entry;
    }
    if (entry instanceof EntryFuture) {
        //如果拿到的是EntryFuture
        future = (EntryFuture) entry;
        if (future.getOwner() == Thread.currentThread()) {
            //如果創(chuàng)建這個EntryFuture線程就是當前線程
            //意思就是當前新創(chuàng)建的這個EntryFuture
            //這里實際判斷該future是否是剛創(chuàng)建的newEntry
            entry = null;
        } else {
            //否則就獲取之前的set的
            entry = future.get();
        }
    }
    if (entry == null) {
        try {
            //如果是新創(chuàng)建的future草巡,此時就創(chuàng)建新的ObjectStreamClass
            entry = new ObjectStreamClass(cl);
        } catch (Throwable th) {
            entry = th;
        }
        if (future.set(entry)) {
            //EntryFuture中沒有關聯(lián)過entry
            //關聯(lián),并緩存為當前key對應引用關系
            Caches.localDescs.put(key, new SoftReference<Object>(entry));
        } else {
            // nested lookup call already set future
            entry = future.get();
        }
    }

    //entr必須為是ObjectStreamClass,否則拋異常
    if (entry instanceof ObjectStreamClass) {
        return (ObjectStreamClass) entry;
    } else if (entry instanceof RuntimeException) {
        throw (RuntimeException) entry;
    } else if (entry instanceof Error) {
        throw (Error) entry;
    } else {
        throw new InternalError("unexpected entry: " + entry);
    }
}

lookup() 方法的第一個 if 語句就判斷了要序列化的類是否實現(xiàn)了 Serializable 接口山憨,否則返回 null查乒,此時回到 ObjectOutputStream 的 writeObject0() 方法將會出現(xiàn)異常,這里也證明要序列化對象必須實現(xiàn) Serializable 接口郁竟。

lookup 整個方法看起來有些繁瑣玛迄,實際是從緩沖中獲取該序列化類型對應的 ObjectStreamClass 對象,ObjectStreamClass 的引入一方面是為了代碼解耦棚亩,另一個重要方面是提高獲取類信息的速度(ObjectStreamClass 保存了當前序列化類的相關信息)蓖议,系統(tǒng)通過 SoftReference 持有 Class 和 ObjectStreamClass 的映射關系。如果反復對一個類的實例進行序列化操作讥蟆,能夠有效減少耗時勒虾,提高緩存命中率。

源碼中有幾個需要說明的地方:

  • WeakClassKey 繼承自 WeakReference瘸彤,這里持有要序列對象的 Class修然,不過正如在注釋中所述:這在 Android 中好像并不能生效,因為在 Android 平臺 Java 虛擬機中质况,類對象是不可以被回收的(這也是出于性能的考慮)愕宋。

  • localDescs 是一個 ConcurrentHashMap 對象,源碼中可以看到每次 new WeakClassKey() 對象作為 key 獲取是否已經緩存過對應的 ObjectStreamClass 實例结榄?實際上 WeakClassKey 重寫了 hashCode() 方法中贝,故這里采用的是 Class 對象來計算的 Hash 值。

  • 源碼中的 do {} while() 作用是保證該 Class 對象能夠正常獲取到對應的 ObjectStreamClass 對象臼朗。

  • EntryFuture 是 ObjectStreamClass 的內部類邻寿,引入它的作用我個人認為是多線程調用 lookup() 方法而設立的。主要體現(xiàn)在判斷:if(future.getOwner() == Thread.currentThread()) 如果成立說明此時 EntryFuture 就是剛剛創(chuàng)建的依溯。否則是從緩存中獲取到的(但不代表就是其它線程創(chuàng)建的)老厌。

跟蹤到這里有必要進一步分析下 ObjectStreamClass 這個類,看下它的構造方法:

private ObjectStreamClass(final Class<?> cl) {
    this.cl = cl;
    //序列化對象的類名稱
    name = cl.getName();
    //判斷是否是代理類
    isProxy = Proxy.isProxyClass(cl);
    //是否是Enum
    isEnum = Enum.class.isAssignableFrom(cl);
    //是否實現(xiàn)了Serialable接口
    serializable = Serializable.class.isAssignableFrom(cl);
    //是否實現(xiàn)了Externalizable接口
    externalizable = Externalizable.class.isAssignableFrom(cl);
    //獲取其父類
    Class<?> superCl = cl.getSuperclass();
    //與之前分析的lookup()方法一致黎炉,這里會遞歸調用每個父類枝秤,
    //因為lookup()又會創(chuàng)建對應類的ObjectStramClass
    //superDesc表示其父類的ObjectStreamClass
    superDesc = (superCl != null) ? lookup(superCl, false) : null;
    //表示當前的ObjectStreamClass
    localDesc = this;

    if (serializable) {
        //如果實現(xiàn)了Serializable接口
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                if (isEnum) {
                    suid = Long.valueOf(0);
                    fields = NO_FIELDS;
                    //枚舉類型,此時沒有 Field
                    return null;
                }
                if (cl.isArray()) {
                    //數(shù)組類型
                    fields = NO_FIELDS;
                    return null;
                }
                //生成serialVersionUID
                //會去反射我們有沒有聲明serialVersionUID
                suid = getDeclaredSUID(cl);
                try {
                    //反射獲取該類所有需要被序列化的Field
                    //首先反射是否定義了serialPersistentFields慷嗜,如果存在則根據(jù)其內部聲明為準淀弹。
                    //否則直接反射獲取該類所有的字段(過濾static 和 transient)
                    fields = getSerialFields(cl);
                    computeFieldOffsets();
                } catch (InvalidClassException e) {
                    serializeEx = deserializeEx =
                            new ExceptionInfo(e.classname, e.getMessage());
                    fields = NO_FIELDS;
                }

                if (externalizable) {
                    //如果實現(xiàn)了Enternalizable接口,獲取其構造方法
                    cons = getExternalizableConstructor(cl);
                } else {
                    //獲取構造方法
                    cons = getSerializableConstructor(cl);
                    //反射獲取是否實現(xiàn)了writeObject方法
                    writeObjectMethod = getPrivateMethod(cl, "writeObject",
                            //參數(shù)類是 ObjectOutputStream
                            new Class<?>[]{ObjectOutputStream.class},
                            Void.TYPE);
                    //反射獲取是否實現(xiàn)了readObject方法
                    readObjectMethod = getPrivateMethod(cl, "readObject",
                            new Class<?>[]{ObjectInputStream.class},
                            Void.TYPE);
                    //反射獲取是否實現(xiàn)了readObjectNoData方法
                    readObjectNoDataMethod = getPrivateMethod(
                            cl, "readObjectNoData", null, Void.TYPE);
                    hasWriteObjectData = (writeObjectMethod != null);
                }
                //反射獲取是否實現(xiàn)了writeReplace
                writeReplaceMethod = getInheritableMethod(
                        cl, "writeReplace", null, Object.class);
                //反射獲取是否實現(xiàn)了readResolve
                readResolveMethod = getInheritableMethod(
                        cl, "readResolve", null, Object.class);
                return null;
            }
        });
    } else {
        suid = Long.valueOf(0);
        fields = NO_FIELDS;
    }

    // ... 省略
}

ObjectStreamClass 類的構造方法做的事情還是蠻多的:獲取序列化類的相關信息庆械、一系列類型檢查薇溃、創(chuàng)建其父類的 ObjecStreamClass 實例、通過反射獲取當前類的所有字段信息缭乘、反射獲取是否自行實現(xiàn)了 writeObject()沐序、readObject() 、writeReplace()、readResolve() 等方法策幼。

下面我們就幾個關鍵方法做進一步說明:

  • suid = getDeclaredSUID(cl) 獲取 serialVersionUID

系統(tǒng)通過反射的方式判斷我們是否有聲明 serialVersionUID:

private static Long getDeclaredSUID(Class<?> cl) {
    try {
        //通過反射獲取是否定義了serialVersionUID
        Field f = cl.getDeclaredField("serialVersionUID");
        int mask = Modifier.STATIC | Modifier.FINAL;
        if ((f.getModifiers() & mask) == mask) {
            //必須static final聲明
            f.setAccessible(true);
            //必須用static聲明邑时,否則獲取不到
            return Long.valueOf(f.getLong(null));
        }
    } catch (Exception ex) {
    }
    return null;
}

從這里我們可以看出自定義 serialVersionUID 必須聲明為 static final。

默認 serialVersionUID 計算規(guī)則在 ObjectStreamClass computeDefaultSUID 方法特姐。序列化使用一個 hash晶丘,該 hash 是根據(jù)給定源文件中幾乎所有東西:方法名稱、字段名稱唐含、字段類型浅浮、訪問修改方法等-計算出來的,序列化將該 hash 值與序列化流中的 hash 值比較捷枯。

為了使 Java 運行時相信兩種類型實際上是一樣的滚秩,第二版和隨后版本的 Person 必須與第一版有相同的序列化版本 hash(存儲為 private static final serialVersionUID 字段)。因此铜靶,我們需要 serialVersionUID 字段叔遂,它是通過對原始(或 V1)版本的 Person 類運行 JDK serialver 命令計算出的。

一旦有了 Person 的 serialVersionUID争剿,不僅可以從原始對象 Person 的序列化數(shù)據(jù)創(chuàng)建 PersonV2 對象(當出現(xiàn)新字段時已艰,新字段被設為缺省值,最常見的是“null”)蚕苇,還可以反過來做:即從 PersonV2 的數(shù)據(jù)通過反序列化得到 Person哩掺。

整個默認 serialVersionUID 計算流程非常繁瑣和復雜,通常我建議顯示聲明會更加穩(wěn)妥涩笤,因為隱士聲明假如類發(fā)生一點點變化嚼吞,進行反序列化都會由于 serialVersionUID 改變而導致 InvalidClassException 異常。

  • fields = getSerialFields(cl) 反射獲取當前類的所有字段

首先系統(tǒng)會判斷我們是否自行實現(xiàn)了字段序列化 serialPersistentFields 屬性蹬碧,否則走默認序列化流程舱禽,既忽律 static、transient 字段恩沽。

private static ObjectStreamField[] getSerialFields(Class<?> cl)
        throws InvalidClassException {
    ObjectStreamField[] fields;
    //必須實現(xiàn)了Serialiazble誊稚,并且不是代理類,不是Externalizable,不是接口類型
    if (Serializable.class.isAssignableFrom(cl)
            && !Externalizable.class.isAssignableFrom(cl)
            && !Proxy.isProxyClass(cl)
            && !cl.isInterface()) {

        //是否聲明serialPersistentFields的自定義字段序列化規(guī)則
        if ((fields = getDeclaredSerialFields(cl)) == null) {
            //默認序列化字段規(guī)則
            fields = getDefaultSerialFields(cl);
        }
        Arrays.sort(fields);
    } else {
        fields = NO_FIELDS;
    }
    return fields;
}

我們先來看下默認序列化流程的字段獲取規(guī)則:

 private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
    //反射獲取當前類的所有字段
    Field[] clFields = cl.getDeclaredFields();
    ArrayList<ObjectStreamField> list = new ArrayList<>();
    //忽略 static 或 transient 字段
    int mask = Modifier.STATIC | Modifier.TRANSIENT;

    for (int i = 0; i < clFields.length; i++) {
        if ((clFields[i].getModifiers() & mask) == 0) {
            //非static罗心,非transient字段
            //將其封裝在ObjectStreamField中
            //ObjectStreamField是對Field封裝
            list.add(new ObjectStreamField(clFields[i], false, true));
        }
    }
    int size = list.size();
    return (size == 0) ? NO_FIELDS :
            list.toArray(new ObjectStreamField[size]);
}

可以看到系統(tǒng)默認過濾掉 static里伯、transient 聲明的字段,將相關字段 Field 包裝在 ObjectStreamField 渤闷,ObjectStreamField 是對 Field 的封裝疾瓮,簡單看下它的構造方法:

 ObjectStreamField(Field field, boolean unshared, boolean showType) {
    //當前字段
    this.field = field;
    //是否可以被序列化
    this.unshared = unshared;
    //字段名稱
    name = field.getName();
    //字段類型
    Class<?> ftype = field.getType();
    //是基本類型還是引用類型
    type = (showType || ftype.isPrimitive()) ? ftype : Object.class;
    //獲取字段類型的簽名
    signature = getClassSignature(ftype).intern();
}

那該如何理解 serialPersistentFields 自行實現(xiàn)字段序列化規(guī)則呢?我們還是通過上面的的示例來了解下:

  • serialPersistentFields 自行實現(xiàn)字段序列化規(guī)則

      public final class Person implements Serializable {
    
      private String name;
      private int age;
      private String like;
    
      private transient String features;
    
      private static final ObjectStreamField[] serialPersistentFields = {
              new ObjectStreamField("name", String.class),
              new ObjectStreamField("age", Integer.class),
              new ObjectStreamField("features", String.class)
      };
    

    }

對 Person 做了下簡單修改飒箭,添加 serialPersistentFields 字段狼电,并指定了要序列化的字段名稱蜒灰,前面有分析道 Serializable 默認序列化規(guī)則會忽略 static、transient 聲明的字段肩碟。此時我們看下自行實現(xiàn)字段序列化規(guī)則卷员,看下它是如何實現(xiàn)的:

private static ObjectStreamField[] getDeclaredSerialFields(Class<?> cl)
        throws InvalidClassException {
    ObjectStreamField[] serialPersistentFields = null;
    try {
        //通過反射serialPersistentFields判斷是否聲明自定義字段序列化規(guī)則
        Field f = cl.getDeclaredField("serialPersistentFields");
        int mask = Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL;
        if ((f.getModifiers() & mask) == mask) {
            //必須聲明為private static final
            f.setAccessible(true);
            serialPersistentFields = (ObjectStreamField[]) f.get(null);
        }
    } catch (Exception ex) {
    }
    if (serialPersistentFields == null) {
        return null;
    } else if (serialPersistentFields.length == 0) {
        return NO_FIELDS;
    }

    //保存需要序列化的Field字段
    ObjectStreamField[] boundFields =
            new ObjectStreamField[serialPersistentFields.length];
    Set<String> fieldNames = new HashSet<>(serialPersistentFields.length);
    for (int i = 0; i < serialPersistentFields.length; i++) {
        ObjectStreamField spf = serialPersistentFields[i];
        //獲取字段名稱
        String fname = spf.getName();
        if (fieldNames.contains(fname)) {
            //不能存在重復的字段名
            throw new InvalidClassException(
                    "multiple serializable fields named " + fname);
        }
        //不允許存在重復的字段名稱
        fieldNames.add(fname);

        //大量反射
        try {
            //根據(jù)字段名稱獲取對應的Field
            Field f = cl.getDeclaredField(fname);
            //自定義傳遞字段類型必須與實際類型一致
            if ((f.getType() == spf.getType())
                    //不是static聲明字段
                    && ((f.getModifiers() & Modifier.STATIC) == 0)) {

                boundFields[i] =
                        new ObjectStreamField(f, spf.isUnshared(), true);
            }
        } catch (NoSuchFieldException ex) {
        }
        if (boundFields[i] == null) {
            //反射獲取第i個字段失敗
            boundFields[i] = new ObjectStreamField(
                    fname, spf.getType(), spf.isUnshared());
        }
    }
    return boundFields;
}

serialPersistentFields 屬性可以幫助我們替換默認字段序列化流程,ObjectStreamField 類型數(shù)組定義了需要被序列化的字段腾务,而且該規(guī)則將會替代默認字段序列化規(guī)則聲明,否則將不會被自動序列化削饵。

此時 transitent 聲明字段也可以被添加進序列化規(guī)則岩瘦,但是 static 聲明字段仍然不可以:

((f.getModifiers() & Modifier.STATIC) == 0)
  • 關于 serialPersistentFields 屬性簡單做下總結:
  1. serialPersistentFields 必須用 private static final 聲明;
  2. 該屬性會替換默認字段(非static窿撬、非transient)序列化規(guī)則启昧,此時需要序列化字段必須添加到屬性中;
  3. 被 transient 聲明字段也可以被添加進序列化中劈伴。

serialPersistentFields 相對來說提供的“支持”有限密末,比如我們我們要將相關敏感數(shù)據(jù)序列化之前先經過加密,反序列時自動對其解密跛璧,該如何更好的實現(xiàn)呢严里?writeObject() 和 readObject() 這兩個方法就可以幫我們一次性滿足。

  • writeObject() 和 readObject() 方法作用

還是通過上面的例子追城,添加 writeObject() 和 readObject() 自定義序列化/反序列流程:

public final class Person implements Serializable {

    private String name;
    private int age;
    private String like;

    private void writeObject(java.io.ObjectOutputStream stream)
            throws java.io.IOException {
        //對age字段序列化之前進行加密
        age = age << 2;
        //繼續(xù)使用默認序列化流程
        stream.defaultWriteObject();
        //也可以直接
        //stream.writeInt(age);
    }

    private void readObject(java.io.ObjectInputStream stream)
            throws java.io.IOException, ClassNotFoundException {
        stream.defaultReadObject();
        //也可以使用
        //stream.readInt();
        //反序列化后對其解密
        age = age << 2;
    }
}

通過自行實現(xiàn)序列化 writeObject 和 反序列化 readObject 方法替換默認流程刹碾,我們可以對某些字段做些特殊修改,也可以實現(xiàn)序列化的加密功能座柱。

如果此時我們序列化對象的類型發(fā)生了變化時該如何處理呢迷帜?這時我們可以通過 writeReplace 和 readResolve 方法實現(xiàn)對序列化的版本兼容。接下來我們就聊聊 wirteReplace 和 readResolve 方法的作用色洞。

  • writeReplace 和 readResolve 方法作用

還是通過上面的例子戏锹,做下改造進行說明:

    public final class Person implements Serializable {

    private String name;
    private int age;
    private String like;

    private Person spouse;

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

    private Object writeReplace()
            throws java.io.ObjectStreamException {
        return new PersonProxy(this);
    }

    public void setSpouse(Person value) {
        spouse = value;
    }

    public Person getSpouse() {
        return spouse;
    }

    //代理類
    private class PersonProxy implements Serializable {

        private String data;

        public PersonProxy(Person person) {
            this.data = person.name + "," + person.age + "," + person.like;

            final Person spouse = person.spouse;
            if (spouse != null) {
                data = data + "," + spouse.name + "," + spouse.age + "," + spouse.like;
            }
        }

        private Object readResolve()
                throws java.io.ObjectStreamException {
            String[] pieces = data.split(",");

            final Person result = new Person(pieces[0], Integer.parseInt(pieces[2]), pieces[1]);
            if (pieces.length > 3) {
                result.setSpouse(new Person(pieces[3], Integer.parseInt
                        (pieces[5]), pieces[4]));
                result.getSpouse().setSpouse(result);
            }
            return result;
        }
    }
}

writeReplace 和 readResolve 方法使 Person 類可以將它的所有數(shù)據(jù)(或其中的核心數(shù)據(jù))打包到一個 PersonProxy 中,將它放入到一個流中火诸,然后在反序列化時再進行解包锦针。

注意,PersonProxy 必須跟蹤 Person 的所有數(shù)據(jù)惭蹂。這通常意味著代理需要是 Person 的一個內部類伞插,以便能訪問 private 字段。有時候盾碗,代理還需要追蹤其他對象引用并手動序列化它們媚污,例如 Person 的 spouse。

這種技巧是少數(shù)幾種不需要讀/寫平衡的技巧之一廷雅。例如耗美,一個類被重構成另一種類型后的版本可以提供一個 readResolve 方法京髓,以便靜默地將被序列化的對象轉換成新類型。類似地商架,它可以采用 writeReplace 方法將舊類序列化成新版本堰怨。

  • Serializable 的序列化/反序列化的調用流程如下:

      //序列化
      E/test:SerializableTestData writeRepace
      E/test:SerializableTestData writeObject
      
      //反序列化
      E/test:Serializable readObject
      E/test:Serializable readResolve
    

分析到這里不知道你是否有感受到,Serializable 整個計算過程非常復雜蛇摸,而且因為存在大量反射和 GC 的影響备图,序列化的性能會比較差。另外一方面因為序列化文件需要包含的信息非常多赶袄,導致它的大小比 Class 文件本身還要大很多揽涮,這樣又會導致 I/O 讀寫上的性能問題。

重新回到 ObjectOutputStream writeObject0 方法中饿肺,接著向下分析(詳細方法體已在上面貼出)蒋困,在拿到 ObjectStreamClass 實例后,首先判斷是否有自行實現(xiàn)的 writeReplace敬辣、并且該方法返回不為 null 與不是當前對象類型:

        if (desc.hasWriteReplaceMethod()
                && (obj = desc.invokeWriteReplace(obj)) != null
                && (repCl = obj.getClass()) != cl) {
            //如果實現(xiàn)了writeReplace方法替換掉原寫入對象 與
            //該方法返回對象不為null 與
            //返回對象類型不是當前序列化類型
            cl = repCl;
            //生成替換對象類型的ObjectStreamClass
            desc = ObjectStreamClass.lookup(cl, true);
        }

這里大家可以結合上面對 ObjectStreamClass 構造方法和示例進行理解雪标。

接著最后 writeOrdinaryObject 方法(這里只看主線流程):

    private void writeOrdinaryObject(Object obj,
                                 ObjectStreamClass desc,
                                 boolean unshared) throws IOException {
    if (extendedDebugInfo) {
        debugInfoStack.push(
            (depth == 1 ? "root " : "") + "object (class \"" +
            obj.getClass().getName() + "\", " + obj.toString() + ")");
    }
    try {
        //檢查ObjectStreamClass對象
        desc.checkSerialize();
        //寫入字節(jié)0x73
        bout.writeByte(TC_OBJECT);
        //寫入class信息
        writeClassDesc(desc, false);
        handles.assign(unshared ? null : obj);
        if (desc.isExternalizable() && !desc.isProxy()) {
            //是否實現(xiàn)了Externalizable與不是代理類型
            writeExternalData((Externalizable) obj);
        } else {
            //寫入該對象變量信息及其父類的成員變量
            writeSerialData(obj, desc);
        }
    } finally {
        if (extendedDebugInfo) {
            debugInfoStack.pop();
        }
    }
}

checkSerialize 方法是檢查之前一系列流程是否發(fā)生過異常,否則將拋出異常溉跃。

這里我們重點跟蹤下 writeSerialData 方法:

   private void writeSerialData(Object obj, ObjectStreamClass desc)
    throws IOException {

    //獲取當前類以及其父類所有實現(xiàn)Serializable的ObectStreamClass
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
    
    for (int i = 0; i < slots.length; i++) {
        ObjectStreamClass slotDesc = slots[i].desc;
        //判斷是否重寫了writeObject()方法
        //還記得在ObjectStreamClass構造方法中判斷當前類中是否定義了writeObject方法村刨,
        //判斷writeObject method 是否為null
        if (slotDesc.hasWriteObjectMethod()) {
            PutFieldImpl oldPut = curPut;
            curPut = null;
            SerialCallbackContext oldContext = curContext;

            if (extendedDebugInfo) {
                debugInfoStack.push(
                    "custom writeObject data (class \"" +
                    slotDesc.getName() + "\")");
            }
            try {
                curContext = new SerialCallbackContext(obj, slotDesc);
                bout.setBlockDataMode(true);
                //反射調用重寫過的writeObject()方法
                slotDesc.invokeWriteObject(obj, this);
                bout.setBlockDataMode(false);
                bout.writeByte(TC_ENDBLOCKDATA);
            } finally {
                curContext.setUsed();
                curContext = oldContext;
                if (extendedDebugInfo) {
                    debugInfoStack.pop();
                }
            }

            curPut = oldPut;
        } else {
            //否則走默認寫入規(guī)則
            defaultWriteFields(obj, slotDesc);
        }
    }
}

注意看 for 循環(huán)遍歷所有的 ObjectStreamClass , 根據(jù)是否自行實現(xiàn)了 writeObject 方法走自行實現(xiàn)字段序列化規(guī)則還是走默認序列化規(guī)則 defaultWriteFields 方法喊积,關于自定義 writeObject 方法序列化規(guī)則烹困,上面通過示例做過分析。下面我們主要看下默認序列化規(guī)則 defaultWriteField 方法:

    private void defaultWriteFields(Object obj, ObjectStreamClass desc)
    throws IOException {
    Class<?> cl = desc.forClass();
    if (cl != null && obj != null && !cl.isInstance(obj)) {
        //obj不是cl類型實例
        throw new ClassCastException();
    }
    //檢查中間過程是否有非法信息
    desc.checkDefaultSerialize();

    int primDataSize = desc.getPrimDataSize();
    if (primVals == null || primVals.length < primDataSize) {
        primVals = new byte[primDataSize];
    }
    desc.getPrimFieldValues(obj, primVals);
    bout.write(primVals, 0, primDataSize, false);
    //獲取當前類的所有字段信息
    ObjectStreamField[] fields = desc.getFields(false);
    Object[] objVals = new Object[desc.getNumObjFields()];
    int numPrimFields = fields.length - objVals.length;
    //返回獲取每個Field的值乾吻,保存在objVals中
    //這里就是通過反射獲取到Field在當前obj中對應的值
    desc.getObjFieldValues(obj, objVals);
    for (int i = 0; i < objVals.length; i++) {
        if (extendedDebugInfo) {
            debugInfoStack.push(
                "field (class \"" + desc.getName() + "\", name: \"" +
                fields[numPrimFields + i].getName() + "\", type: \"" +
                fields[numPrimFields + i].getType() + "\")");
        }
        try {
            //根據(jù)實際類型調用
            //這里會遞歸調用writeObject0方法髓梅,
            //writeObject0方法的最后根據(jù)實際類型寫入
            //如果是引用類型,又會重復該過程
            writeObject0(objVals[i],
                         fields[numPrimFields + i].isUnshared());
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }
}

該類的所有字段已經保存在 ObjectStreamClass 中绎签,這里也是直接獲取到所有 Field枯饿,然后通過反射獲取到該 Field 在當前 obj 中的值,然后根據(jù)實際類型調用 writeObject0 方法诡必,注意如果是引用類型奢方,又會遞歸調用該過程。

    if (obj instanceof Class) {
        writeClass((Class) obj, unshared);
    } else if (obj instanceof ObjectStreamClass) {
        writeClassDesc((ObjectStreamClass) obj, unshared);
    // END Android-changed
    } else 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) {
        //我們只看實現(xiàn) Serializable 序列化過程
        writeOrdinaryObject(obj, desc, unshared);
    } else {
        if (extendedDebugInfo) {
            throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
        } else {
            throw new NotSerializableException(cl.getName());
        }
    }

以寫入 writeString 方法為例看下:

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);
    }
}

實際先寫入到 BlockDataOutputStream 緩存中(ObjectOutputStream 構造方法中創(chuàng)建爸舒,默認緩存大小是 1KB)蟋字,每當寫入內容超過該閾值:

public void write(int b) throws IOException {
        if (pos >= MAX_BLOCK_SIZE) {
            //超過最大最大可寫入此時要強制寫入
            //outputStream
            drain();
        }
        buf[pos++] = (byte) b;
}

void drain() throws IOException {
        if (pos == 0) {
            return;
        }
        if (blkmode) {
            writeBlockHeader(pos);
        }
        //寫入到outputStream中
        out.write(buf, 0, pos);
        pos = 0;
        warnIfClosed();
    }

此時調用的就是傳入 ObjectOutputStream 構造方法的輸出流了。

分析到這里扭勉,關于 Serializable 序列化背后的實現(xiàn)原理就已經分析完了鹊奖,當然文中還省去了大部分實現(xiàn)細節(jié),感興趣的朋友可以進一步閱讀源碼進行分析涂炎。

Java 提供的 Serializable 對象序列化機制忠聚,遠比大多數(shù) Java 開發(fā)人員想象的更靈活设哗,這使我們有更多的機會解決棘手的情況。其實像這樣的編程妙招在 JVM 中隨處可見两蟀。關鍵是我們要知道它們网梢,在后續(xù)的項目中可以更好的實踐它們。

總結

  • Serializable 序列化支持替代默認流程赂毯,它會先反射判斷是否存在我們自己實現(xiàn)的序列化方法 writeObject 或 反序列化方法 readObject战虏。通過這兩個方法,我們可以對某些字段做一些特殊修改党涕,也可以實現(xiàn)序列化的加密功能活烙。

  • 我們可以通過 writeReplace 和 readResolve 方法實現(xiàn)自定義返回的序列化實例。通過它們實現(xiàn)對序列化的版本兼容遣鼓,例如通過 readResolve 方法可以把老版本的序列化對象轉換成新版本的對象類型。

  • Serializable 整個序列化過程使用了大量的反射和臨時變量重贺,而且在序列化對象的時候骑祟,不僅會序列化當前對象本身,還需要遞歸序列化引用的其它對象气笙。

Serializable 使用注意事項
  • 不被序列化字段次企。類的 static 變量以及被聲明為 transient 的字段,默認的序列化機制都會忽略該字段潜圃,不會進行序列化存儲缸棵。當然我們也可以使用進階的 writeObject 和 readObject 方法做自定義的序列化存儲。

  • serialVersionUID谭期。在類實現(xiàn)了 Serializable 接口后堵第,我們需要添加一個 Serial Version ID,它相當于類的版本號隧出。這個 ID 我們可以顯示聲明也可以讓編譯器自己計算踏志。通常建議顯示聲明會更加穩(wěn)妥,因為隱士聲明假如類發(fā)生了一點點變化胀瞪,進行反序列化都會由于 serialVersionUID 改變而導致 InvalidClassException 異常针余。

  • 構造方法。Serializable 的反序列化默認是不會執(zhí)行構造函數(shù)的凄诞,它是根據(jù)數(shù)據(jù)流中對 Object 的描述信息創(chuàng)建對象的圆雁。如果一些邏輯依賴構造函數(shù),就可能會出現(xiàn)問題帆谍,例如一個靜態(tài)變量只在構造方法中賦值伪朽,當然我們也可以通過進階的自定義反序列化修改。

雖然 Serializalbe 性能那么差既忆,但是它也有一些進階的使用技巧驱负。不過在 Android 需要重新設計一套更加輕量且高效的機制嗦玖,感興趣可以繼續(xù)閱讀該系列下篇文章《Android 對象序列化之 Parcelable 取代 Serializable ?

推薦參考資料

以上便是個人在學習 Serializable 時相關心得和體會跃脊,文中如有不妥或有更好的分析結果宇挫,歡迎指出。

文章如果對你有幫助酪术,就請留個贊吧器瘪!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市绘雁,隨后出現(xiàn)的幾起案子橡疼,更是在濱河造成了極大的恐慌,老刑警劉巖庐舟,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件欣除,死亡現(xiàn)場離奇詭異,居然都是意外死亡挪略,警方通過查閱死者的電腦和手機历帚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來杠娱,“玉大人挽牢,你說我怎么就攤上這事√螅” “怎么了禽拔?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長室叉。 經常有香客問我睹栖,道長,這世上最難降的妖魔是什么茧痕? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任磨淌,我火速辦了婚禮,結果婚禮上凿渊,老公的妹妹穿的比我還像新娘梁只。我一直安慰自己,他們只是感情好埃脏,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布搪锣。 她就那樣靜靜地躺著,像睡著了一般彩掐。 火紅的嫁衣襯著肌膚如雪构舟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天堵幽,我揣著相機與錄音狗超,去河邊找鬼弹澎。 笑死,一個胖子當著我的面吹牛努咐,可吹牛的內容都是我干的苦蒿。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼渗稍,長吁一口氣:“原來是場噩夢啊……” “哼佩迟!你這毒婦竟也來了?” 一聲冷哼從身側響起竿屹,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤报强,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后拱燃,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體秉溉,經...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年碗誉,在試婚紗的時候發(fā)現(xiàn)自己被綠了坚嗜。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡诗充,死狀恐怖,靈堂內的尸體忽然破棺而出诱建,到底是詐尸還是另有隱情蝴蜓,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布俺猿,位于F島的核電站茎匠,受9級特大地震影響,放射性物質發(fā)生泄漏押袍。R本人自食惡果不足惜诵冒,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望谊惭。 院中可真熱鬧汽馋,春花似錦、人聲如沸圈盔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽驱敲。三九已至铁蹈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間众眨,已是汗流浹背握牧。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工容诬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人沿腰。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓览徒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親矫俺。 傳聞我的和親對象是個殘疾皇子吱殉,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354