注册

Bundle源码解析

Bundle源码解析


做一个调用系统分享json的时候遇到一个问题,在用Bundle传递String太大的时候会报错,所以可以计算String的大小,size小的时候传String,size大的时候可以把String存文件然后分享文件。但是问题来了,这个大小的边界在哪儿呢?到底传多大的数据才会报错呢?

我们先看一个报错的错误栈:

Caused by: android.os.TransactionTooLargeException: data parcel size 1049076 bytes
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(Binder.java:1129)
at android.app.IActivityManager$Stub$Proxy.startActivity(IActivityManager.java:3754)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1671)
at android.app.Activity.startActivityForResult(Activity.java:4586) 
at android.app.Activity.startActivityForResult(Activity.java:4544) 
at android.app.Activity.startActivity(Activity.java:4905) 
at android.app.Activity.startActivity(Activity.java:4873)

跟着各种startActivity追溯上去,BinderProxy#transact方法里面调用transactNative方法,这是个native方法,我们去往androidxref网站查看。追查到/frameworks/base/core/jni/android_util_Binder.cpp里面android_os_BinderProxy_transact方法,最后调用signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/, data->dataSize());方法771行,看到:


TransTooBig.png


发现把parcelSize限制在了200K的大小,当大于200K的时候就会报错。


那到底Bundle扮演了什么角色呢?我们从一个最简单的场景看起:


使用方法
//putExtra
public static void startActivity(Context context) {
Intent intent = new Intent(context, TestActivity.class);
intent.putExtra("KEY", 1);
context.startActivity(intent);
}

//使用getStringExtra获取
String msg = getIntent().getStringExtra(KEY);

几乎是最简单的StartActivity场景,我们分步来看这几句代码都做了什么:


数据的写入 - Intent#putExtra

Intent#putExtra实际上是调用了Bundle#putExtra:

//Intent.java
public @NonNull Intent putExtra(String name, int value) {
if (mExtras == null) {
mExtras = new Bundle();
}
mExtras.putInt(name, value);
return this;
}

//BaseBundle.java
BaseBundle() {
this((ClassLoader) null, 0);
}
BaseBundle(@Nullable ClassLoader loader, int capacity) {
mMap = capacity > 0 ? new ArrayMap<String, Object>(capacity) : new ArrayMap<String, Object>();
//初始化了一个叫mMap的空ArrayMap,同时初始化了一个mClassLoader,用来实例化Bundle里面的对象。
mClassLoader = loader == null ? getClass().getClassLoader() : loader;
}

public void putInt(@Nullable String key, int value) {
unparcel();
mMap.put(key, value);
}

mMap是用来存储我们需要传递的Kay-Value的,在putInt的方法里面调了一个unparcel()方法,然后往mMap里面put了一个value,看起来很简单,其实我们可以看到在所有的putXXX方法里面都是先调用了一个unparcel()再执行了put方法,其实我们可以从名字和逻辑猜出来这个方法是做什么的,其实就是在put之前如果已经有了序列化的数据,需要先反序列化填到eMap里面,再尝试去添加新的数据。

带着这个猜测我们来看unparcel()方法

	//BaseBundle.java
void unparcel() {
synchronized (this) {
final Parcel source = mParcelledData;
if (source != null) {//parcelledData不为空的时候会走initializeFromParcelLocked。mParcelledData什么时候赋值呢?在后面初始化的时候能看到。
initializeFromParcelLocked(source, /*recycleParcel=*/ true, mParcelledByNative);
} else {
...
}
}
}
//把data从NativeData中读取出来,save到mMap中。
private void initializeFromParcelLocked(@NonNull Parcel parcelledData, boolean recycleParcel, boolean parcelledByNative) {
...
final int count = parcelledData.readInt();//拿到count,也就是size,是write的时候第一个写入的。
if (count < 0) {
return;
}
ArrayMap<String, Object> map = mMap;
if (map == null) {
map = new ArrayMap<>(count);//根据拿到的count初始化map
} else {
map.erase();
map.ensureCapacity(count);
}
try {
if (parcelledByNative) {
parcelledData.readArrayMapSafelyInternal(map, count, mClassLoader);
} else {
parcelledData.readArrayMapInternal(map, count, mClassLoader);
}
} catch (BadParcelableException e) {
...
} finally {
mMap = map;
if (recycleParcel) {
recycleParcel(parcelledData);
}
mParcelledData = null;
mParcelledByNative = false;
}
}
//Parcel.java
void readArrayMapSafelyInternal(@NonNull ArrayMap outVal, int N, @Nullable ClassLoader loader) {
while (N > 0) {
String key = readString();
Object value = readValue(loader);
outVal.put(key, value);
N--;
}
}

