程序员必懂小技巧之Parcelable

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值