Android 對象序列化之追求完美的 Serial

閃存
Android 存儲優化系列專題
  • SharedPreferences 系列

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

  • ContentProvider 系列(待更)

《Android 存儲選項之 ContentProvider 啟動過程源碼分析》
《Android 存儲選項之 ContentProvider 深入分析》

  • 對象序列化系列

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

  • 數據序列化系列(待更)

《Android 數據序列化之 JSON》
《Android 數據序列化之 Protocol Buffer 使用》
《Android 數據序列化之 Protocol Buffer 源碼分析》

  • SQLite 存儲系列

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


前言

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

對象序列化系列,已經分別為大家介紹了實現對象序列化的兩種不同方案:《Android 對象序列化之你不知道的 Serializable》和《Android 對象序列化之 Parcelable 取代 Serializable ?》。

先來回顧下它們各自都有哪些優缺點:

  1. Serializable

它是 Java 原生提供的序列化機制,在 Android 中也被廣泛使用。可以通過它實現對象持久化存儲,也可以通過 Bundle 傳遞 Serializable 的序列化數據。

Serializable 的原理是通過 ObjectInputStream 和 ObjectOutputStream 來實現,整個序列化過程使用了大量的反射和臨時變量,而且還需要遞歸序列化對象引用的其它對象。可以說整個過程計算非常復雜,而且因為存在大量反射和 GC 的影響,序列化的性能會比較差。雖然 Serializable 性能那么差,但是它也有一些進階的使用技巧,如序列化加密、版本替換等。

  1. Parcelable

由于 Java 的 Serializable 的性能較低,Android 需要重新設計一套更加輕量且高效的對象序列化和反序列化機制。Parcelable 正是這個背景下產生的,它的核心作用就是為了解決 Android 中大量跨進程通信的性能問題

另外你可以發現 Parcelable 序列化和 Java 的 Serializable 序列化差別還是比較大的,Parcelable 只會在內存中進行序列化操作,并不會將數據存儲到磁盤里。

在時間開銷和使用成本的權衡上,Parcelable 與 Serializable 的選擇截然不同,Serializable 更注重使用成本,而 Parcalable 機制選擇的是性能優先。說道這里,如果你對它們還不熟悉的話,可以先去參考下。

那有沒有更好的方案來解決 Serializalbe 和 Parcleable 的痛點呢?今天我們就來了解一個開源且高性能的序列化方案。

Serial

作為程序員,我們肯定會追求完美。那有沒有性能更好的方案并且可以解決這些痛點呢?

事實上,關于序列化基本每個大公司都會有自己自研的一套方案,這里推薦 Twitter 開源的高性能序列化方案 Serial

Serial 力求幫助開發者實現高性能和高可控的序列化過程。這個序列化框架提供了一個名叫 Serializers 序列化器,開發者可以通過它來明確地定義對象的序列化過程。

Serial 集成

從 Maven Central 上下載最新的依賴

repositories {
    mavenCentral()
}

dependencies {
    compile 'com.twitter.serial:serial:0.1.6'
}
Serial 方案的主要優點如下:
  1. 相比起傳統的反射序列化方案更加高效(沒有使用反射)
  • 性能相比傳統方案提升了 3 倍(序列化的速度提升了 5 倍,反序列化提升了 2.5 倍)
  • 序列化生成的數據量(byte[])大約是之前的 1/5
  1. 開發者對于序列化過程的控制較強,可定義哪些 Object,Field 需要被序列化
  2. 有很強的 debug 能力,可以調試序列化的過程

那它是否真的是高性能呢?我們可以將它和前面的兩套方案做一個對比測試。

... 序列化時間/ms 反序列化時間/ms 文件大小/Byte
Serializable 162 79 427932
Parcelable 61 30 755564
Serial 57 18 357528
Serial 使用

Serializer 是本庫中的關鍵類,這個類提供了序列化和反序列化的能力,序列化的定義和流程都是通過它來實現的。

目前庫中默認提供的序列化實現類是 ByteBufferSerial,它的產物是 byte[]。使用者也可以自行更換實現類,不用拘泥于 byte[]。

