前语
在咱们日常开发中,有可能会接触到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,他们的联系便是:
图片来自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就登场了
咱们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…