美文网首页
Bundle源码解析

Bundle源码解析

作者: YocnZhao | 来源:发表于2020-07-15 15:45 被阅读0次

    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申请的地址指针,读写的时候都是用这个指针去操作。

    相关文章

      网友评论

          本文标题:Bundle源码解析

          本文链接:https://www.haomeiwen.com/subject/byfjhktx.html