定義 Serializer
  • 于之前的實現 Serializable 接口不同,這里需要給每個被序列化的對象單獨定義一個 Serializer。
  • Serializers 中需要給每個 field 明確定義 write 和 read 操作,對于有繼承關系的序列化類,需要被遞歸的進行定義
  • Serializers 已經為使用者處理了空對象問題,就像 read/writeString 一樣,記住不要使用原始的方法
  • Serializers 是無狀態的,所以我們可以將其寫為 Object 的內部類,并通過 SERIALIZER 作為名稱來訪問它。

對于大多數類,你可以建立一個繼承自 ObjectSerializer 的子類,然后實現 serializeObject 方法和 deserializeObject 方法:

public static class ExampleObject {

    public static final ObjectSerializer<ExampleObject> SERIALIZER = new ExampleObjectSerializer();

    public final int num;
    public final SubObject obj;

    public ExampleObject(int num, @NotNull SubObject obj) {
        this.num = num;
        this.obj = obj;
    }

    ...

    private static final class ExampleObjectSerializer extends ObjectSerializer<ExampleObject> {
        @Override
        protected void serializeObject(@NotNull SerializationContext context, @NotNull SerializerOutput output,
                @NotNull ExampleObject object) throws IOException {
            output
                .writeInt(object.num) // first field
                .writeObject(object.obj, SubObject.SERIALIZER); // second field
        }

        @Override
        @NotNull
        protected ExampleObject deserializeObject(@NotNull SerializationContext context, @NotNull SerializerInput input,
                int versionNumber) throws IOException, ClassNotFoundException {
            final int num = input.readInt(); // first field
            final SubObject obj = input.readObject(SubObject.SERIALIZER); // second field
            return new ExampleObject(num, obj);
        }
    }
}

將 ExampleObject 對象序列化為 byte[]

final Serial serial = new ByteBufferSerial();
final byte[] serializedData = serial.toByteArray(object, ExampleObject.SERIALIZER)

將對象從 byte[] 反序列化為 Object

final ExampleObject object = serial.fromByteArray(serializedData, ExampleObject.SERIALIZER)

在 ExampleObject 中內部類和 Parcelable.Creator 極為相似,都是按照順序對變量進行讀寫操作。接下來我們就從源碼的角度分析下 Serial 實現序列化的原理。在分析的過程中我們希望能夠更多的與 Android 系統提供的 Parcelable 做對比,看下 Serial 實現更加高效序列化機制都做了哪些優化?

源碼分析

Serial 序列化過程分析

從 ByteBufferSerial 的 toByteArray 方法開始:

serial.toByteArray(object, ExampleObject.SERIALIZER)

將 Objecdt 轉換成 byte[] ,ByteBufferSerial 的 toByteArray 方法:

@Override
@NotNull
public <T> byte[] toByteArray(@Nullable T value, @NotNull Serializer<T> serializer)
        throws IOException {
    if (value == null) {
        //空字節數組
        return InternalSerialUtils.EMPTY_BYTE_ARRAY;
    }
    final Pools.SynchronizedPool<byte[]> currentPool = mBufferPool;
    //獲取復用的byte[]
    final byte[] tempBuffer = currentPool != null ? currentPool.acquire() : null;
    //默認mBufferPool為null
    if (tempBuffer != null) {
        try {
            synchronized (tempBuffer) {
                return toByteArray(value, serializer, tempBuffer);
            }
        } finally {
            //回收該byte[]
            currentPool.release(tempBuffer);
        }
    }
    //默認mBufferPool為null,此時走這里
    return toByteArray(value, serializer, null);
}

ByteBufferSerial 內部可以指定一個 byte[] 的復用池 Pools.SynchronizedPool。有助于減少頻繁內存空間申請可能帶來的問題。

繼續向下分析,實際調用了 toByteArray 的重載方法:

