前言:
我们开发android写的都是java代码,java代码都是写在class里面,虚拟机需要把class文件加载进来才能创建实例对象并工作,完成类加载的角色就是ClassLoader。那这个ClassLoader是如保工作的呢?
1. 一个Android应用有几个ClassLoader实例?
一个运行的Android应用至少有2个ClassLoader,一个是BootClassLoader(系统启动时创建的),一个是PathClassLoader(应用启动时创建的,用于加载/data/data/packagename/apkname.apk)
2. 双亲代理模型
创建一个ClassLoader实列,需要使用一个现有的ClassLoader实例作为新创建的实例的Parent。
ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
if (parentLoader == null && !nullAllowed) {
throw new NullPointerException("parentLoader == null && !nullAllowed");
}
parent = parentLoader;
}
这样一个Android应用,甚至整个Android系统里所有的ClassLoader实例都会被一棵树关联起来,这也是ClassLoader的双亲代理模型(Parent-Delegation Model)的特点。
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
//1. 先查询当前ClassLoader实例是否加载过此类,有则直接返回
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
//2.查找父类的ClassLoader是否加载过此类,有则返回
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
//3.若继承路线上的ClassLoader都没有加载,则由Child执行此类的加载工作
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
这种从继承路上查询类加载实例,如果一个类被位于树根的ClassLoader加载过,在以后的整个系统的生命周期内,这个类永远不会被重新加载。
注意点:
在升级一些逻辑代码,通过动态加载dex文件获得新类替换原有的旧类时,为达到修复原有类的bug,就必须保证在加载新类的时候,旧类还没有被加载,若已加载过旧类,那么ClassLoader会一直优先使用旧类。
Java中判断一个类是否被加载过是通过判断
同一个Class = 相同的ClassName+PackageName+ClassLoader
所以如果使用不同的ClassLoader加载同一个类,也会造成类型不一样。
3.双亲代理模型的作用
1) 共享功能:一些Framework层级的类一旦被顶层的ClassLoader加载过就缓存在内存里,以后任何地方要用,都不需要重新加载;
2) 隔离功能:不同继承路线上的ClassLoader加载的类肯定不是同一个类,这样限制避免用户自己的代码冒充核心类库的类访问枋心库包里可见的成员变量。
4. android中两种类加载器
Google中的multidex方案解决了65535问题,即一个apk文件包含多个dex文件,除了主dex其他dex都是以资源的形式被加载进来。
前面说过Android虚拟机的类加载机制,同一个类只会被加载一次,所以热修复技术中,让修复后的类替换原有的类必须让补丁包的类优先被加载,插到原有dex之前。
android中的两个主要ClassLoader,都继承自BaseDexClassLoader,加载类都是通过findClass方法。
(1) PathClassLoder:加载系统的类和主dex中的类
我们看一下PathClassLoder类文件,发现里边只有两个构造函数
/**只能加载本地文件系统或是目录,但不能从网络加载,用来加载系统类和我们的应用程序*/
public class PathClassLoader extends BaseDexClassLoader {
/**
* @param dexPath jar/apk路径下包含的文件或类,多个用/分开
* @param parent 加载器的父类
*/
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
/**
* @param dexPath jar/apk中包含的类或资源,多个有/分隔
* @param libraryPath 包含本地库的目录,多个用/分隔,可为空
* @param parent 加载器的父类
*/
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
PathClassLoader的两个构造函数都会直接调用父类构造器函数,最终加载类还是会调用BaseDexClassLoader中的findClass方法。
(2) DexClassLoader:加载其他类的加载器
/**加载包含了classex.dex对象的jar/apk文件,可用来加载没有安装的应用 */
public class DexClassLoader extends BaseDexClassLoader {
/**
* @param dexPath jar/apk中包含的类或资源,多个有/分隔
* @param optimizedDirectory dex文件被加载后会被编译器优化,优化后dex存放的路径,不可为null
* @param libraryPath 包含libraries的目录列表,多个可用/分隔,可为null
* @param 父类构造器
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
可见两个类加载器都调用了父类的构造函数,只是PathClassLoader中的第二个参数为空,而DexClassLoader,第二个参数传的是new File(optimizedDirectory)。
我们再来看看父类BaseDexClassLoader的实现:
(3)BaseDexClassLoader
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
/**
* @param dexPath jar/apk中包含的类或资源,多个有/分隔
* @param optimizedDirectory dex文件被加载后会被编译器优化,优化后dex存放的路径,不可为null
* @param libraryPath 包含libraries的目录列表,多个可用/分隔,可为null
* @param 父类构造器
*/
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
}
从BaseDexClassLoader的构造函数中我们可以看出第二个参数optimizedDirectory是不能为空的,可是PathClassLoader里传的第二个参数为空,这按道理不符合调用原则。接着网下看,BaseDexClassLoader中的findClass方法调用的是DexPathList的findClass方法。
(4)DexPathList
我们再来看看DexPathList中的findClass方法
/**这个类中维护这一个dexElements的数组,在findClass的时候会遍历数组来查找*/
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
//给一个dexElements数组赋值,这个其实是存放我们的dex文件数组
this.dexElements =
makeDexElements(splitDexPath(dexPath), optimizedDirectory);
//nativeLibraryDirectories就是lib库
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
在DexPathList类中如果optimizedDirectory是用来缓存我们需要加载的dex文件的,并创建一个DexFile对象,如果它为null,那么会直接使用dex文件原有的路径来创建DexFile对象。因为应用安装后会进行优化,优化后的dex存在于/data/dalvik-cache目录下。
从代码中可以看出optimizedDirectory必须是一个内部存储路径,无论哪种动态加载,加载的可执行文件一定要存放在内部存储。DexClassLoader可以指定自己的optimizedDirectory,所以它可以加载外部的dex,因为这个dex会被复制到内部路径的optimizedDirectory;而PathClassLoader没有optimizedDirectory,所以它只能加载内部的dex,这些大都是存在系统中已经安装过的apk里面的。
我们再来看看DexPathList中的findClass方法,它其实遍历了之前所有的DexFile实例,其实就是遍历了所有加载过的dex文件,再调用dex.loadClassBinaryName方法
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
loadClassBinaryName调用了Native方法defineClass加载类。
通过上面的分析,我们可以得出以下结论:
android中每个应用的类加载器至少有两个,一个是系统类加载器BootClassLoader用来加载系统类,一个是PathClassLoader用来加载应用的类;
android中应用的类加载器主要有两种,分别是PathClassLoader和DexClassLoader,PathClassLoader只能用来加载已安装应用的dex文件,而DexClassLoader可以用来加载未安装的apk\java\dex等文件;
- android类加载会使用双亲代理模型进行加载类,主要是通过树型结构,先从child ClassLoader节点查找此类实例是否存在,不存在则到parent的CloassLoader中查找,存在返回,不存在再由child ClassLoder进行加载类。