美文网首页Android进阶之路android基础知识Android
Bundle/Intent传递序列化参数暗藏杀机!

Bundle/Intent传递序列化参数暗藏杀机!

作者: chzphoenix | 来源:发表于2018-04-03 10:16 被阅读634次

    Android进阶之路系列:http://blog.csdn.net/column/details/16488.html

    前几天一个朋友跟我说了一个诡异且恐怖的事情,有个人用了几行代码就让他们的app歇菜了。

    这勾起了我极大的兴趣,于是我亲自尝试了一下。代码非常简单,如下:

    Intent intent = new Intent();
    
    intent.setComponent(new ComponentName("com.test.test", "com.test.test.MainActivity"));
    
    intent.putExtra("anykey", new Boom());
    
    startActivity(intent);
    

    其中Boom是一个序列化类(serializable),而且extra的key可以是任何值。

    而com.test.test.MainActivity则是另外一个app中允许外部调起的activity,即MainActivity有一个为android.intent.action.MAIN的action,否则代码会报错。

    还需要满足一个条件,MainActivity代码中有从intent(getIntent或newIntent的参数)取参数的操作,如

    Bundle bundle = intent.getExtras();
    
    if (bundle != null) {
    
        int sd = bundle.getInt("key");
    
    }
    

    int sd = intent.getIntExtra("key", -1);
    

    注意,不仅仅是getInt,任何类型的都会出问题,而且key不必与之前的anykey一样!

    崩溃日志如下:

    E/AndroidRuntime: FATAL EXCEPTION: main
    
      Process: xxx, PID: 1688
    
      java.lang.RuntimeException: Parcelable encountered ClassNotFoundException reading a Serializable object (name = com.example.Boom)
    
      at android.os.Parcel.readSerializable(Parcel.java:2630)
    
      at android.os.Parcel.readValue(Parcel.java:2416)
    
      at android.os.Parcel.readArrayMapInternal(Parcel.java:2732)
    
      at android.os.BaseBundle.unparcel(BaseBundle.java:271)
    
      at android.os.BaseBundle.get(BaseBundle.java:364)
    
      at com.test.test.MainActivity.onNewIntent(MainActivity.java:128)
    
      ...
    
      Caused by: java.lang.ClassNotFoundException: com.example.Boom
    
      at java.lang.Class.classForName(Native Method)
    
      at java.lang.Class.forName(Class.java:400)
    
      at android.os.Parcel$2.resolveClass(Parcel.java:2616)
    
      at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1613)
    
      at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)
    
      at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1772)
    
      at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
    
      at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
    
      at android.os.Parcel.readSerializable(Parcel.java:2624)
    
      at android.os.Parcel.readValue(Parcel.java:2416)
    
      at android.os.Parcel.readArrayMapInternal(Parcel.java:2732)
    
      at android.os.BaseBundle.unparcel(BaseBundle.java:271)
    
      at android.os.BaseBundle.get(BaseBundle.java:364)
    
      at com.test.test.MainActivity.onNewIntent(MainActivity.java:128)
    
      ...
    
    02-27 17:33:33.799 1688-1688/? E/AndroidRuntime: Caused by: java.lang.ClassNotFoundException: Didn't find class "com.example.Boom" on path: DexPathList[...]
    
      at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
    
      at java.lang.ClassLoader.loadClass(ClassLoader.java:380)
    
      at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
    
      ... 27 more
    
    02-27 17:33:33.813 1688-1688/? E/MobclickAgent: onPause called before onResume
    

    可以看到是因为应用中没有Boom这个类,反序列化时找不到,那么既然应用中没有用到anykey,为什么会去做反序列化的操作呢?

    查看Intent及Bundle源码可以发现,那些get函数最终都会调用BaseBundle的相应的get函数。

    在BaseBundle中不论get函数还是put函数中都会先调用unparcel函数,如:

    public int getInt(String key, int defaultValue) {
    
        unparcel();
    
        Object o = mMap.get(key);
    
        if (o == null) {
    
            return defaultValue;
    
        }
    
        try {
    
            return (Integer) o;
    
        } catch (ClassCastException e) {
    
            typeWarning(key, o, "Integer", defaultValue, e);
    
            return defaultValue;
    
        }
    
    }
    
    public void putString(@Nullable String key, @Nullable String value) {
    
        unparcel();
    
        mMap.put(key, value);
    
    }
    

    unparcel函数我们后面再说,先看看在这两种函数中存取数据实际上都是在mMap中做的,这是BaseBundle中一个重要的参数,它存储着Bundle的数据。

    那么这个mMap中的数据又是哪里来的呢?

    下面我们就来看看这个unparcel函数,关键源码如下:

    /* package */ synchronized void unparcel() {
    
        synchronized (this) {
    
            ...
    
            ArrayMap<String, Object> map = mMap;
    
            if (map == null) {
    
                map = new ArrayMap<>(N);
    
            } else {
    
                map.erase();
    
                map.ensureCapacity(N);
    
            }
    
            try {
    
                mParcelledData.readArrayMapInternal(map, N, mClassLoader);
    
            } catch (BadParcelableException e) {
    
                if (sShouldDefuse) {
    
                    Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e);
    
                    map.erase();
    
                } else {
    
                    throw e;
    
                }
    
            } finally {
    
                mMap = map;
    
                mParcelledData.recycle();
    
                mParcelledData = null;
    
            }
    
            if (DEBUG) Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this))
    
                    + " final map: " + mMap);
    
        }
    
    }
    

    这里面涉及到了Bundle中的两个重要参数mMap和mParcelledData,mMap我们上面说过,另外一个mParcelledData则是一个Parcel对象。它是怎么来的呢?

    这要从activity的启动过程来说,参见探索startActivity流程及在Activity间是如何传递Intent的。在这篇文章的最后,我们看到在ActivityManagerNative中onTransact函数中处理binder接收的消息,其中就有这么一行:

    Bundle options = data.readInt() != 0
    
      ? Bundle.CREATOR.createFromParcel(data) : null;
    

    这行的作用就是从binder的消息中解析出传送过来的Bundle数据,继续看来Bundle.CREATOR:

    public static final Parcelable.Creator<Bundle> CREATOR =
    
        new Parcelable.Creator<Bundle>() {
    
        @Override
    
        public Bundle createFromParcel(Parcel in) {
    
            return in.readBundle();
    
        }
    
        @Override
    
        public Bundle[] newArray(int size) {
    
            return new Bundle[size];
    
        }
    
    };
    
    createFromParcel函数其实就是调用来Parcel的readBundle函数,代码如下:
    
    public final Bundle readBundle() {
    
        return readBundle(null);
    
    }
    
    public final Bundle readBundle(ClassLoader loader) {
    
        int length = readInt();
    
        ...
    
        final Bundle bundle = new Bundle(this, length);
    
        ...
    
        return bundle;
    
    }
    

    通过Bundle的构造函数来新建了一个对象,这个构造函数则调用了父类BaseBundle对应的构造函数如下:

    BaseBundle(Parcel parcelledData, int length) {
    
        readFromParcelInner(parcelledData, length);
    
    }
    
    private void readFromParcelInner(Parcel parcel, int length) {
    
        ...
    
        Parcel p = Parcel.obtain();
    
        p.setDataPosition(0);
    
        p.appendFrom(parcel, offset, length);
    
        if (DEBUG) Log.d(TAG, "Retrieving "  + Integer.toHexString(System.identityHashCode(this))
    
                + ": " + length + " bundle bytes starting at " + offset);
    
        p.setDataPosition(0);
    
        mParcelledData = p;
    
    }
    

    这样我们就在readFromParcelInner函数中找到了mParcelledData的来源,它实际上就是传送过来的Bundle序列化后的数据。

    那么就有了另外一个疑问,既然传送过来的只有mParcelledData,那么mMap中其实是空的,那么get函数怎么取到值的?

    这就是为什么每个get和put函数都先调用unparcel函数的原因。继续观察上面的unparcel函数,我们发现“mParcelledData.readArrayMapInternal(map, N, mClassLoader);”这句代码,调用了Parcel的readArrayMapInternal函数,并且传入了map,这个map后面会赋值给mMap,所以实际上两者是一致的。函数的源码如下:

    /* package */ void readArrayMapInternal(ArrayMap outVal, int N,
    
        ClassLoader loader) {
    
        if (DEBUG_ARRAY_MAP) {
    
            RuntimeException here =  new RuntimeException("here");
    
            here.fillInStackTrace();
    
            Log.d(TAG, "Reading " + N + " ArrayMap entries", here);
    
        }
    
        int startPos;
    
        while (N > 0) {
    
            if (DEBUG_ARRAY_MAP) startPos = dataPosition();
    
            String key = readString();
    
            Object value = readValue(loader);
    
            if (DEBUG_ARRAY_MAP) Log.d(TAG, "  Read #" + (N-1) + " "
    
                    + (dataPosition()-startPos) + " bytes: key=0x"
    
                    + Integer.toHexString((key != null ? key.hashCode() : 0)) + " " + key);
    
            outVal.append(key, value);
    
            N--;
    
        }
    
        outVal.validate();
    
    }
    

    在这个函数里就可以比较明显的看出来,从Parcel中分别读取出key和value,然后put进map中。这样就解决了之前的疑惑,unparcel函数的作用实际上是预处理,提前将序列化的数据反序列化并放入mMap中,然后Bundle再从mMap中存取数据。

    我们越来越接近真相了!读取value用的是readValue函数,代码如下:

    public final Object readValue(ClassLoader loader) {
    
        int type = readInt();
    
        switch (type) {
    
        case VAL_NULL:
    
            return null;
    
        case VAL_STRING:
    
            return readString();
    
        case VAL_INTEGER:
    
            return readInt();
    
        ...
    
        case VAL_SERIALIZABLE:
    
            return readSerializable(loader);
    
        ...
    
        default:
    
            int off = dataPosition() - 4;
    
            throw new RuntimeException(
    
                "Parcel " + this + ": Unmarshalling unknown type code " + type + " at offset " + off);
    
        }
    
    }
    

    根据不同的类型调用不同的函数来获得value,这里我们只关注Serializable这个类型,readSerializable代码如下:

    private final Serializable readSerializable(final ClassLoader loader) {
    
        String name = readString();
    
        ...
    
        try {
    
            ObjectInputStream ois = new ObjectInputStream(bais) {
    
                @Override
    
                protected Class<?> resolveClass(ObjectStreamClass osClass)
    
                        throws IOException, ClassNotFoundException {
    
                    if (loader != null) {
    
                        Class<?> c = Class.forName(osClass.getName(), false, loader);
    
                        if (c != null) {
    
                            return c;
    
                        }
    
                    }
    
                    return super.resolveClass(osClass);
    
                }
    
            };
    
            return (Serializable) ois.readObject();
    
        } catch (IOException ioe) {
    
            throw new RuntimeException("Parcelable encountered " +
    
                "IOException reading a Serializable object (name = " + name +
    
                ")", ioe);
    
        } catch (ClassNotFoundException cnfe) {
    
            throw new RuntimeException("Parcelable encountered " +
    
                "ClassNotFoundException reading a Serializable object (name = "
    
                + name + ")", cnfe);
    
        }
    
    }
    

    我们终于找到了最开始的崩溃错误的源头,在这里反序列化时需要根据类名去找到Class对象,这时就出问题了,因为通过上面我们知道,unparcel函数预处理时会将mParcelledData中所有的数据都解析出来,这时当解析到最开始的Boom类时,由于在本App中并不存在这个类,所以无法找到这个类,这样就出问题了。这样也解释了为什么任意key都会出问题。


    上面我们只说到了序列化的一种:serializable,我们知道在Android中还有另外一种推荐的序列化:Parcelable。

    至于这两种序列化的差别,请参考:Android中Intent/Bundle的通信原理及大小限制(Parcelable原理及与Serializable的区别)

    那么Parcelable会出现这种crash么,经测试也会出现这样的问题,但是报出的错误是不同的:

    E/AndroidRuntime: FATAL EXCEPTION: main
    
      Process: com.huichongzi.locationmocker, PID: 30769
    
      java.lang.RuntimeException: Unable to start activity ComponentInfo{com.test.test/com.test.test.MainActivity}: android.os.BadParcelableException: ClassNotFoundException when unmarshalling: com.example.Boom
    
      at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2369)
    
      at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2431)
    
      ...
    
      Caused by: android.os.BadParcelableException: ClassNotFoundException when unmarshalling: com.example.bennu.testapp.Boom
    
      at android.os.Parcel.readParcelableCreator(Parcel.java:2295)
    
      at android.os.Parcel.readParcelable(Parcel.java:2245)
    
      at android.os.Parcel.readValue(Parcel.java:2152)
    
      at android.os.Parcel.readArrayMapInternal(Parcel.java:2485)
    
      at android.os.BaseBundle.unparcel(BaseBundle.java:221)
    
      at android.os.BaseBundle.get(BaseBundle.java:280)
    
      at com.test.test.MainActivity.onCreate(MainActivity.java:142)
    
      ...
    

    情况其实与serializable差不多,差别在readValue函数这一步调用了另外一个函数readParcelable,源码如下:

    public final <T extends Parcelable> T readParcelable(ClassLoader loader) {
    
        Parcelable.Creator<?> creator = readParcelableCreator(loader);
    
        if (creator == null) {
    
            return null;
    
        }
    
        if (creator instanceof Parcelable.ClassLoaderCreator<?>) {
    
          Parcelable.ClassLoaderCreator<?> classLoaderCreator =
    
              (Parcelable.ClassLoaderCreator<?>) creator;
    
          return (T) classLoaderCreator.createFromParcel(this, loader);
    
        }
    
        return (T) creator.createFromParcel(this);
    
    }
    
    /** @hide */
    
    public final Parcelable.Creator<?> readParcelableCreator(ClassLoader loader) {
    
        String name = readString();
    
        if (name == null) {
    
            return null;
    
        }
    
        Parcelable.Creator<?> creator;
    
        synchronized (mCreators) {
    
            HashMap<String,Parcelable.Creator<?>> map = mCreators.get(loader);
    
            if (map == null) {
    
                map = new HashMap<>();
    
                mCreators.put(loader, map);
    
            }
    
            creator = map.get(name);
    
            if (creator == null) {
    
                try {
    
                    ClassLoader parcelableClassLoader =
    
                            (loader == null ? getClass().getClassLoader() : loader);
    
                    Class<?> parcelableClass = Class.forName(name, false /* initialize */,
    
                            parcelableClassLoader);
    
                    if (!Parcelable.class.isAssignableFrom(parcelableClass)) {
    
                        throw new BadParcelableException("Parcelable protocol requires that the "
    
                                + "class implements Parcelable");
    
                    }
    
                    Field f = parcelableClass.getField("CREATOR");
    
                    if ((f.getModifiers() & Modifier.STATIC) == 0) {
    
                        throw new BadParcelableException("Parcelable protocol requires "
    
                                + "the CREATOR object to be static on class " + name);
    
                    }
    
                    Class<?> creatorType = f.getType();
    
                    if (!Parcelable.Creator.class.isAssignableFrom(creatorType)) {
    
                        throw new BadParcelableException("Parcelable protocol requires a "
    
                                + "Parcelable.Creator object called "
    
                                + "CREATOR on class " + name);
    
                    }
    
                    creator = (Parcelable.Creator<?>) f.get(null);
    
                }
    
                catch (IllegalAccessException e) {
    
                    Log.e(TAG, "Illegal access when unmarshalling: " + name, e);
    
                    throw new BadParcelableException(
    
                            "IllegalAccessException when unmarshalling: " + name);
    
                }
    
                catch (ClassNotFoundException e) {
    
                    Log.e(TAG, "Class not found when unmarshalling: " + name, e);
    
                    throw new BadParcelableException(
    
                            "ClassNotFoundException when unmarshalling: " + name);
    
                }
    
                catch (NoSuchFieldException e) {
    
                    throw new BadParcelableException("Parcelable protocol requires a "
    
                            + "Parcelable.Creator object called "
    
                            + "CREATOR on class " + name);
    
                }
    
                if (creator == null) {
    
                    throw new BadParcelableException("Parcelable protocol requires a "
    
                            + "non-null Parcelable.Creator object called "
    
                            + "CREATOR on class " + name);
    
                }
    
                map.put(name, creator);
    
            }
    
        }
    
        return creator;
    
    }
    

    在readParcelable函数中调用readParcelableCreator函数来解析数据,在这个函数中就可以看到同样需要查找class来反序列化,而不同的是对Expection没有直接抛出,而是包装成BadParcelableException抛出的,这也是为什么crash信息有区别。

    你以为这样就结束了?还没有!

    让我们回到之前的unparcel函数,看看最后部分的代码:

    /* package */ synchronized void unparcel() {
    
        synchronized (this) {
    
            ...
    
            try {
    
                mParcelledData.readArrayMapInternal(map, N, mClassLoader);
    
            } catch (BadParcelableException e) {
    
                if (sShouldDefuse) {
    
                    Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e);
    
                    map.erase();
    
                } else {
    
                    throw e;
    
                }
    
            } finally {
    
                mMap = map;
    
                mParcelledData.recycle();
    
                mParcelledData = null;
    
            }
    
            if (DEBUG) Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this))
    
                    + " final map: " + mMap);
    
        }
    
    }
    

    可以看到mParcelledData.readArrayMapInternal是在一个try-catch中的,而是catch部分又catch了BadParcelableException,这里就有了一个小彩蛋:当sShouldDefuse为true时,这个错误就被吞掉了,而为false时继续抛出。

    那么这个sShouldDefuse的值怎么来的?

    在BaseBundle中sShouldDefuse默认是false,但是有一个函数可以设值,如下:

    /**
    
    * Set global variable indicating that any Bundles parsed in this process
    
    * should be "defused." That is, any {@link BadParcelableException}
    
    * encountered will be suppressed and logged, leaving an empty Bundle
    
    * instead of crashing.
    
    *
    
    * @hide
    
    */
    
    public static void setShouldDefuse(boolean shouldDefuse) {
    
        sShouldDefuse = shouldDefuse;
    
    }
    

    这个函数是static的,但是是隐藏的,所以我们不能直接使用。通过这个函数的注释我们可以知道,当设为true的时候,会吞掉所有BadParcelableException错误,这时会返回一个空的Bundle代替crash。

    根据网上相关android framwork层源码来看,高版本的android系统中默认将其设置为true,应该是google做的一步优化。具体那个版本开始的还有待调查。


    经过测试发现,不论serializable还是Parcelable在部分华为手机上并不会crash,估计是华为系统对此进行了优化,将问题直接吞掉了。

    而serializable的情况,android各个版本(8.0未测试)都还存在这个问题。

    Parcelable则像上面说的,高版本已经处理了,具体那个版本还需要调查一下。

    目前想到的解决方法是,在对外的Activity中如果获取bundle数据,try-catch一下。

    Android进阶之路系列:http://blog.csdn.net/column/details/16488.html

    相关文章

      网友评论

        本文标题:Bundle/Intent传递序列化参数暗藏杀机!

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