@NotNull
public <T> byte[] toByteArray(@Nullable T value, @NotNull Serializer<T> serializer,
        @Nullable byte[] tempBuffer) throws IOException {
    if (value == null) {
        //空字節數組
        return InternalSerialUtils.EMPTY_BYTE_ARRAY;
    }
    //真正開始完成序列化的是ByteBufferSerializerOutput
    final ByteBufferSerializerOutput serializerOutput = new ByteBufferSerializerOutput(tempBuffer);
    try {
        //將ByteBufferSerializerOutput作為參數,
        //此時回到ExampleObjectSerializer的serializeObject方法
        serializer.serialize(mContext, serializerOutput, value);
    } catch (IOException e) {
        throw e;
    }
    //返回序列化后的字節數組
    //ByteBufferSerializerOutput內部是通過ByteBuffer完成序列化
    return serializerOutput.getSerializedData();
}

注意參數 serializer 就是在上面例子 ExampleObject 中定義的 ExampleObjectSerializer 實例,然后創建 ByteBufferSerializerOutput 實例作為 serialize 方法的參數:

try {
    //將ByteBufferSerializerOutput作為參數,
    //此時回到ExampleObjectSerializer的serializeObject方法
    serializer.serialize(mContext, serializerOutput, value);
} catch (IOException e) {
    throw e;
}

ExampleObjectSerializer 繼承自 ObjectSerializer,每個需要序列化的對象都需要為其自定義一個 ObjectSerializer,這也是 Serial 提供的序列化規則,實際與 Android 系統提供的 Parcelable.Creator 機制類似。

所以這里先調用了 ObjectSerialize 的 serialize 方法:

@Override
public final void serialize(@NotNull SerializationContext context,
        @NotNull SerializerOutput output, @Nullable T object) throws IOException {
    //object不為null
    if (!SerializationUtils.writeNullIndicator(output, object)) {
        //序列化版本管理
        if (context.isDebug()) {
            output.writeObjectStart(mVersionNumber, getClass().getSimpleName());
        } else {
            output.writeObjectStart(mVersionNumber);
        }
        //noinspection BlacklistedMethod
        //這里調用重寫了該方法ExampleObjectSerializer中
        serializeObject(context, output, object);
        output.writeObjectEnd();
    }
}

//serializeObject必須由子類實現
protected abstract void serializeObject(@NotNull SerializationContext context,
        @NotNull SerializerOutput output, @NotNull T object) throws IOException;

Serial 默認為我們提供了序列化版本管理,這有區別與 Android 的 Parcelable,我們可以通過該版本號實現序列化的版本兼容,后面會為大家介紹該部分內容。

serializeObject 方法開始真正自定義序列化過程,這個過程其實和 Parcelable 中的 Parcelable.Creator 極為相似,都是按照順序對變量進行讀寫,為了方便理解,可以和 Parcelable.Creator 做下類比:

    public static final Parcelable.Creator<Person> CREATOR = new Creator<Person>() {

    @Override
    public Person createFromParcel(Parcel source) {
        Person person = new Person();
        person.mName = source.readString();
        person.mSex = source.readString();
        person.mAge = source.readInt();
        return person;
    }

    //供反序列化本類數組時調用的方法
    @Override
    public Person[] newArray(int size) {
        return new Person[size];
    }
};

通過上面的分析我們知道 SerializerOutput 的實際類型是 ByteBufferSerializerOutput,這也是我們要重點分析的序列化實現過程:

ByteBufferSerializerOutput 中提供了一系列的 write 操作,我們先通過 output.writeInt 方法看下:

@Override
@NotNull
public ByteBufferSerializerOutput writeInt(int val) {
    //SerializerDefs.TYPE_INT byte類型,值為2
    writeIntHeader(SerializerDefs.TYPE_INT, val);
    return this;
}

從方法的返回其實可以看出 ByteBufferSerializerOutput 設計為構建者模式,可以更方便的操作。

繼續向下看 writeIntHeader 方法:

/**
 * int 類型劃分成4種方式存儲
 * 0 默認不寫入value,只寫入類型 defualt類型,默認值為0
 * 1~255,僅用一個字節表示,類型 byte
 * 256 ~ 65535 僅用2個字節表示,類型short
 * 大于65535 再用4個字節表示,類型int
 */
