ClassLoader浅析(二) —— Android ClassLoader

  • 本篇是基于上一篇ClassLoader(一) —— Java ClassLoader。
  • Android虚拟机和JVM一样,运行程序时首先要将对应的类加载到内存中。但是和JVM不同的是Android虚拟机上运行的是Dex字节码,因此Android的ClassLoader和Java的ClassLoader有一定不同。

Android 类加载

  • Android中的类加载器有

    1. BootClassLoader
    2. URLClassLoader
    3. PathClassLoader
    4. DexClassLoader
    5. BaseDexClassLoader
    6. ClassLoader

    其中BootClassLoader,PathClassLoader和DexClassLoader是重点。

    看看他们之间的继承关系:

    ClassLoader浅析(二) —— Android ClassLoader

BootClassLoader

  • BootClassLoader在Android系统启动的时候就被创建,它用于加载一些Android系统框架的类,包括APP用到的一些系统类。它是ClassLoader中的内部类,由Java实现。这个内部类是包内可见,所以我们没法使用。

URLClassLoader

  • 它继承自SecureClassLoader,用来通过URl路径从jar文件和文件夹中加载类和资源。由于 dalvik 不能直接识别jar,所以在 Android 中无法使用这个加载器。

PathClassLoader

  • PathClassLoader是用来加载Android系统类和应用的类。

  • 在Dalvik虚拟机上PathClassLoader只能加载已安装的apk的dex文件。但在ART虚拟机上可以加载未安装的apk的dex文件。

  • PathClassLoader的源码,只有2个构造方法:

    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);
        }
    }
    

    由于都是只调用了父类BaseDexClassLoader的构造方法,所以每个参数的含义将会留到BaseDexClassLoader再分析。

DexClassLoader

  • DexClassLoader可以加载一个未安装的APK,也可以加载其它包含dex文件的JAR/ZIP类型的文件,可以从 SD 卡上加载包含 class.dex 的 .jar 和 .apk 文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的 dex 的加载。

  • 上面说dalvik不能直接识别jar,DexClassLoader却可以加载jar文件,这难道不矛盾吗?其实在BaseDexClassLoader里对".jar",".zip",".apk",".dex"后缀的文件最后都会生成一个对应的dex文件,所以最终处理的还是dex文件,而URLClassLoader并没有做类似的处理。

  • DexClassLoader的源码,只有1个构造方法:

    public class DexClassLoader extends BaseDexClassLoader {
        public DexClassLoader(String dexPath, String optimizedDirectory,
                String libraryPath, ClassLoader parent) {
            super(dexPath, new File(optimizedDirectory), libraryPath, parent);
        }
    }
    

    由于只是调用了父类BaseDexClassLoader的构造方法,所以每个参数的含义将会留到BaseDexClassLoader再分析。

BaseDexClassLoader

  • PathClassLoader和DexClassLoader都继承自BaseDexClassLoader,其中的主要逻辑都是在BaseDexClassLoader完成的。

  • 先来填下上文留下的坑,看看BaseDexClassLoader的构造方法:

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                String librarySearchPath, ClassLoader parent) {
         super(parent);
         this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
         if (reporter != null) {
         	reporter.report(this.pathList.getDexPaths());
    	}
    }
    

    BaseDexClassLoader的构造函数包含四个参数,分别为:

    1. **dexPath:**指目标类所在的APK或jar文件的路径,类装载器将从该路径中寻找指定的目标类,该类必须是APK或jar的全路径。如果要包含多个路径,路径之间必须使用特定的分割符分隔,分隔符通常为":"。
    2. **optimizedDirectory:**由于dex文件被包含在APK或者Jar文件中,因此在装载目标类之前需要先从APK或Jar文件中解压出dex文件,该参数就是制定解压出的dex 文件存放的路径。如果该参数为null,则设置默认路径为/data/dalvik-cache 目录。
    3. **libraryPath:**指目标类中所使用的C/C++库存放的路径,多个路径也是以“:”分隔。
    4. **parent:**父类加载器,遵从双亲委派。
  • 在BaseDexClassLoader中的成员变量private final DexPathList pathList十分重要,ClassLoader中的抽象方法findClass()findResource()findResources()findLibrary()均是基于 pathList 来实现的(省略了部分源码):

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    	List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    	Class c = pathList.findClass(name, suppressedExceptions);
    	...
    	return c;
    }
    
    @Override
    protected URL findResource(String name) {
    	return pathList.findResource(name);
    }
    
    @Override
    protected Enumeration<URL> findResources(String name) {
    	return pathList.findResources(name);
    }
    
    @Override
    public String findLibrary(String name) {
    	return pathList.findLibrary(name);
    }
    

    那我们来看看DexPathList中做了什么。

