前语

在咱们日常开发中,有可能会接触到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,他们的联系便是:

Parcel-Binder流水线的打包侠

图片来自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就登场了

Parcel-Binder流水线的打包侠
咱们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…