private void writeIntHeader(byte type, int val) {
    if (val == 0) {
        //ByteBufferSerializerDefs.SUBTYPE_DEFAULT)) 為byte類型值為1
        //makeHeader得到的值是:01000001
        writeHeader(ByteBufferSerializerDefs.makeHeader(type, ByteBufferSerializerDefs.SUBTYPE_DEFAULT));
    } else if ((val & 0xFFFFFF00) == 0) {
        //不超過一個字節的取值范圍 1~255
        //0x00000001~0x000000FF & 0XFFFFFF00 = 0 此時的值<=255(一個字節空間足以)
        //<= 255 
        writeHeader(ByteBufferSerializerDefs.makeHeader(type, ByteBufferSerializerDefs.SUBTYPE_BYTE));
        //一個字節空間,確保剩余空間
        ensureCapacity(ByteBufferSerializerDefs.SIZE_BYTE);
        mByteBuffer.put((byte) val);
    } else if ((val & 0xFFFF0000) == 0) {
        //不超過2個字節的取值范圍256~65535
        //2個字節的最大取值是65535
        writeHeader(ByteBufferSerializerDefs.makeHeader(type, ByteBufferSerializerDefs.SUBTYPE_SHORT));
        //2個字節,確保剩余空間
        ensureCapacity(ByteBufferSerializerDefs.SIZE_SHORT);
        //short 2個字節空間
        mByteBuffer.putShort((short) val);
    } else {
        //否則四個字節空間 2^32 - 1
        writeHeader(ByteBufferSerializerDefs.makeHeader(type, ByteBufferSerializerDefs.SUBTYPE_INT));
        //4個字節,確保剩余空間充足
        ensureCapacity(ByteBufferSerializerDefs.SIZE_INT);
        mByteBuffer.putInt(val);
    }
}

在 writeIntHeader 方法中,寫入 int 值按照取值范圍被劃分成 4 種類型:

  • 寫入值為 0
  • 寫入值在 1 ~ 255 之間(1個字節)
  • 寫入值在 256 ~ 65535 之間(2個字節)
  • 寫入值在 65536 ~ 2^32 -1(4個字節)

可以看出 Serial 根據具體寫入值范圍來優化字節存儲空間。

先來看下 writeHeader 方法:

private void writeHeader(byte headerType) {
    //確保剩余空間充足
    //擴容機制是原容量*2
    ensureCapacity(ByteBufferSerializerDefs.SIZE_BYTE);
    //表示當前寫入值的類型,default/byte/short/int 注意default將不再寫入value,直接默認值0
    mByteBuffer.put(headerType);
}

writeHeader 方法是為了確定當前寫入值的類型,用于在反序列化時根據數據類型讀取合適的字節空間。

ensureCapacity 方法是保證當前剩余空間大于要寫入字節數,默認擴容該機制是原容量 * 2。

/**
 * 這里單位是字節,如10字節
 */
private void ensureCapacity(int sizeNeeded) {
    //剩余空間小于需要的空間,單位字節
    if (mByteBuffer.remaining() < sizeNeeded) {
        //當前位置
        final int position = mByteBuffer.position();
        final byte[] bufferContents = mByteBuffer.array();
        //擴容原容量的2倍
        final byte[] newBufferContents = new byte[2 * mByteBuffer.capacity()];
        //將原來的字節數中拷貝擴容后的數組中
        System.arraycopy(bufferContents, 0, newBufferContents, 0, position);
        final ByteBuffer newBuffer = ByteBuffer.wrap(newBufferContents);
        newBuffer.position(position);
        mByteBuffer = newBuffer;
        //擔心仍然不夠
        ensureCapacity(sizeNeeded);
    }
}

重新回到 writeIntHeader 方法

  1. 寫入值范圍 val == 0

當寫入值 val == 0 時,我們看到 serial 只是寫入了一個字節的 Header,用以表示當前數據的類型。并沒有將 val 的值在寫入。

  1. 寫入值范圍:(val & 0xFFFFFF00) == 0

此時表示變量 val 的取值在 1 ~ 255(0 不會走到這里) 之間,僅用一個字節便可以表示。

  1. 寫入值范圍:(val & 0xFFFF0000) == 0

