另一篇关于 Serializable 和 Parcelable 对比的文章

译自 Yet another post on Serializable vs Parcelable

在我的 Android 开发生涯中我一直通过实现 Serializable接口 来封装 Activity 之间传递的数据. 我听说过 Parcelable 这个接口以及它声称的优势, 但是从来没有真正见过任何与此相关的性能上的问题. 所以我一直没有看到任何必要性让我使用 Parcelable, 给自己的类增加很多代码, 而放弃使用只要增加一个声明就可以使用的 Serializable. 当我最近开始接手一个工程的时候我很快改变了自己这种做法, 因为: a) Parcelable 被认为会快一个数量级以上 (这篇文章的译文在这里). b) 有一个非常棒的代码生成库, Parceler, 是的开发者不需要手动去完成 Parcelable 接口的实现工作.

我刚刚完成了这个应用中我的一个大的功能点, 所有看起来都非常的好直到我们的 QA 人员来向我报告了一个问题 - 当从一个 Activity 跳转到另一个 Activity 的过程中程序崩溃了, 抛出了一个 TransactionTooLargeException(在 API 15 以下只是一句简单的日志 E: !!! FAILED BINDER TRANSACTION !!!). 而他并没有做任何疯狂的操作. Google 搜索提供的都是远程进程调用和 Binder Transaction 缓存相关的内容, 而这些内容是我之前从没有关心过的. 其中提到 Binder Transaction 缓存, 它被好像被限制为1MB, 而且它被用于同时用于所有进行中的 Transaction. 我做的事情是通过一个启动 Activity 的 Intent 传递一个相当大的图像对象, 那么是不是我的这个操作导致缓存溢出了呢?

让我们深入研究一下这个

我尝试的第一件事是把 Parcelable 实现转换为 Serializable 实现, 正如所预料的, 程序现在正常运行了. 然后我试图找到使用 Serializable 的情况下传输数据的极限, 但是我失败了, 即使我创建的图像比那个之前造成崩溃的图像大100倍, 我操作的次数比之前能想到的最坏的情况多很多很多. 在这个问题出现之前我已经发现, 使用 Parcelable/Parceler 时并不会在反编码(demarshalling)时保存对同一个图像对象的引用, 而是会在引用同一个图像的时候创建多个对象. 尽管这种行为可能不是很让人惊讶, 或者一点都不让人惊讶, 但是它确实和 Serializable 的做法不同.

为了在一个更加可控的环境中进行测试, 我创建了一个小的实验 app. 你可以在这里找到 apk 文件, 在这里找到源码. 在里面你会看到一个 EmptyObject 类(后面有时简称 EO), 而它也的确就是一个空的对象, 里面没有任何成员变量也没有任何方法. 我想看看分别使用 SerializableParcelable 我能把多少个这种没用的对象塞进 ArrayList 并将它从一个 Activity 传递到另一个 Activity. 我还创建了一个叫做 Package 的类只是用来持有这个 ArrayList<EmptyObject> 对象. 我唯一所得事情就是把一个 Package 类的实例放到一个 Intent 中, 然后通过这个 Intent 来启动另一个 Activity.

测试结果

使用 Parcelable 接口可以传送 4045 个 EmptyObject 实例. 当相应的 Package 实例被编组之后的 byte[] 长度为 517764. 当增加一个 EO 对象之后程序抛出 TransactionTooLargeException 异常崩溃了, 所以我猜想 517892 个 byte 有点太大了. 与此同时, 当我使用 Serializable 接口的时候 4045 个 EO 对象编组后的长度仅为 244482 所以它很轻松地被传送了. 使用 Serializable 的临界值是 86276 个 EO 对象, 序列化后的 byte[] 长度为 517928. 我的假定是如果我创建一个所有引用都是指向同一个 EO 对象的列表这个值会变得非常大, 但是它并没有. 一个持有 86276 个指向同一个 EO 实例的引用的 Package 对象, 当序列化之后 byte[] 大小为 431593, 所以我们可以在塞大约 20k 的其他东西进去. 一件很清楚的事情是, 不管使用那种方式, 限制是你能够穿梭的 byte 数量. 在我的运行 Android 5.0 系统的 Moto X(2代) 中, 这个值好像是比 512KB 稍微少一点. 结果是和设备相关的: 我看到过的其他设备有从可以多传递几KB的 Galaxy S5和S4, 到几乎可以传递满满 1MB 的 HTC One M8. 而且它看起来和手机的运行时环境(dalvik/ART)并没有什么关系. 一个可以确定的事情是对于相同的图片对象使用 Serializable 接口比使用 Parcelable 接口生成的 byte[] 少很多.