DexPathList

  • 在DexPathList中有个private Element[] dexElements是它的重点,Element是DexPathList的内部类,有下面的成员变量:

    static class Element {
            private final File path;
            private final DexFile dexFile;
            private ClassPathURLStreamHandler urlHandler;
            private boolean initialized;
    }
    
  • 让我们看看Element数组是如果生成的:

    //在DexPathList构造方法中调用makeDexElements方法生成
    public DexPathList(ClassLoader definingContext, String dexPath,
                String librarySearchPath, File optimizedDirectory) {
        ...
      	//splitDexPath()方法是把String切割成多个地址,再把每个地址生成File,该方法返回List<File>
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                               suppressedExceptions, definingContext); 
        ...
    }
    
        private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
                List<IOException> suppressedExceptions, ClassLoader loader) {
          Element[] elements = new Element[files.size()];
          int elementsPos = 0;
          //打开所有文件并预先加载(直接或包含)dex文件
          for (File file : files) {
              if (file.isDirectory()) {
                  // 如果是文件夹,则直接添加 Element,这个一般是用来处理 native 库和资源文件
                  elements[elementsPos++] = new Element(file);
              } else if (file.isFile()) {
                  String name = file.getName();
                  if (name.endsWith(DEX_SUFFIX)) {
                      // 直接是.dex文件,而不是zip/jar文件(apk归为zip),则直接加载dex文件
                      try {
                          DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
                          if (dex != null) {
                              elements[elementsPos++] = new Element(dex, null);
                          }
                      } catch (IOException suppressed) {
                          System.logE("Unable to load dex file: " + file, suppressed);
                          suppressedExceptions.add(suppressed);
                      }
                  } else {
    				//如果是zip/jar文件(apk归为zip),加载dex文件。
                      DexFile dex = null;
                      try {
                          dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      } catch (IOException suppressed) {
                          suppressedExceptions.add(suppressed);
                      }
    				//如果dex为空则不传进Element,file文件是肯定会传进的
                      if (dex == null) {
                          elements[elementsPos++] = new Element(file);
                      } else {
                          elements[elementsPos++] = new Element(dex, file);
                      }
                  }
              } else {
                  System.logW("ClassLoader referenced unknown path: " + file);
              }
          }
          if (elementsPos != elements.length) {
              elements = Arrays.copyOf(elements, elementsPos);
          }
          return elements;
        }
    

    DexPathList.loadDexFile() 方法最终会调用 JNI 层的方法来读取 dex 文件,这里不再深入探究,有兴趣的可以阅读 从源码分析 Android dexClassLoader 加载机制原理 这篇文章深入了解。

  • 获得了Element数组就可以通过DexPathList.findClass()方法来对类进行加载了,源码如下:

    public Class<?> findClass(String name, List<Throwable> suppressed) {
        // 遍历 dexElements  数组,依次寻找对应的 class,一旦找到就终止遍历
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
             	return clazz;
         	}
         }
         if (dexElementsSuppressedExceptions != null) {
         	suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
         }
    	return null;
    }
    

    ​ 这里有关于热修复实现的一个点,就是将补丁 dex 文件放到 dexElements 数组前面,这样在加载 class 时,优先找到补丁包中的 dex 文件,加载到 class 之后就不再寻找,从而原来的 apk 文件中同名的类就不会再使用,从而达到修复的目的。

