ClassLoader类加载,是动态加载机制及现在火热的插件化机制中很基础但同时又很重要的知识点;今天我们就来讲解下.
前言
深入理解Android中的类加载器
ClassLoader类加载,是动态加载机制及现在火热的插件化机制中很基础但同时又很重要的知识点;
今天我们就来讲解下
一、ClassLoader介绍
1、Android中的ClassLoader
Java中的 ClassLoader可以加载 jar 文件和Class文件(本质时加载Class文件)。在Android中,它们加载到是dex文件;
Android中的ClassLoader类型分别是系统类加载器和自定义加载器。其中系统类加载器主要包括3种,分别是 BootClassLoader 、PathClassLoader 和 DexClassLoader;
BootClassLoader: Dalvik/ART虚拟机用于加载Android系统类的Loader,应用层通过获取父ClassLoader的最终项;
PathClassLoader: 我们知道,打包APK后实际上是把java文件都生成dex文件,而这个Loader就是在应用启动时,加载已安装APK的dex文件;
DexClassLoader: 常见的动态加载机制都用这个类,传入指定路径加载指定dex文件;
PathClassLoader和DexClasLoader都是继承自 dalviksystem.BaseDexClassLoader,它们的类加载逻辑全部写在BaseDexClassLoader中;
2、加载原理
ClassLoader使用的是双亲委托机制。双亲委派模型,旨在于让顶级父类加载器先加载类,若不成功,则一层层往下加载,最终到当前加载器。这样做的目的是保持类加载系统的稳定性,不会出现不同加载器加载同一个类时,出现多个类实例;
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException { Class<?> clazz = findLoadedClass(className); if (clazz == null) { ClassNotFoundException suppressed = null; try { clazz = parent.loadClass(className, false); } catch (ClassNotFoundException e) { suppressed = e; } if (clazz == null) { try { clazz = findClass(className); } catch (ClassNotFoundException e) { e.addSuppressed(suppressed); throw e; } } } return clazz; }
会先查询当前ClassLoader实例是否加载过此类,有就返回;
如果没有。查询Parent是否已经加载过此类,如果已经加载过,就直接返回Parent加载的类;
如果继承路线上的ClassLoader都没有加载,才由Child执行类的加载工作;
这样做有个明显的特点,如果一个类被位于树根的ClassLoader加载过,那么在以后整个系统的生命周期内,
这个类永远不会被重新加载;
如果希望通过动态加载的方式,加载一个新版本的dex文件,使用里面的新类替换原有的旧类,从而修复原有类的BUG,那么必须保证在加载新类的时候,旧类还没有被加载,因为如果已经加载过旧类,那么ClassLoader会一直优先使用旧类;
二、ClassLoader源码分析
1、PathClassLoader
Android主要关心的是PathClassLoader和DexClassLoader;
PathClassLoader用来操作本地文件系统中的文件和目录的集合。并不会加载来源于网络中的类。Android采用这个类加载器一般是用于加载系统类和它自己的应用类。这个应用类放置在data/data/包名下;
看一下PathClassLoader的源码,只有2个构造方法:
package dalvik.system; public class PathClassLoader extends BaseDexClassLoader { public PathClassLoader(String dexPath, ClassLoader parent) { super(dexPath, null, null, parent); } public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) { super(dexPath, null, libraryPath, parent); } }
2、DexClassLoader
DexClassLoader可以加载一个未安装的APK,也可以加载其它包含dex文件的JAR/ZIP类型的文件。DexClassLoader需要一个对应用私有且可读写的文件夹来缓存优化后的class文件;
而且一定要注意不要把优化后的文件存放到外部存储上,避免使自己的应用遭受代码注入攻击;
package dalvik.system; import java.io.File; public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); } }
PathClassLoader和DexClassLoader除了构造方法传参不同,其它的逻辑都是一样的;
要注意的是DexClassLoader构造方法第2个参数指的是dex优化缓存路径,这个值是不能为空的;
而PathClassLoader对应的dex优化缓存路径为null是因为Android系统自己决定了缓存路径;
Android中具体负责类加载的并不是哪个ClassLoader,而是通过DexFile的defineClassNative()方法来加载的;
3、BaseDexClassLoader
接下来我们看一下BaseDexClassLoader这个类:
BaseDexClassLoader的构造方法有四个参数:
dexPath,指的是在Androdi包含类和资源的jar/apk类型的文件集合,指的是包含dex文件。多个文件用“:”分隔开,用代码就是File.pathSeparator;
optimizedDirectory,指的是odex优化文件存放的路径,可以为null,那么就采用默认的系统路径;
libraryPath,指的是native库文件存放目录,也是以“:”分隔;
parent,parent类加载器;可以看到,在BaseDexClassLoader类中初始化了DexPathList这个类的对象。这个类的作用是存放指明包含dex文件、native库和优化目录;
# dalvik.system.BaseDexClassLoader public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); }
4、DexPathList
dalvik.system.DexPathList封装了dex路径,是一个final类,而且访问权限是包权限,也就是说外界不可继承,也不可访问这个类;
BaseDexClassLoader在其构造方法中初始化了DexPathList对象,我们来看一下DexPathList的源码,我们需要重点关注一下它的成员变量dexElements,它是一个Element[]数组,是包含dex的文件集合;
Element是DexPathList的一个静态内部类。DexPathList的构造方法有4个参数。从其构造方法中也可以看到传递过来的classLoade对象和dexPath不能为null,否则就抛出空指针异常;# dalvik.system.DexPathList
private final Element[] dexElements; public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) { if (definingContext == null) { throw new NullPointerException("definingContext == null"); } if (dexPath == null) { throw new NullPointerException("dexPath == null"); } if (optimizedDirectory != null) { if (!optimizedDirectory.exists()) { throw new IllegalArgumentException( "optimizedDirectory doesn't exist: " + optimizedDirectory); } // 如果文件不是可读可写的也会抛出异常 if (!(optimizedDirectory.canRead() && optimizedDirectory.canWrite())) { throw new IllegalArgumentException( "optimizedDirectory not readable/writable: " + optimizedDirectory); } } this.definingContext = definingContext; ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); // 通过makeDexElements方法来获取Element数组 // splitDexPath(dexPath)方法是用来把我们之前按照“:”分隔的路径转为File集合。 this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions); if (suppressedExceptions.size() > 0) { this.dexElementsSuppressedExceptions = suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]); } else { dexElementsSuppressedExceptions = null; } this.nativeLibraryDirectories = splitLibraryPath(libraryPath); }
5、makeDexElements
makeDexElements方法的作用是获取一个包含dex文件的元素集合;
# dalvik.system.DexPathList
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) { ArrayList<Element> elements = new ArrayList<Element>(); // 遍历打开所有的文件并且加载直接或者间接包含dex的文件。 for (File file : files) { File zip = null; DexFile dex = null; String name = file.getName(); if (file.isDirectory()) { // We support directories for looking up resources. // This is only useful for running libcore tests. // 可以发现它是支持传递目录的,但是说只测试libCore的时候有用 elements.add(new Element(file, true, null, null)); } else if (file.isFile()){ // 如果文件名后缀是.dex,说明是原始dex文件 if (name.endsWith(DEX_SUFFIX)) { // Raw dex file (not inside a zip/jar). try { //调用loadDexFile()方法,加载dex文件,获得DexFile对象 dex = loadDexFile(file, optimizedDirectory); } catch (IOException ex) { System.logE("Unable to load dex file: " + file, ex); } } else { // dex文件包含在其它文件中 zip = file; try { // 同样调用loadDexFile()方法 dex = loadDexFile(file, optimizedDirectory); } catch (IOException suppressed) { // 和加载纯dex文件不同的是,会把异常添加到异常集合中 /* * IOException might get thrown "legitimately" by the DexFile constructor if * the zip file turns out to be resource-only (that is, no classes.dex file * in it). * Let dex == null and hang on to the exception to add to the tea-leaves for * when findClass returns null. */ suppressedExceptions.add(suppressed); } } } else { System.logW("ClassLoader referenced unknown path: " + file); } // 如果zip或者dex二者一直不为null,就把元素添加进来 // 注意,现在添加进来的zip存在不为null也不包含dex文件的可能。 if ((zip != null) || (dex != null)) { elements.add(new Element(file, false, zip, dex)); } } return elements.toArray(new Element[elements.size()]); }
6、loadDexFile()、loadDex
通过上面的代码也可以看到,加载一个dex文件调用的是loadDexFile()方法;
# dalvik.system.DexPathList
private static DexFile loadDexFile(File file, File optimizedDirectory) throws IOException { // 如果缓存存放目录为null就直接创建一个DexFile对象返回 if (optimizedDirectory == null) { return new DexFile(file); } else { // 根据缓存存放目录和文件名得到一个优化后的缓存文件路径 String optimizedPath = optimizedPathFor(file, optimizedDirectory); // 调用DexFile的loadDex()方法来获取DexFile对象。 return DexFile.loadDex(file.getPath(), optimizedPath, 0); } }
DexFile的loadDex()方法如下,内部也做了一些调用。抛开这些细节来讲,它的作用就是加载DexFile文件,而且会把优化后的dex文件缓存到对应目录;
# dalvik.system.DexFile
static public DexFile loadDex(String sourcePathName, String outputPathName, int flags)throws IOException { /* * TODO: we may want to cache previously-opened DexFile objects. * The cache would be synchronized with close(). This would help * us avoid mapping the same DEX more than once when an app * decided to open it multiple times. In practice this may not * be a real issue. */ //loadDex方法内部就是调用了DexFile的一个构造方法 return new DexFile(sourcePathName, outputPathName, flags); } private DexFile(String sourceName, String outputName, int flags) throws IOException { if (outputName != null) { try { String parent = new File(outputName).getParent(); if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) { throw new IllegalArgumentException("Optimized data directory " + parent + " is not owned by the current user. Shared storage cannot protect" + " your application from code injection attacks."); } } catch (ErrnoException ignored) { // assume we'll fail with a more contextual error later } } mCookie = openDexFile(sourceName, outputName, flags); mFileName = sourceName; guard.open("close"); //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName); } private static long openDexFile(String sourceName, String outputName, int flags) throws IOException { // Use absolute paths to enable the use of relative paths when testing on host. return openDexFileNative(new File(sourceName).getAbsolutePath(), (outputName == null) ? null : new File(outputName).getAbsolutePath(), flags); } private static native long openDexFileNative(String sourceName, String outputName, int flags);
在BaseDexClassLoader对象构造方法内,创建了PathDexList对象。而在PathDexList构造方法内部,通过调用一系列方法,把直接包含或者间接包含dex的文件解压缩并缓存优化后的dex文件,通过PathDexList的成员变量 Element[] dexElements来指向这个文件;
到此我们就分析完了BaseDexClassLoader的构造方法;
7、loadClass
之前讲Java类加载器的时候已经说了,类加载是按需加载,也就是说当明确需要使用class文件的时候才会加载;
与在Java中的loadClass()方法主要流程是类似的,不过因为Android中BootClassLoader是用Java代码写的,所以可以直接当作系统类加载器的parent类加载器。在Android中如果parent类加载器找不到类,最终还是会调用ClassLoader对象自己的findClass()方法;
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException { Class<?> clazz = findLoadedClass(className); if (clazz == null) { ClassNotFoundException suppressed = null; try { clazz = parent.loadClass(className, false); } catch (ClassNotFoundException e) { suppressed = e; } if (clazz == null) { try { clazz = findClass(className); } catch (ClassNotFoundException e) { e.addSuppressed(suppressed); throw e; } } } return clazz; }
我们可以去看一下BaseDexClassLoader类的findClass()方法;
# dalvik.system.BaseDexClassLoader
@Override protected Class<?> findClass(String name) throws ClassNotFoundException { List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); // 调用DexPathList对象的findClass()方法 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; }
相关推荐
0评论