public final Object rea(@Nullable ClassLoader loader) {
int type = readInt();
switch (type) {
...
case VAL_INTEGER:
return readInt();
...
}

public final int readInt() {
return nativeReadInt(mNativePtr);
}
private static native int nativeReadInt(long nativePtr);

调用到native方法里面,我们可以到androidxref网站查看相关源码,下面的代码基于Android 9.0

// /frameworks/base/core/jni/android_os_Parcel.cpp
788 {"nativeReadInt", "(J)I", (void*)android_os_Parcel_readInt},

// /frameworks/base/core/jni/android_os_Parcel.cpp
static jint android_os_Parcel_readInt(jlong nativePtr)
402{
403 Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
404 if (parcel != NULL) {
405 return parcel->readInt32();
406 }
407 return 0;
408}

// /frameworks/native/libs/binder/Parcel.cpp
int32_t Parcel::readInt32() const
1822{
1823 return readAligned<int32_t>();
1824}

// /frameworks/native/libs/binder/Parcel.cpp
template<class T>
T Parcel::readAligned() const {
1606 T result;
1607 if (readAligned(&result) != NO_ERROR) {
1608 result = 0;
1609 }
1610
1611 return result;
1612}
/frameworks/base/core/jni/android_os_Parcel#android_os_Parcel_readInt(jlong nativePtr)
- frameworks/native/libs/binder/Parcel#Parcel::readInt32()

// /frameworks/native/libs/binder/Parcel.cpp
template<class T>
status_t Parcel::readAligned(T *pArg) const {
1583 COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));
1584
1585 if ((mDataPos+sizeof(T)) <= mDataSize) {//越界检查
1586 if (mObjectsSize > 0) {
1587 status_t err = validateReadData(mDataPos + sizeof(T));//检测是否可以读取到这么多数据
1588 if(err != NO_ERROR) {
1590 mDataPos += sizeof(T);
1591 return err;
1592 }
1593 }
1594
1595 const void* data = mData+mDataPos;//指针偏移,指向期待的数据地址。
1596 mDataPos += sizeof(T);//数据偏移量增加。
1597 *pArg = *reinterpret_cast<const T*>(data);//指针强转赋值,读取到对应的数据
1598 return NO_ERROR;
1599 } else {
1600 return NOT_ENOUGH_DATA;
1601 }
1602}

最终都是调用到Parcel的方法,根据count循环的读取KEY/VALUE,先读key,再根据value的类型读value,我们可以看到最终都是调用了nativeReadXxx方法去读取对应的值。


Parcel的读取大致是首先我们init的时候会拿到一个native给我们的地址值nativePtr,java拿到这个地址值用一个long存储起来,long是64位的,所以用来存储32/64位的机器的地址就都够用了,相当于java调用native去申请了一块内存,并且java可以随时随地的访问到这块内存地址,再根据偏移量就可以拿到这块内存上存储的内容。
我们知道为了方便读取之类的原因,Native里面都是要做内存对齐的,所以Parcel的存储都是最少为4个字节,So,存储一个byte和一个int都会占用4个字节。


所有的存储和读取都是在native层面进行的,只需要得到偏移量就能够访问,毫无疑问这种处理是高效的。

但是问题就是我们无法得知下一个是Int还是Long,所以需要严格保证顺序,序列化和反序列化要保证顺序。
关于Parcel推荐看(关于Parcel),深入浅出。