ClassLoader

  • ClassLoader是所有ClassLoader的最终父类。我们来瞧瞧ClassLoader的源码:

    public abstract class ClassLoader {
    
        static private class SystemClassLoader {
            public static ClassLoader loader = ClassLoader.createSystemClassLoader();
        }
    
        //父加载器
        private final ClassLoader parent;
    
        private static ClassLoader createSystemClassLoader() {
            String classPath = System.getProperty("java.class.path", ".");
            String librarySearchPath = System.getProperty("java.library.path", "");
            //可以看出构造PathClassLoader传入了BootClassLoader
            return new PathClassLoader(classPath, librarySearchPath,BootClassLoader.getInstance());
        }
        
         public static ClassLoader getSystemClassLoader() {
            return SystemClassLoader.loader;
        }
    
        private ClassLoader(Void unused, ClassLoader parent) {
            this.parent = parent;
        }
    
        protected ClassLoader(ClassLoader parent) {
            this(checkCreateClassLoader(), parent);
        }
    
        protected ClassLoader() {
            //外界没有传入指定父加载器的情况
            this(checkCreateClassLoader(), getSystemClassLoader());
        }
    
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            return loadClass(name, false);
        }
    
        protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException{
                // 检查是否已经加载过
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    // 没有被加载过
                     // 首先委派给父类加载器加载
                    try {
                        if (parent != null) {
                            //父加载器不为空则调用父加载器的loadClass
                            c = parent.loadClass(name, false);
                        } else {
                            //父加载器为空
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        // ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                    }
                    if (c == null) {
                       // 如果父类加载器无法加载,才尝试加载
                        c = findClass(name);
                    }
                }
                return c;
        }
        
        private Class<?> findBootstrapClassOrNull(String name){
            return null;
        }
        ...
    }
    

    ​ 从上面可以看出Android中的ClassLoader和Java中的区别并不大,ClassLoader的构造方法也是分为指定parent和不指定parent两种,不同的是在外界不指定parent的情况下,会通过createSystemClassLoader()来获取到PathClassLoader作为parent。直白的说,一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是PathClassLoader,且此PathClassLoader父构造器为BootClassLoader。

    ​ 可以看到Android中的ClassLoader.loadClass()和Java中的基本是不变的,都是实现了双亲委托。甚至就连在Java中调用BootstrapClassLoader的findBootstrapClassOrNull方法也保留着,然而android中并没有BootstrapClassLoader,而且并没有出现因为某个ClassLoader不是Java实现的而导致无法持有父加载器的情况。。。所以在这里该方法直接返回nuil。

双亲委派

  • 通过从ClassLoader.loadClass()方法中我们可以明白Android ClassLoader中的双亲委派流程。

  • ClassLoader浅析(二) —— Android ClassLoader

  • 带上DexClassLoader一起玩双亲委派:

    ​ ClassLoader的构造方法中有一个参数是parent,那么是不是有办法把PathClassLoader的parent替换成我们想要的DexClassLoader,在把DexClassLoader的parent设置成BootClassLoader,再加上父委托的机制,查找类的过程就变成BootClassLoader->DexClassLoader->PathClassLoader,这样我们就能够通过双亲委派先去加载外部apk的类了。我们可以通过反射来实现我们的设想。

    public static void loadApk(Context context, String apkPath) {
        File dexFile = context.getDir("dex", Context.MODE_PRIVATE);
        File apkFile = new File(apkPath);
    	//获取到PathClassLoader
        ClassLoader classLoader = context.getClassLoader();
        //创建DexClassLoader并设置父加载器为BootClassLoader
        DexClassLoader dexClassLoader = new DexClassLoader(apkFile.getAbsolutePath(),
                dexFile.getAbsolutePath(), null, classLoader.getParent());
        try {
            //通过反射获取到PathClassLoader的parent成员变量
            Field fieldClassLoader = ClassLoader.class.getDeclaredField("parent");
            if (fieldClassLoader != null) {
                //把parent成员变量赋值为DexClassLoader
                fieldClassLoader.setAccessible(true);
                fieldClassLoader.set(classLoader, dexClassLoader);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    

    ​ 这样就实现了DexClassLoader的插入,每次加载app的类之前都会通过DexClassLoader指定的位置查找是否有要用来覆盖的类。

    ​ 新的双亲委派流程图如下:
    ClassLoader浅析(二) —— Android ClassLoader

参考

Android动态加载之ClassLoader详解

苹果核 - Android插件化实践(2)–ClassLoader

热修复入门:Android 中的 ClassLoader