此時表示 val 的取值在 256 ~ 65535 之間,用兩個字節空間表示足以。

  1. 否則就是 4 個字節空間

不知道分析到這里,大家是否還記得 Parcelable 的序列化規則:在 Parcelable 中用于表示數據類型采用的是 int,相比起 Serial 要多占用 3 個字節空間,并且 Serial 根據寫入值的范圍進一步節約字節空間,這個優化其實非常有效,因為在實際的項目中用到的絕大多數整數值都相對較小(絕大多數 2 個字節空間足以)。

而這樣的優化在 Serial 中到處可見:

/**
 * long 被劃分成2種
 */
private void writeLongHeader(byte type, long val) {
    if ((val & 0xFFFFFFFF00000000L) == 0) {
        //實際這里又根據值劃分為4種,結合上面分析的writeIntHeader
        writeIntHeader(type, (int) val);
    } else {
        //否則8個字節
        writeHeader(ByteBufferSerializerDefs.makeHeader(type, ByteBufferSerializerDefs.SUBTYPE_LONG));
        ensureCapacity(ByteBufferSerializerDefs.SIZE_LONG);
        mByteBuffer.putLong(val);
    }
}

分析到這里其實我們也驗證了前面講到 Serial 主要優點之一的:序列化生成的數據量(byte[])大約是之前的 1/5。

Serial 在整個序列化過程中的確與 Parcelable 極其類似,都沒有像 Java 原生提供的 Serializable 那樣使用大量的反射和臨時變量(Serializable 機制選擇的是使用成本優先),Serial 和 Parcelable 在性能和使用成本的權衡上,都選擇了性能優先,這樣對于開發人員在使用上就復雜了很多。 Serial 在序列化的過程中對字節的控制更加高效。相比 Parcelable 更節省空間。

以上便是 Serial 整個序列化過程,接下來我們就分析下 Serial 的反序列化過程。

Serial 反序列化過程分析

還是結合上面給出的示例:

final ExampleObject object = serial.fromByteArray(serializedData, ExampleObject.SERIALIZER)

調用 ByteBufferSerial 的 fromByteArray 方法:

@Override
@Nullable
@Contract("null, _ -> null")
public <T> T fromByteArray(@Nullable byte[] bytes, @NotNull Serializer<T> serializer)
        throws IOException,
        ClassNotFoundException {
    if (bytes == null || bytes.length == 0) {
        //直接返回null
        return null;
    }
    //序列化是ByteBufferSerializerOutput
    //反序列化是ByteBufferSerializerInput
    final SerializerInput serializerInput = new ByteBufferSerializerInput(bytes);
    try {
        //將ByteBufferSerializerInput作為參數調用ObjectSerializer的deserialize方法
        //最終調用到自定義ExampleObjectSerializer的deserializeObject方法
        return serializer.deserialize(mContext, serializerInput);
    } catch (IOException | ClassNotFoundException | IllegalStateException e) {
        throw new SerializationException("Invalid serialized data:\n" +
                SerializationUtils.dumpSerializedData(bytes, serializerInput.getPosition(), mContext.isDebug()), e);
    }
}

序列化的操作者是 ByteBufferSerializerOutput,對應反序列化的操作者 ByteBufferSerializerInput。將其作為參數調用 ObjectSerializer 的 deserialize 方法。

@Nullable
@Override
public T deserialize(@NotNull SerializationContext context, @NotNull SerializerInput input)
        throws IOException, ClassNotFoundException {
    if (SerializationUtils.readNullIndicator(input)) {
        //如果byte[]為null,或無數據直接返回null
        return null;
    }
    //獲取序列化版本號
    final int deserializedVersionNumber = input.readObjectStart();
    if (deserializedVersionNumber > mVersionNumber) {
        //如果序列化版本號大于當前版本號則拋出異常
        throw new SerializationException("Version number found (" + deserializedVersionNumber + ") is " +
                "greater than the maximum supported value (" + mVersionNumber + ")");
    }
    //回到自定義的ExampleObjectSerializer的deserializeObject方法
    final T deserializedObject = deserializeObject(context, input, deserializedVersionNumber);
    input.readObjectEnd();
    return deserializedObject;
}

