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 时的心得和体会,文中分析如有不妥或更好的分析结果,还请大家指出!

文章如果对你有帮助,就请留个赞吧!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342