所以,Intent#putExtra其实就是调用了bundle#putExtra,先检查是否有unparcel的数据,有就先读取出来,一个流程图如下:


readInt.png


上面是写一个数据的代码,我们知道Intent实现了Parcelable接口,所以Parcelable的写入接口方法就是writeToParcel方法,最终调用了BaseBundle的writeToParcelInner.

//Intent.java
public void writeToParcel(Parcel out, int flags) {
...
out.writeBundle(mExtras);
}

//Parcel.java
public final void writeBundle(@Nullable Bundle val) {
if (val == null) {
writeInt(-1);
return;
}

val.writeToParcel(this, 0);
}

public void writeToParcel(Parcel parcel, int flags) {
...
super.writeToParcelInner(parcel, flags);
...
}

//BaseBundle.java
void writeToParcelInner(Parcel parcel, int flags) {
// 如果parcal自己set了一个ReadWriteHelper,先调用unparcel,mMap赋值,mParcelledData为null
if (parcel.hasReadWriteHelper()) {
unparcel();
}
final ArrayMap<String, Object> map;
synchronized (this) {
if (mParcelledData != null) {
if (mParcelledData == NoImagePreloadHolder.EMPTY_PARCEL) {
parcel.writeInt(0);
} else {
int length = mParcelledData.dataSize();
parcel.writeInt(length);
parcel.writeInt(mParcelledByNative ? BUNDLE_MAGIC_NATIVE : BUNDLE_MAGIC);
parcel.appendFrom(mParcelledData, 0, length);
}
return;
}
map = mMap;
}

// 如果map为空,直接写入length = 0
if (map == null || map.size() <= 0) {
parcel.writeInt(0);
return;
}
int lengthPos = parcel.dataPosition();//先记录开始的位置
parcel.writeInt(-1); // dummy, will hold length 写入-1占length的位置
parcel.writeInt(BUNDLE_MAGIC); //写入MAGIC CODE

int startPos = parcel.dataPosition();
parcel.writeArrayMapInternal(map);
int endPos = parcel.dataPosition();

// 表示游标回到之前记录的开始位置
parcel.setDataPosition(lengthPos);
int length = endPos - startPos;
parcel.writeInt(length); //写入length
parcel.setDataPosition(endPos); //aprcel回到结束位置。
}

Parcel的写入是按照顺序先写入data的length,再写入一个MAGIC,然后写入真正的data。当然,读取的时候也是按照这个顺序来读取的。mParcelledData是存储的Parcel格式的Bundle的,如果mParcelledData不为空,那么mMap一定是空的。如果data被unparcel出来了,那么mMap有数据mParcelledData为空。
所以,写入的时候发现mParcelledData不为空,直接把mParcelledData写入parcel就好了,否则调用writeXXX把mMap写入parcel。



parcel的dataPosition()表示当前在写的位置,类似写文件吧,seek到不同的位置开始写对应位置的数据。默认获取到的dataPosition()是在数据的最后的位置。



写入的时候有个小Trick,这里写入的时候先记录一个开始的位置lengthPos,先写一个-1代表length,再写MAGIC CODE,然后开始写数据并且记录开始结束位置startPosendPos,得到数据的length之后再seek到-1的位置用length把-1覆盖掉再seek到结束位置。


写入逻辑结束。


数据读取 - getIntent().getStringExtra

数据读取,直接调用到了Bundle的getString方法,除了调用了unparcel,上面我们看了unparcel的代码,作用就是给mMap赋值,反序列化之后就是直接从mMap中取数据了。

//Intent.java
public @Nullable String getStringExtra(String name) {
return mExtras == null ? null : mExtras.getString(name);
}

//BaseBundle.java
public String getString(@Nullable String key) {
unparcel();
final Object o = mMap.get(key);
try {
return (String) o;
} catch (ClassCastException e) {
typeWarning(key, o, "String", e);
return null;
}
}

