Bundle源码解析

2020-07-15  本文已影响0人  YocnZhao

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中使用。

总结:

上一篇下一篇

猜你喜欢

热点阅读