05 | Android 高级进阶(源码剖析篇) Twitter 的高性能序列化框架 Serial(一)

作者简介:ASCE1885, 《Android 高级进阶》作者。
本文由于潜在的商业目的,未经授权不开放全文转载许可,谢谢!
本文分析的源码版本已经 fork 到我的 Github

数据序列化在 Android 应用开发中占据着举足轻重的位置,无论是进程间通信,本地数据存储,网络数据传输等等,都离不开序列化的支持。针对不同场景选择正确的序列化方案,对应用的性能有着极大的影响。

广义上讲,序列化是将数据结构或者对象转换成可用于存储或者传输的数据格式的过程,在序列化期间,数据结构或者对象将其状态信息写入到临时或者持久性存储区中;反序列化是将序列化过程中生成的数据还原成数据结构或者对象的过程。Android 应用开发有很多种可选的序列化和反序列化方案,我们在《Android 高级进阶》一书的《Android 数据序列化方案研究》一节已经做过介绍。在正式开始 Serial 相关内容之前,这里我们先来简单回顾一下几个跟本文主题密切相关的序列化方式。

序列化基础

Serializable

Serializable 是 Java 语言的特性,它是最简单的也是使用最广泛的序列化方案之一,只有实现了 Serializable 接口的 Java 对象才可以实现内建的序列化,这种类型的序列化是将 Java 对象转换成字节序列的过程,而反序列化则是将字节序列恢复成 Java 对象的过程。

public interface Serializable {
}

Serializable 接口是一种标识接口,也就是无需实现方法,Java 便会对这个对象进行序列化操作。它的缺点是使用反射机制,在序列化的过程中会创建很多临时对象,容易触发垃圾回收,序列化的过程比较慢,对于性能要求很严格的场景不建议使用这种方案。

Externalizable

Externalizable 接口继承自 Serializable 接口,并定义了 writeExternalreadExternal 这两个方法,从而让开发者对序列化过程拥有更多的控制权,方便的实现自定义操作,同时可以实现一些使用 Serializable 接口无法实现的功能,例如实现 Serializable 接口的对象,其中 static 和 transient 类型的成员变量默认是不会被序列化的,而通过实现 Externalizable 接口开发者可以对 static 和 transient 类型的成员变量进行手动序列化的。

public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

当然,Externalizable 接口的序列化机制跟 Serializable 接口一样,都是基于反射机制的,性能方面也是比较差的。

Parcelable

前面两种序列化方式都是 Java 内置的接口,可以将对象序列化到磁盘文件,数据库,网络等。到了 Android 平台,为了实现 Android 应用内以及应用间(基于 AIDL)高效的数据传输,Android 设计了 Parcelable 接口,需要注意的是,Parcelable 只支持内存方式的序列化,不能将对象序列化到磁盘等介质。

public interface Parcelable {
    public static final int PARCELABLE_WRITE_RETURN_VALUE = 0x0001;
    public static final int CONTENTS_FILE_DESCRIPTOR = 0x0001;
    
    public int describeContents();
    public void writeToParcel(Parcel dest, int flags);
    public interface Creator<T> {
        public T createFromParcel(Parcel source);
        public T[] newArray(int size);
    }
    public interface ClassLoaderCreator<T> extends Creator<T> {
        public T createFromParcel(Parcel source, ClassLoader loader);
    }
}

因此,Serial 跟 Serializable(Externalizable)的关键区别是性能,跟 Parcelable 的关键区别是能够序列化到的介质。

Serial 的特性

Android 应用的流畅度是影响用户体验的关键性能指标之一,没有用户会喜欢使用起来卡顿的应用。具体到 Twitter 的 Android 端应用,经过性能剖析,他们的开发者发现,使用标准的 Android Externalizable 接口实现的 Java 对象和数据库之间的序列化和反序列化所花的时间占 UI 线程时间的 15% 左右,这使得 Twitter 时间轴滚动的流畅度受到对象序列化的严重影响。现有的对象序列化开源库对对象的迭代式修改提供的支持有限,任何可能破坏对象序列化的修改都会引入难以定位的 bugs,修复起来也非常困难,除非清除数据库数据。因此,Twitter 研发并于近期推出了 Serial,一个开源的基于 Java 平台的轻量级对象序列化框架(同时支持 Android 平台)。

Serial 解决了传统 Android 序列化函数库的以下四大痛点:

  • 性能:缓慢的序列化直接影响用户体验
  • 可调试性:当序列化后的数据中存在 bugs 时,调试信息不足以定位问题的原因
  • 向后兼容性:在不完全清除序列化后数据的前提下,Android 提供的序列化函数库几乎不支持对序列化对象进行修改,这使得对象属性的迭代升级变得困难
  • 灵活性:序列化函数库的使用应该对现有代码和模型类的数据结构侵入性小