最后實際回到自定義 ExampleObjectSerializer 的 deserializeObject 方法中:

@Override
    @NotNull
    protected ExampleObject deserializeObject(@NotNull SerializationContext context, @NotNull SerializerInput input,
            int versionNumber) throws IOException, ClassNotFoundException {
        final int num = input.readInt(); // first field
        final SubObject obj = input.readObject(SubObject.SERIALIZER); // second field
        return new ExampleObject(num, obj);
    }

其實這個過程與序列化過程基本一致。只不過這里是從 byte[] 還原到 Object 過程。

分析到這里,大家是否還記得 Parcelable 的反序列過程:
雖然相比 Java 的 Serializable 減少使用反射提升整體序列化/反序列化性能。但是 Parcelable 在反序列化的過程中仍然使用到了少許反射:首次創建該 Parcelable 時,首先根據 name 通過 Classloader 加載返回 Class 對象,然后反射獲取其內部的 CREATOR 對象 ,然后在通過該 Creator 實例完成反序列化過程,當然后續系統會緩存該 Parcelable 的 Creator 實例。

Serial 則徹底沒有使用任何反射,其內部通過使用者手動傳入對應的 Serializer 序列化/反序列化器。正如 Serial 介紹其主要優點之一的:性能相比起傳統反射序列化方案更加高效(沒有使用反射),性能比傳統方案提升了 3 倍(序列化速度提升了 5 倍,反序列化提升了 2.5 倍)。

重新回到 deserializeObject 方法內 input.readInt 方法:

@Override
public int readInt() throws IOException {
    //SerializerDefs.TYPE_INT表示int類型
    return readIntHeader(SerializerDefs.TYPE_INT);
}

private int readIntHeader(byte expectedType) throws IOException {
    //讀取數據類型信息
    final byte subtype = readHeader(expectedType);
    return readIntValue(subtype);
}

readHeader 方法用于確定當前數據的類型,還記得序列化時的 writeHeader 方法嗎,此時通過寫入數據類型確定要讀取的字節數,重點看下 readIntValue 方法:

/**
 * 這里就是根據寫入時數據類型讀取相應的字節數
 */
private int readIntValue(byte subtype) throws IOException {
    try {
        if (subtype == ByteBufferSerializerDefs.SUBTYPE_DEFAULT) {
            //default默認就是0,在writeInt時,也是只寫了Header,并且沒有寫value
            return 0;
        } else if (subtype == ByteBufferSerializerDefs.SUBTYPE_BYTE) {
            //byte類型讀取一個字節
            //& 0xFF 的作用是:byte轉換成int,要進行補位(byte 8位,int 32位),補位高24為都為1
            //此時數值已經不一致了,通過& 0xFF運算保證高24位為0,低8位保持原樣
            return mByteBuffer.get() & 0xFF;
        } else if (subtype == ByteBufferSerializerDefs.SUBTYPE_SHORT) {
            //short類型讀取兩個字節
            //& 0xFFFF的作用參照上面0xFF,保證高16位為0,低16位保持原樣
            return mByteBuffer.getShort() & 0xFFFF;
        } else {
            //否則4個字節
            return mByteBuffer.getInt();
        }
    } catch (BufferUnderflowException ignore) {
        throw new EOFException();
    }
}

readIntValue 方法完全對應序列化時的 writeIntValue 劃分進行字節數讀取。

這里需要說的是:& 0xFF 的作用,當 byte 強轉為 int 時,由于 byte 占用 8 位,而 int 占用 32 位,此時需要補充高位的 24 位為 1,這樣補碼后的二進制位就不一致了,此時通過 & 0xFF 保證高 24 位為 0,低 8 位保持原樣,這就就保證了二進制數據的一致性。

小結

至此關于 Serial 的序列化/反序列化主線工作流程就分析完了,感興趣的朋友可以進一步深入源碼分析。

從 Serial 的實現原理上看,它與 Android 的 Parcelable 及其相似,不過相對 Parcelable 在寫入數據做了進一步的空間優化,而且整個過程沒有使用一點反射。