咦, 出现了奇怪的现象

可能你会感到奇怪, 为什么我需要一个 Package 类, 为什么我不直接发送一个 ArrayList<EmptyObject>. 为了简化我觉得这样做. 然后所有事情都爆炸了(blew up). 突然我无法使用 Serializable 来发送 86k 的对象, 我甚至不能使用 Parcelable 来发送 4k 的对象. 更奇怪的是如果我发送一个拥有10个指向同一个对象的引用的列表, 我会在另一边得到10个不同的对象. 我不知道发生了什么.

既然 ArrayList 实现了 Serializable 接口, 而它里面又装满了同样实现了 Serializable 接口的 EmptyObject 实例, 我认为我调用了 putExtra 重载方法中的 Intent.putExtra(String name, Serializable value). 而我确实是的, 但是事情并非像我预期的那样. 如果你查看代码你会发现它实际上调用了 Bundle 类(API 21 以上是 BaseBundle 类)的 putSerializable(String key, Serializable value) 方法, 而这个方法所做的事情就是把从一个映射(Map)中抽取出指定键值相应的序列化的对象(Serializable object). 而这个映射(Map)最终会被 Parcel 类中的 writeArrayMapInternal(ArrayMap<String, Object> val) 方法所编码(marshall). 有趣的地方就在这里. 这个方法会遍历这个映射(Map), 并对它其中的每一个对象调用 writeValue(Object v) 方法, 这个方法里只是通过一个 switch 语句进行一系列的 instanceof 判断, 然后调用一些写方法. 我们的列表最终并不会通过 writeSerializable(Serializable s) 方法被写入, 而是会通过一个 writeList(List val) 方法进行遍历, 然后对每一个列表中的元素单独调用 writeSerializable(Serializable s) 方法. 然后你就明白了: 当我们把一个 Serializable 的对象传给 IntentputExtra 方法, 而这个对象刚好是一个列表, 我们期望整个对象/列表作为一个整体被序列化但是实际上它会依次序列化列表中的每一个对象.

Ok, 那么 Parcelable 依然要快10倍吗?

实际上我并不知道, 因为我并不打算实现纯净的 Parcelable 类(implement pure Parcelable classes). 然而这个实验 app 测量了从在 Activity A 创建一个 Intent 到在 Activity B 中提取出(unpacking)它的时间, 尽管做法可能很天真. 我并没有重复测试1000次来获取平均值, 但是对于我的情况来看, 比 Serializable 实现快上10倍到17倍是没有的. 的确, 看上去 Parcelable 实现要快2倍多. 但是, 我是在传送一堆空的对象. 记得那个实际应用中的图片对象吗, 就是我们最开始讨论的那个? 对于它来说, 实际上使用 Serializable 实现反而要快差不多3倍, 这是我通过很多设备验证之后得出的结论. 我不知道为什么, 而且我也不再想去问了.

结论

我并没有要告诉你用回 Serializable 实现序列化. 或许我现在所做的事情都是错的, 或者由于现在的 Android 设备与前些年的比起来已经完全不同, 有些事情需要随之改变. 我唯一的建议是不要盲目相信你在互联网上看到的所有事情, 在你的场景下亲自做一些尝试. 那应该会使你得出一些有意义的结论.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值