虽然现有的一些 Java 序列化函数库像 KryoFlatbuffer 尝试解决上面一些痛点,但这些函数库重点在对性能和向后兼容性的支持上,通常忽略了对可调试性和灵活性的支持,例如为了在项目中使用这些函数库,通常需要对现有代码库作较大的改动。

使用 Java 的 Externalizable 类实现序列化时,类相关信息例如类名和包名,会被添加进序列化后生成的二进制数据中。这样,Java 平台才能根据这些信息在反序列化时通过反射机制还原这个对象,但同时这是一个非常耗时的操作。为了解决这个问题,Serial 通过规范开发者为每个需要序列化的对象实现 Serializer 内部类,并明确列举哪些属性需要序列化和反序列化,从而摒弃了反射机制和动态查找的使用。

使用 Serial 对一个大对象进行序列化和反序列化测试,和 Externalizable 方式相比,初步的性能指标如下所示:

  • 序列化速度提升 5 倍,反序列化速度提升 2.5 倍
  • 序列化后生成的数据字节大小约等于原来的 1/5

Serial 的基本用法

Serial 的使用很简单,首先我们为每个需要序列化的类定义一个 Serializer,它通常是以静态内部类的形式存在,并通过定义名为 SERIALIZER 的静态变量来访问它。Serializer 要求开发者显式的对序列化类中的属性进行读写,如果是原始数据类型,那么直接读写即可,如果是对象类型则递归调用该对象中定义的 Serializer 来读写。Serializer 在读写对象类型和字符串类型时能自动处理 null 对象。下面的例子中,我们在 ExampleObject 类中定义了一个继承自 ObjectSerializer 的 Serializer 内部类,并重写 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 ExampleObjectSerializer extends ObjectSerializer<ExampleObject> {
        // 自定义序列化操作
        @Override
        protected void serializeObject(@NotNull SerializerOutput output,
                @NotNull ExampleObject object) throws IOException {
            output.writeInt(object.num)
                .writeObject(object.obj, SubObject.SERIALIZER);
        }

        // 自定义反序列化操作
        @Override
        @NotNull
        protected ExampleObject deserializeObject(@NotNull SerializerInput input,
                int versionNumber) throws IOException, ClassNotFoundException {
            final int num = input.readInt();
            final SubObject obj = input.readObject(SubObject.SERIALIZER);
            return new ExampleObject(num, obj);
        }
    }
}

上面这个例子 ExampleObject 是通过构造方法初始化的,但有的时候,我们会使用 Builder 模式来实例化一个类(详情可以参见《Android 高级进阶》一书的《Builder 模式详解》一节);又或者在 Serial 中要实现对象的迭代式修改,这时候我们的 Serializer 类就不能继承 ObjectSerializer,而应该继承 BuilderSerializer 类,并重写 createBuilder,serializeObject 和 deserializeToBuilder 这三个方法。

public static class ExampleObject {
    ...

    public ExampleObject(@NotNull Builder builder) {
        this.num = builder.mNum;
        this.obj = builder.mObj;
    }

    ...

    public static Builder extends ModelBuilder<ExampleObject> {
        ...
    }

    private static final ExampleObjectSerializer extends BuilderSerializer<ExampleObject, Builder> {
        @Override
        @NotNull
        protected Builder createBuilder() {
            return new Builder();
        }

        @Override
        protected void serializeObject(@NotNull SerializerOutput output,
                @NotNull ExampleObject object) throws IOException {
            output.writeInt(object.num)
                .writeObject(object.obj, SubObject.SERIALIZER);
        }

         @Override
        protected void deserializeToBuilder(@NotNull SerializerInput input,
                @NotNull Builder builder, int versionNumber) throws IOException, ClassNotFoundException {
            builder.setNum(input.readInt())
                .setObj(input.readObject(SubObject.SERIALIZER));
        }
    }
}
还有 42% 的精彩内容
©著作权归作者所有,转载或内容合作请联系作者
支付 ¥5.20 继续阅读
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • JAVA序列化机制的深入研究 对象序列化的最主要的用处就是在传递,和保存对象(object)的时候,保证对象的完整...
    时待吾阅读 10,825评论 0 24
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,042评论 25 707
  • 第一章-唤醒内在的自己: 唤醒,这是行动教练需要掌握的一个重要技能。也是开展教练活动的基础,有句话说道“最难唤醒的...
    宋洋SongYang阅读 189评论 0 0
  • 1感恩: 感恩自己今天如约打卡,例行约定。 感恩今天依然是好天气,看到冬日暖阳,心情也很舒畅。 感恩公公今天就提前...
    遇见念念相续阅读 111评论 0 0
  • 孩子星期二好好的去上学,回来一身包,一直抓。第一时间我问孩子怎么回事,老师知道吗?孩子说知道,有跟老师说,老...
    施丽萍阅读 290评论 0 0