Parcel-Binder流水线的打包侠

1,778 阅读8分钟

前言

在我们日常开发中,有可能会接触到Parcel,这是一个在android中非常有趣的类,本章将通过对Parcel的作用出发,了解到Parcel类设计的出发点,同时通过一个例子实践,使我们能通过Parcel去监控我们的跨进程数据传输的数据量。

Parcel 作用

我们在业务开发很有可能会遇到跨进程通信相关的场景,比如我们常用的跨进程是通过Binder机制去实现的,当然,本章跟Binder没有什么关系啦!我们来想一下,如果我们传输相关的“数据”给另一个进程,我们怎么做呢?这里有两个方面,如果是基本类型,比如int,我们想要在进程1中传递到进程2中,其实不断把数据复制过去就可以了,但如果是一个对象(Object)呢?如果只是把某个对象传递,当然,这个对象本质只是一个内存地址对吧!比如我有一个Object,假设引用是Ox8000,对象的值0x16555,这个内存值在进程1中是有意义的,但是到了进程2,同样的引用值有意义吗?答案肯定是否定的,因为两个进程都有自己独立的内存地址,因此单纯传递一个对象是没有意义的。

通过上面的阐述,我们能够知道在进程间中进行数据传递,需要解决这样一类问题。那么如果我们只把进程1的数据进行一个“打包”,传递到进程2中,我们只需要在进程2中还原一下数据的内容(区别于上述的地址),是不是就能实现数据的传递了呢!没错,Parcel就是为了解决这个问题而诞生的。

理解Parcel

我们常用的Parcel,可以这样用

val parcel = Parcel.obtain()
parcel.writeString(“xxx”)
parcel.writeParcelable(Parcelable)
parcel.recycle()

初始化部分

那么从这个例子出发,我们看看Parcel内部做了什么趣事。首先采用了Parcel.obtain获取一个Parcel,我们在外部是不能直接获取的,聪明的小伙伴肯定就知道了,其实这就是一个对象池的封装

public static Parcel obtain() {
    Parcel res = null;
    synchronized (sPoolSync) {
        对象池
        if (sOwnedPool != null) {
            尝试从sOwnedPool获取Parcel对象
            res = sOwnedPool;
            sOwnedPool因为已经被使用了,此时就指向了下一个未使用的Parcel
            sOwnedPool = res.mPoolNext;
            res.mPoolNext = null;
            sOwnedPoolSize--;
        }
    }

    如果对象池没有缓存,就新建一个
    if (res == null) {
        res = new Parcel(0);
    } else {
        if (DEBUG_RECYCLE) {
            res.mStack = new RuntimeException();
        }
        res.mReadWriteHelper = ReadWriteHelper.DEFAULT;
    }
    return res;
}

我们继续看一个,Parcel初始化干了什么

private Parcel(long nativePtr) {
    if (DEBUG_RECYCLE) {
        mStack = new RuntimeException();
    }
    //Log.i(TAG, "Initializing obj=0x" + Integer.toHexString(obj), mStack);
    init(nativePtr);
}

private void init(long nativePtr) {
    if (nativePtr != 0) {
        mNativePtr = nativePtr;
        mOwnsNativeParcelObject = false;
    } else {
        一开始先执行这里
        mNativePtr = nativeCreate();
        mOwnsNativeParcelObject = true;
    }
}

回顾一下上面的obtain,一开始对象池里面Parcel都没有,肯定会走到new Parcel(0)里面,此时Parcel构造函数传入的就是0,这里调用了nativeCreate方法,它是一个jni调用,返回值保存在mNativePtr变量中,那么我们其实就可以猜测了,Java层的Parcel,其实本质还是一个壳,真正进行数据传递存储的地方,肯定还是在native层。

我们可以通过Parcel的native实现,找到对应的jni注册关系

{"nativeCreate",              "()J", (void*)android_os_Parcel_create},

nativeCreate其实最终就会调用到android_os_Parcel_create

static jlong android_os_Parcel_create(JNIEnv* env, jclass clazz)
{
    Parcel* parcel = new Parcel();
    return reinterpret_cast<jlong>(parcel);
}

我们可以看到,这里在native中new了一个Parcel(Parcel.cpp),这个才是真正的Parcel,紧接着把parcel这个指针返回了。所以这里我们就知道了,在java层中的mNativePtr,其实就保存着native层中Parcel的指针,这里跟Thread类的实现有异曲同工之妙。