其他

較強的 Debug 能力

Serial 具有較強的 Debug 能力,可以調試序列化的過程 SerializationUtils

  • dumpSerialzedData 會根據序列化后的 byte[] 數據產生 String 類型的 Log。
public static String dumpSerializedData(@NotNull byte[] bytes) {
    return dumpSerializedData(bytes, -1, true);
}

@NotNull
public static String dumpSerializedData(@NotNull SerializerInput input, int position, boolean includeValues) {
    final StringBuilder builder = new StringBuilder().append('{').append(InternalSerialUtils.lineSeparator());
    try {
        int objectNesting = 0;
        String indentation = "    ";
        boolean addPositionMarker = position >= 0;
        byte type;
        while ((type = input.peekType()) != SerializerDefs.TYPE_EOF) {
            if (type == SerializerDefs.TYPE_END_OBJECT) {
                --objectNesting;
                if (objectNesting < 0) {
                    throw new SerializationException("Object end with no matching object start.");
                }
                indentation = InternalSerialUtils.repeat("    ", objectNesting + 1);
                input.readObjectEnd();
                builder.append(indentation).append('}');
            } else {
                builder.append(indentation);
                switch (type) {
                    case SerializerDefs.TYPE_BYTE: {
                        final byte b = input.readByte();
                        if (includeValues) {
                            builder.append("Byte: ").append(b);
                        } else {
                            builder.append("Byte");
                        }
                        break;
                    }
                    case SerializerDefs.TYPE_INT: {
                        final int i = input.readInt();
                        if (includeValues) {
                            builder.append("Integer: ").append(i);
                        } else {
                            builder.append("Integer");
                        }
                        break;
                    }
                    
                    // ... 省略
            }
}

Serial 最終會將序列化后的 byte[] 數據封裝進 StringBuilder 中進行輸出。

  • validataSerializedData 確保了序列化后的對象有有效的結構(比如每個對象都有開頭和結尾)

部分源碼:

private static void readStream(@NotNull SerializerInput input, boolean singleObject) throws IOException {
    int objectNesting = 0;
    byte type;
    while ((type = input.peekType()) != SerializerDefs.TYPE_EOF) {
        switch (type) {
            case SerializerDefs.TYPE_START_OBJECT:
            case SerializerDefs.TYPE_START_OBJECT_DEBUG: {
                input.readObjectStart();
                //TYPE_START_OBJECT 與 TYPE_END_OBJECT 成對出現
                ++objectNesting;
                break;
            }
            case SerializerDefs.TYPE_END_OBJECT: {
                --objectNesting;
                input.readObjectEnd();
                if (singleObject && objectNesting == 0) {
                    return;
                }
                if (objectNesting < 0) {
                    throw new SerializationException("Object end with no matching object start.");
                }
                break;
            }
            default: {
                throw new SerializationException("Unknown type: " + SerializerDefs.getTypeName(type) + '.');
            }
        }
    }
    if (objectNesting > 0) {
        throw new SerializationException("Object start with no matching object end.");
    }
}

Serial 的異常信息中會包含很多序列化失敗的原因,比如期望的類型和實際類型不匹配這種常見錯誤。

Serial 版本控制

如果我們在新版本 App 中添加或刪除了之前已經被序列化的對象的 Field,那么在反序列化老版本數據的時候可能會碰到一些問題。

Serial 為我們提供了幾種方案來處理這種情況:

  1. OptionalFieldException

還是結合上面的例子,在 ExampleObject 添加了一個新的字段,這時反序列化老版本 ExampleObject 就會出問題。Serializer 默認會依次讀取所有的 Field,此時拋出 OptionalFieldException 異常。

如果使用的是普通的 Serializer,我們可以通過 try-catch 來處理這個問題。

  • 比如想要給 ExampleObject 的最后增加一個叫 name 字段(原先的 ExampleObject 僅有 num 和 SubObject 這兩個字段)

此時我們必須向下面一樣修改deserializeObject 方法:

@Override
@NotNull
protected ExampleObject deserializeObject(@NotNull SerializationContext context, @NotNull SerializerInput input, int versionNumber) throws IOException, ClassNotFoundException {
    final int num = input.readInt();
    final SubObject obj = input.readObject(SubObject.SERIALIZER);
    final String name;
    try {
        name = input.readString();
    } catch (OptionalFieldException e) {
        name = DEFAULT_NAME; // 老版本中沒有這個字段,給它一個默認值
    }
    return new ExampleObject(num, obj, name);
}

對于 BuilderSerializer,只需要在 deserializeToBuilder 的最后添加 .setName(input.readString()) 即可。

  1. 版本號

還可以通過給 Serializer 添加一個版本號,這樣在反序列化的過程中就可以通過這個版本號進行復雜的處理了。添加版本號十分簡單,只需要在 SERIALIZER 的構造函數中傳入數字即可。

我們修改下上面的代碼,通過版本號來處理字段新老版本的問題:

final Serializer<ExampleObject> SERIALIZER = new ExampleObjectSerializer(1);

...

@Override
@NotNull
protected ExampleObject deserializeObject(@NotNull SerializationContext context, @NotNull SerializerInput input, int versionNumber) throws IOException, ClassNotFoundException {
    final int num = input.readInt();
    final SubObject obj = input.readObject(SubObject.SERIALIZER);
    final String name;
    if (versionNumber < 1) {
        name = DEFAULT_NAME;
    } else {
        name = input.readString();
    }
    return new ExampleObject(num, obj, name);
}

如果你刪除了序列化對象中部的某個 Field,比如 ExampleObject 中間的 SubObject。你可能需要用 SerializationUtils.skipObject() 來終止整個反序列化過程。如果已經把 SubObject 完全移除了,那么可以不用保留 SubObject 中的 Serializer 對象。

比如,你可能在新版本中刪除了 SubObject,而老版本的數據中含有這個對象,你可以進行下面的處理:

@Override
@NotNull
protected ExampleObject deserializeObject(@NotNull SerializationContext context, @NotNull SerializerInput input, int versionNumber)
        throws IOException, ClassNotFoundException {
    final int num = input.readInt();
    if (versionNumber < 1) {
        SerializationUtils.skipObject()
        name = DEFAULT_NAME;
    } else {
        name = input.readString();
    }
    return new ExampleObject(num, name);
}

另一種方法是調用 input.peekType,這個方法可以讓你在讀取 Object 對象前進行下一個參數的類型檢查,它提供了一個除判斷版本號之外的解決新老數據問題的方案。當你不愿意升級版本號或是不愿意擦除數據的時候,這個方法會十分有用。

需要注意的是:該方法僅僅適用于兩個對象類型不同的情況。因為這里 Object 是 SubObject,name 類型是 String,所以可以進行如下處理:

@Override
@NotNull
protected ExampleObject deserializeObject(@NotNull SerializationContext context, @NotNull SerializerInput input, int versionNumber) throws IOException, ClassNotFoundException {
    final int num = input.readInt();
    if (input.peekType() == SerializerDefs.TYPE_START_OBJECT) {
        SerializationUtils.skipObject();
        name = DEFAULT_NAME;
    } else {
        name = input.readString();
    }
    return new ExampleObject(num, name);
}

總結

不知大家有沒有注意到,Serial 從實現原理上看就像是把 Parcelable 和 Serializable 的優點集合在一起的方案。

  • 由于沒有使用反射,相比起傳統的反射序列話方案更加高效,具體你可以參考上面的測試數據。
  • 開發者對于序列化過程的控制較強,可定義哪些 Object、Field 需要被序列化。
  • 有很強的 Debug 能力,可以調試序列化的過程
  • 有很強的版本管理能力,可以通過版本號和 OptionalFieldException 做兼容。

總體看 Serial 這套對象序列化方案還不錯,但是對象的序列化要記錄的信息還是比較多,在操作比較頻繁時候,對應用的影響還是不少的,這個時候我們可以選擇使用數據的序列化。


以上便是個人在學習 Serial 時的心得和體會,文中分析如有不妥或更好的分析結果,還請大家指出!

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。