Bundle.setClassLoader()方法解析 BootClassLoader PathClassLoader

bundle经常会用,但是对setClassLoader()方法不是特别的理解,上网查了下也有很多不是很明白的同学,在此借用看到的文章记录一下原因。

在开发中可能有时候会遇到用Bundle传递一个Parcelable对象时出现ClassNotFoundException异常,而且这个异常有时候会出现有时候又不会出现,比如你在同一个进程的Activity间传递数据时就不会出现,但是你通过Messenger携带bundle进行进程通信时就会出现,具体是什么原因?

我先总体说下出现这个问题的原因和解决方案,然后再从源码分析为什么这样做.
首先出现这个异常的原因是因为ClassLoader不对造成的,我们应用中存在两种类加载器它们分别是BootClassLoader和PathClassLoader.
BootClassLoader用来加载系统类,PathClassLoader用来加载我们在应用中自己写的类.所以当类加载器为BootClassLoader时我们要加载自己写的类就会出现ClassNotFound异常.

在进程间通信中Messenger携带bundle传递Parcelable对象,在到达另一个进程后通过bundel取出parcelable对象时出现ClassNotFound异常是因为它通过BootClassLoader来加载我们的Parcelable对象

解决方案:
在bundle读取数据前加这行代码:
bundle.setClassLoader(getClass().getClassLoader());
这里有两点需要注意:
1.getClass().getClassLoader()不一定得到的是PathClassLoader,有些类的ClassLoader默认是BootClassLoader,这个后面会讲.所以在用这行代码之前还是要先确定是否拿到的是PathClassLoader.
2.如果bundle同时传了parcelable对象和其他基本类型的数据,比如int或者String.那么必须在getxxx之前就调用bundle.setClassLoader(getClass().getClassLoader());,否则同样会出错,原因见后面分析.

现在我们来具体分析:
我们通过源码看看Bundle是怎么获取数据的:
当bundle调用getXXX方法时会先调用一个unparcel()方法,该方法的源码如下:
Bundle.setClassLoader()方法解析 BootClassLoader PathClassLoader_第1张图片
主要关注 图中用红框圈起来的代码:
首先来看int N = mParcelledData.readInt();
这行代码会读出所有存在bundle里的数据总量.比如你存数据的代码是这样的:
bundle.putInt(“age”,22);
bundle.putString(“name”,”zhangsan”);
bundle.putParcelable(“parcelableData”,book);(book是一个parcelable对象)
那么N的值就是3,因为你在bundle存了三组数据.

然后执行到mParcelledData.readArrayMapInternal(mMap, N, mClassLoader);
mParcelledData的数据类型是Parcel,调用readArrayMapInternal会把mMap,N,mClassLoader作为参数传进去.现在看一下这个方法是干吗的:
Bundle.setClassLoader()方法解析 BootClassLoader PathClassLoader_第2张图片
还是关注红框部分的代码,你会发现key和value其实就是我们存放在bundle里面的对应数据的key和value值.这里就会从native读取我们的数据然后封装到outVal里面,也就是封装到我们传进来的mMap.这时候N=3,就会循环取出我们存在bundle的组数据.key的值就是我们的age,name,parcelableData.value就是每个key对应的值.

在读取value值时,会调用readValue(loader)方法,它把我们的类加载器传进去了.
我们现在再来看看该方法是 做什么的:
Bundle.setClassLoader()方法解析 BootClassLoader PathClassLoader_第3张图片
你会发现首先会判断读取的数据是什么类型,如果读取基本数据类型基本不必用到ClassLoader,如果是取集合或者是对象的数据就需要ClassLoader.

我们找到readParcelable(loader)方法,再看看里面它做了什么.
Bundle.setClassLoader()方法解析 BootClassLoader PathClassLoader_第4张图片
还是看红框的部分,系统会先调用readParcelableCreator方法,我们继续看:
Bundle.setClassLoader()方法解析 BootClassLoader PathClassLoader_第5张图片
看红框部分的代码,它会先判断传进来的ClassLoader是否为null,如果是的话就会取出本类(Parcel)的ClassLoader.问题就出在这里了.Parcel类的ClassLoader是BootClassLoader,如果通过它来加载我们自己写的类就出现了ClassNotFoundException异常.

有几点需要注意:
1.如果自己创建bundle对象,则bundle对象的ClassLoader默认为BootClassLoader.这个可以自己去看源码中bundle的构造函数.但是Activity跳转时Intent也是通过Bundle传递数据啊,那为什么在Activity跳转时传递Parcelable对象就不会报错呢?那是因为Intent会对传进去的bundle做一些处理,你调用bundle.getxxx方法取对象时,这时候bundle的ClassLoader会被赋予一个PathClassLoader,所以就不会出错.具体也是去看看源码就知道了.

2.通过Messenger在进程间传递数据时,如果通过bundle来携带数据,则从一个进程到另一个进程,bundle的ClassLoader会变成null,这个我也不知道为什么,先记住吧.

总结:
1.通过Messenger在进程间传递数据,如果通过bundle来携带数据则从另一个进程取出bundle时它的ClassLoader是null.bundle在取Parcelable数据时如果发现bundle的ClassLoader是null就会从Parcel类取出ClassLoader,而Parcel类的ClassLoader是BootClassLoader,要用它来加载我们自己的类就会出现异常.所以我们就要通过bundle.setClassLoader(getClass().getClassLoader());来自己设置bundle的类加载器.如果bundle的类加载器不是null的话就会通过bundle的类加载器来加载类.
而在进程间传递基本数据类型时不会出错,因为bundle读取基本数据类型不需要通过ClassLoader.

2.还有如果bundle里面同时传了基本数据类型和parcelable数据类型为什么要在getxxx之前就调用bundle.setClassLoader(getClass().getClassLoader());
这是因为bundle每个getxxx方法都会先调用unparcel方法,也就是我们第一次get数据前就得指定ClassLoader不然就会报错.


认真阅读这篇文章之后我作一下总结:

1.必须使用setClassLoader()方法的原因是在一些情况下比如跨进程通讯传递Bundle数据是,而且此时bundle中含有非基本类似的value值时,为了能加载非基本类似的value并且某一类型为自定义类型时,所以必须对用于加载类的ClassLoader进行设置,设置成PathClassLoader; 如果Bundle中没有非基本类型的value(另:如果除基本类型还只有系统类型),则不用通过此方法对classloader进行刻意指定,但是指定也是可以的。(为了保险,建议凡是bundle中有非基本类型value都使用bundle.setclassloader()方法对classloader进行指定为PathClassLoader)




你可能感兴趣的:(android)