那么我们继续看空,这个native层的Parcel干了什么,我们直接看它的构造函数

Parcel::Parcel()
{
    LOG_ALLOC("Parcel %p: constructing", this);
    initState();
}

void Parcel::initState()
{
    LOG_ALLOC("Parcel %p: initState", this);
    mError = NO_ERROR; 错误吗
    mData = nullptr; Parcel中存储的数据,它是一个指针
    mDataSize = 0; Parcel已经存储的数据
    mDataCapacity = 0; 最大存储空间
    mDataPos = 0; 数据指针
    ALOGV("initState Setting data size of %p to %zu", this, mDataSize);
    ALOGV("initState Setting data pos of %p to %zu", this, mDataPos);
    mVariantFields.emplace<KernelFields>();
    mAllowFds = true;
    mDeallocZero = false;
    mOwner = nullptr;
    mEnforceNoDataAvail = true;
}

这里很有趣,只是初始化了几个成员变量,赋予初始值,这里需要注意的是,这里仅仅只是初始化,ing没有进行真正的内存分配,这里也是动态扩展的原则,只有这个Parcel真正被使用的时候,才进行内存的分配。同时我们也看到了几个关键的变量,mData,mDataSize,mDataCapacity,mDataPos,他们的关系就是:

image.png

图片来自Parcelable 是如何实现的

使用部分

我们已经从上面的初始化部分,了解到了一个Parcel是怎么被创建出来的,接着我们再看一下其使用,我们以writeString为出发点,解析一下其内部的原理,我们调用writeString,最终会被调用到一个jni函数,nativeWriteString16,它的真正实现在

