https://juejin.im/user/3491704658736942
在威胁“封禁TikTok”后,美国总统特朗普近日再次发出赤裸裸恐吓——TikTok必须在9月15日之前卖给美国,否则必须关门,而且相当一部分钱要交给美国财政部,“因为是我们让这笔交易成为可能”。
/ 前言 /
序列化,简单来说,就是将对象数据,按照一定的规则,转成一串有迹可循的二进制流,然后将此二进制流在双方中传输。其中,输出方按照约定的规则转译对象,接收方按照约定的规则进行翻译,从而获得有效数据。
应对Android的日常开发中,出镜率最高的序列化手段无非Serializable、以及Parcelable。也常将二者进行比较,以其各自的优劣势来应对不同的场景。
Serializable有进行过分析(https://juejin.im/post/6850418112501268494),对于Serializable的整体实现也算有个认识,简单做个回顾:
Serializable 将对象当成一颗树,遍历并反射各个节点获取信息。期间产生很多中间变量来保存信息
提供来一些可实现的对象方法,可以在序列化、反序列化过程中做一些处理,比如替换对象、比如加解密,比如默认值(包括以对象为粒度的默认值)
可以实现 ObjectOutputStream.writeObjectOverride() 和 ObjectInputStream.readObjectOverride()来完全控制序列化、反序列化过程
本篇文章的目的,将分析Parcelable实现原理,一者可以明白其实现;二者可以更好地与Serializable进行比较;三者对于序列化所要到达的目的考量也会有较清晰的认识。
本文将会回答以下问题,如果你不知道答案,或许有些帮助:
Parcelable 如何实现
为什么序列化与反序列化要保持相同的顺序
能否自行实现Parcel
子类是否需要实现Parcelable
Parcelable 真的比 Serializable 快吗
/ Parcel存储 /
实际上,Parcelable的实现可以用一句话概括:按照顺序,将从标记处获取的信息,加以辅助信息逐个写入存储区域(看完后文会理解这段话)。因此对于Parcelable来说,存储就显得尤为重要。而对于存储,主要实现均由Parcel.cpp来完成。
Parcel.cpp的出现,是为了应对IPC过程中的数据传输问题而出现的,这一点从Parcel.cpp位于Binder包下可窥探一二。以及为了intercode communication,这一点从Java侧能享用,以及Parcel.cpp的存储方式能看出。
进程间通信需要序列化参与,而Serializable以Java实现,天然就无法解决此问题。得益于Parcel.cpp,Parcelable借势来处理一些对性能要求较高的场景了,比如面对Intent。
Parcel.cpp位于
platform/frameworks/native/libs/bilder/Parcel.cpp
对于理解Parcel.cpp,以下成员变量需要了解
uint8_t* mData; //缓存数据的首地址
size_t mDataSize; //当前数据大小
size_t mDataCapacity; //数据总容量
mutable size_t mDataPos; //下一个数据的首地址
用图理解是这样的
也就是说,Parcel.cpp实际上提供了一块连续的内存区域来存储数据。Parcel.cpp提供了很多的成员函数来写入数据,而大部分的写入操作,会调用到writeAligned()
template<class T>
status_t Parcel::writeAligned(T val) {
COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));
if ((mDataPos+sizeof(val)) <= mDataCapacity) {
//计算当前容量是否还能容得下新数据
restart_write:
// mData+mDataPos地址为指针T*,值为val
*reinterpret_cast<T*>(mData+mDataPos) = val;
// 修改偏移地址,也就是mDataPos
return finishWrite(sizeof(val));
}
// 当前容量不够,增加容量
status_t err = growData(sizeof(val));
// 增加容量成功,跳转到restart_write执行
if (err == NO_ERROR) goto restart_write;
// NO_ERROR 代表函数按预期执行
return err;
}
结合上图,Parcel.cpp存储的,实际上,是连续的各种类型的对象。也因此存在一条规则,即使用Parcel.cpp来存储,必须要清楚,在哪个位置,存储的是什么类型的数据,这是后话了。当然Parcel.cpp也提供了诸如write()等方法,来将一数据,通过memcpy()写入mDataPos后的存储区域。
有写入,就有读出,对应的函数为readAligned()
template<class T>
status_t Parcel::readAligned(T *pArg) const {
COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));
if ((mDataPos+sizeof(T)) <= mDataSize) {
//检查要读取的数据是否越界
if (mObjectsSize > 0) {
// 检查数据能否被正确读取
status_t err = validateReadData(mDataPos + sizeof(T));
if(err != NO_ERROR) {
// 这段区域的数据无法正常读取,但是偏移值还是要修改 mDataPos += sizeof(T);
return err;
}
}
// 要读取的数据的物理地址
const void* data = mData+mDataPos;
// 更新偏移地址
mDataPos += sizeof(T);
// data的数据类型为T,pArg指向它
*pArg = *reinterpret_cast<const T*>(data);
return NO_ERROR;
} else {
return NOT_ENOUGH_DATA;
}
}
Parcel.cpp对于存储区域的内容,没有做过多的限制,这也是足够灵活的原因。如果你愿意你大可以借助Parcel.cpp来实现自己的序列化方式,并能享受到Parcelable所能享受的优势。
在Java侧,当然不会让开发者直接操作Parcel.cpp,与之对应的也就是Parcel。为了便于区分,在之后的内容Parcel指的是Java侧的Parcel,Parcel.cpp如其名为C++的。
Parcel定义了足够多的Native方法,通过JNI,与Parce