前言
在我们日常开发中,有可能会接触到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,他们的关系就是:
![](https://img.haomeiwen.com/i28627856/295508dc3ee11d61.png)
使用部分
我们已经从上面的初始化部分,了解到了一个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就登场了
![](https://img.haomeiwen.com/i28627856/4bae298e35f3ebe5.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…
作者:Pika
链接:https://juejin.cn/post/7180934643493896252
网友评论