void unparcel() {
synchronized (this) {
final Parcel source = mParcelledData;//这里我们看下上面没看到的条件,mParcelledData什么时候赋值呢?
if (source != null) {
initializeFromParcelLocked(source, /*recycleParcel=*/ true, mParcelledByNative);
} else {
...
}
}
}

那mMap是什么时候赋值的呢?我们知道Bundle是实现了Parcelable接口的,需要实现序列化反序列化方法,我们在Intent里能找到CREATOR方法:

//Intent.java
public static final @android.annotation.NonNull Parcelable.Creator<Intent> CREATOR
= new Parcelable.Creator<Intent>() {
public Intent createFromParcel(Parcel in) {
return new Intent(in);
}
public Intent[] newArray(int size) {
return new Intent[size];
}
};

反序列化的调用顺序,代码逻辑很简单,都略过,只看调用逻辑:

Intent#Intent(Parcel in)
- Intent#readFromParcel(Parcel in)
- Parcel#readBundle()
- Parcel#readBundle(ClassLoader loader)
- Bundle#Bundle(Parcel parcelledData, int length)
- BaseBundle#BaseBundle(Parcel parcelledData, int length)
- BaseBundle#readFromParcelInner(Parcel parcel, int length)

//调用到readFromParcelInner方法,看一下这个方法做了什么。

//BaseBundle.java
void readFromParcelInner(Parcel parcel) {
int length = parcel.readInt();//先读了一个length
readFromParcelInner(parcel, length);
}
//BaseBundle.java
private void readFromParcelInner(Parcel parcel, int length) {
...
final int magic = parcel.readInt();
final boolean isJavaBundle = magic == BUNDLE_MAGIC;
final boolean isNativeBundle = magic == BUNDLE_MAGIC_NATIVE;
if (!isJavaBundle && !isNativeBundle) {
throw new IllegalStateException("Bad magic number for Bundle: 0x" + Integer.toHexString(magic));
}

if (parcel.hasReadWriteHelper()) {
synchronized (this) {
initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle);
}
return;
}

// Advance within this Parcel
int offset = parcel.dataPosition();
parcel.setDataPosition(MathUtils.addOrThrow(offset, length));

Parcel p = Parcel.obtain();
p.setDataPosition(0);
p.appendFrom(parcel, offset, length);
p.adoptClassCookies(parcel);
p.setDataPosition(0);

mParcelledData = p;
mParcelledByNative = isNativeBundle;
}

So,结合我们上面看过的源码,Bundle实际数据的存储有两种形式,一种是作为Parcel存储在mParcelledData里面,一种是作为K-V存储在mMap里面。区别是parcel.hasReadWriteHelper(),有没有给Parcel设置ReadWriteHelper



  1. 如果设置了,实际逻辑是调用了initializeFromParcelLocked,跟上面我们看过的unparcel方法调用的一致,把数据unparcel出来放到mMap中。
  2. 如果没设置,obtain一个可用的Parcel,把数据从传过来的parcel中放进去,赋值给mParcelledData

后面如果有putXXX操作的时候再通过unparcel方法反序列化到mMap中使用。


总结:



  • Bundle里面最多能传递200K的数据。
  • 调用putExtra的时候K-V数据需要放到mMap中

    • 如果Bundle是新new出来的,初始化一个mMap,K-V直接放到mMap中。
    • 如果Bundle之前就存在,readFromParcelInner方法中反序列化,结束后数据有两种方式存储,一种是K-V结构存储在mMap中,一种是以parcel结构存储在mParcelledData中。如果还需要putExtra,如果mMap中有值,直接put到mMap中,否则调用unparcelmParcelledData反序列化到mMap中再put到mMap中。


  • Bundle的存和读都是调用的Parcel的WriteXXX/ReadXXX方法,都会调用到nativeWriteXXX/nativeReadXXX,native中也有一个Parcel与java中的对应。类似于DexClassLoaderDexFile的处理,也是java持有一个native申请的地址指针,读写的时候都是用这个指针去操作。

作者:Yocn
链接:https://juejin.cn/post/7222833868503253051
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册