{"nativeWriteString16",       "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeString16},

可以看到,我们在java层的一切writeXXX操作,都会被切换到native中执行

我们以android11的分支为例子,不同版本有一些实现的差异
static void android_os_Parcel_writeString16(JNIEnv* env, jclass clazz, jlong nativePtr, jstring val)
{
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
    if (parcel != NULL) {
        status_t err = NO_MEMORY;
        if (val) {
        const jchar* str = env->GetStringCritical(val, 0);
            if (str) {
                最后还是通过Parcel类的方法writeString16进行实现,外面都是检查
                err = parcel->writeString16(
                        reinterpret_cast<const char16_t*>(str),
                        env->GetStringLength(val));
                env->ReleaseStringCritical(val, str);
            }
        } else {
            err = parcel->writeString16(NULL, 0);
        }
        if (err != NO_ERROR) {
            signalExceptionForError(env, clazz, err);
        }
    }
}

我们可以知道,android_os_Parcel_writeString16其实还是一个壳,用于一些校验检查,真正实现在writeString16这个方法中

Parcel.cpp

status_t Parcel::writeString16(const char16_t* str, size_t len)
    {
    if (str == nullptr) return writeInt32(-1);

    // NOTE: Keep this logic in sync with android_os_Parcel.cpp
     先写入了当前数据的长度
    status_t err = writeInt32(len);
    if (err == NO_ERROR) {
    len *= sizeof(char16_t);
    writeInplace计算复制数据的目标所在的地址,data是怎么找到的,需要注意这个函数
    uint8_t* data = (uint8_t*)writeInplace(len+sizeof(char16_t));
    if (data) {
    通过writeInplace拿到了数据,最后通过memcpy把数据拷贝到目标进程的内存空间
    memcpy(data, str, len);
    *reinterpret_cast<char16_t*>(data+len) = 0;
    return NO_ERROR;
    }
    err = mError;
    }
    return err;
    }

还记得我们一直说,Parcel其实要做到把进程1的内存数据打包,然后在进程2中还原,还原的过程就是writeInplace,最后通过memcpy把数据拷贝过去

void* Parcel::writeInplace(size_t len)
        {
        if (len > INT32_MAX) {
        // don't accept size_t values which may have come from an
        // inadvertent conversion from a negative int.
        return nullptr;
        }
        进行了数据对齐,比如我们以长度为4对齐是,此时len为3,也需要填充为4
        const size_t padded = pad_size(len);

       检查是否溢出,我们记得上面那个图,mDataPos就是当前数据的指针,如果加上padded后,产生溢出,就会使得mDataPos+padded < mDataPos
        if (mDataPos+padded < mDataPos) {
        return nullptr;
        }
        当前数据是否超过了最大容量mDataCapacity
        if ((mDataPos+padded) <= mDataCapacity) {
        restart_write:
        //printf("Writing %ld bytes, padded to %ld\n", len, padded);
        uint8_t* const data = mData+mDataPos;

        判断采用BIG_ENDIAN还是LITTLE_ENDIAN方式填充
        if (padded != len) {
        #if BYTE_ORDER == BIG_ENDIAN
static const uint32_t mask[4] = {
        0x00000000, 0xffffff00, 0xffff0000, 0xff000000
        };
        #endif
        #if BYTE_ORDER == LITTLE_ENDIAN
static const uint32_t mask[4] = {
        0x00000000, 0x00ffffff, 0x0000ffff, 0x000000ff
        };
        #endif
        //printf("Applying pad mask: %p to %p\n", (void*)mask[padded-len],
        //    *reinterpret_cast<void**>(data+padded-4));
        *reinterpret_cast<uint32_t*>(data+padded-4) &= mask[padded-len];
        }
        更新数据指针mDataPos
        finishWrite(padded);
        return data;
        }
        如果执行到这里,说明上面的操作已经超过了Parcel的存储空间大小,需要调用growData进行扩容
        status_t err = growData(padded);
        扩容完成后,调用restart_write重新来一次分配过程
        if (err == NO_ERROR) goto restart_write;
        return nullptr;
        }

扩容的手段也是很简单,新size = ((mDataSize+len)*3)/2,即扩容了存储数据后的1.5倍,期间也会判断是否超过SIZE_MAX 这个宏定义

status_t Parcel::growData(size_t len)
    {
    if (len > INT32_MAX) {
    // don't accept size_t values which may have come from an
    // inadvertent conversion from a negative int.
    return BAD_VALUE;
    }

    if (len > SIZE_MAX - mDataSize) return NO_MEMORY; // overflow
    if (mDataSize + len > SIZE_MAX / 3) return NO_MEMORY; // overflow
    size_t newSize = ((mDataSize+len)*3)/2;
    return (newSize <= mDataSize)
    ? (status_t) NO_MEMORY
    : continueWrite(std::max(newSize, (size_t) 128));
    }

扩展实践

我们经过了一大串的源码解析,相信我们能够理解Parcel这个类的是怎么实现的了,那么了解这个有什么用呢?嗯!我们从实践出发才能真正获取到知识。

实战:在项目中,大家可能会遇到TransactionTooLargeException,这是因为进行binder传输的时候,数据量过大导致的?可能大家会问,我们项目中哪里用到binder了?其实我们最熟悉的startActivity就用到了binder进行跨进程传输,只是细节被android封装起来罢了。还有比如onSaveInstance,保存数据的时候,其实也是。如果一个Bundle数据过大,或者传输的Parcelable数据过大,就会触发TransactionTooLargeException,然后在实际项目中,我们怎么知道一个Bundle或者Parcelable数据的实际大小呢?这里需要注意一些,这里的实际大小并不是单单指这个数据的大小,而是跨进程通讯时打包后的大小,那我们打包后的大小怎么算呢?这个时候Parcel就登场了

image.png 我们Binder通讯其实就是用的Parcel进行数据打包的,所以判断一个Bundle的大小,就可以用以下方式

private fun sizeAsParcel(bundle: Bundle): Int {
    val parcel = Parcel.obtain()
    try {
        parcel.writeBundle(bundle)
        return parcel.dataSize()
    } finally {
        parcel.recycle()
    }
}

当然,Parcelable的数据也可以知道(比如我们的Intent就是实现了Parcelable)

fun sizeAsParcel(parcelable: Parcelable): Int {
    val parcel = Parcel.obtain()
    try {
        parcel.writeParcelable(parcelable, 0)
        return parcel.dataSize()
    } finally {
        parcel.recycle()
    }
}

知道大小后,我们在startActivitiy或者onSaveInstance这些需要进行binder通讯的地方,通过插桩或者监听系统回调的方式,就能做到一个卡口了!这里不是本篇的重点,所以没列出具体实现,在之后我们通过这种方式,实现一个parcelable的数据大小监控

总结

最后,感谢观看!!

链接

developer.android.google.cn/reference/a…