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是重点。

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

    Android ClassLoader 继承.PNG

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中的双亲委派流程。

  • Android 双亲委派机制.jpg
  • 带上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指定的位置查找是否有要用来覆盖的类。

    ​ 新的双亲委派流程图如下:


    dexclassloader加入双亲委派.png

参考

Android动态加载之ClassLoader详解

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

热修复入门:Android 中的 ClassLoader

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容