Android动态加载之ClassLoader加载和插件热修复的机制原理详解
追忆似水年华 · 275浏览 · 发布于2021-10-25 +关注

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

                      实际上BaseDexClassLoader调用的是其成员变量DexPathList pathList的findClass()方法;

                      # dalvik.system.DexPathList


                      public Class findClass(String name, List<Throwable> suppressed) { 
                          // 遍历Element 
                          for (Element element : dexElements) { 
                              // 获取DexFile,然后调用DexFile对象的loadClassBinaryName()方法来加载Class文件。 
                              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; 
                      }

                        • 从上面的代码中我们也可以看到,实际上DexPathList最终还是遍历其自身的Element[]数组,获取DexFile对象来加载Class文件;

                        • DexPathList构造方法内是调用其makeDexElements()方法来创建Element[]数组的,而且也提到了如果zip文件或者dex文件二者之一不为null,就把元素添加进来,而添加进来的zip存在不为null也不包含dex文件的可能;

                        • 上面的代码中也可以看到,获取Class的时候跟这个zip文件没什么关系,调用的是dex文件对应的DexFile的方法来获取Class;

                        • 数组的遍历是有序的,假设有两个dex文件存放了二进制名称相同的Class,类加载器肯定就会加载在放在数组前面的dex文件中的Class;

                        • 现在很多热修复技术就是把修复的dex文件放在DexPathList中Element[]数组的前面,这样就实现了修复后的Class抢先加载了,达到了修改bug的目的;

                        • Android加载一个Class是调用DexFile的defineClass()方法。而不是调用ClassLoader的defineClass()方法。这一点与Java不同,毕竟Android虚拟机加载的dex文件,而不是class文件;

                        # dalvik.system.DexFile


                        public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) { 
                            return defineClass(name, loader, mCookie, suppressed); 
                        } 
                        private static Class defineClass(String name, ClassLoader loader, long cookie, 
                                                         List<Throwable> suppressed) { 
                            Class result = null; 
                            try { 
                                result = defineClassNative(name, loader, cookie); 
                            } catch (NoClassDefFoundError e) { 
                                if (suppressed != null) { 
                                    suppressed.add(e); 
                                } 
                            } catch (ClassNotFoundException e) { 
                                if (suppressed != null) { 
                                    suppressed.add(e); 
                                } 
                            } 
                            return result; 
                        }

                          # java.lang.ClassLoader

                          protected final Class<?> defineClass(String className, byte[] classRep, int offset, int length, 
                                  ProtectionDomain protectionDomain) throws java.lang.ClassFormatError { 
                              throw new UnsupportedOperationException("can't load this type of class file"); 
                          }

                            Android中加载一个类是遍历PathDexList的Element[]数组,这个Element包含了DexFile,调用DexFile的方法来获取Class文件,如果获取到了Class,就跳出循环。否则就在下一个Element中寻找Class;

                            三、热修复的原理

                            利用pathClassLoader 的 对dex 文件进行替换,补丁 dex 文件加载到Element对象,并插入到 dexElement前面,具体还是使用反射;

                            双亲委派:当一个class文件被加载时,classloader发现已经加载过则不会重新加载,如果没加载过则递归地把这个请求委派给父类加载器完成。当父加载器找不到指定的类时,子加载器尝试自己加载

                            步骤

                            关键是ClassLoader中loadeClass() 方法, loadClass()双亲委托机制

                            一个dex被加载的步骤

                            先从自己缓存中取

                            自己缓存没有,就在 父 ClassLoader 要 (parent.loadClass())

                            父 ClassLoader 没有,就自加载(findClass)

                            makeDexElements(将dex文件或压缩包中的信息保存到dexElements中)

                            findCLass(遍历Element,并将Element转成Dex文件,获取Dex文件中的Class文件,直到找到对应的class文件位置)

                            总结

                            了解各种加载流程,还是需要多深入源码,Android-ClassLoader实现逻辑算是非常清晰易懂,但对我们日常开发如插件化方案会有非常大的帮助;


                            相关推荐

                            android下vulkan与opengles纹理互通

                            talkchan · 819浏览 · 2020-11-23 10:37:39
                            Android 使用RecyclerView实现轮播图

                            奔跑的男人 · 1850浏览 · 2019-05-09 17:11:13
                            微软发布新命令行工具 Windows Terminal

                            吴振华 · 700浏览 · 2019-05-09 17:15:04
                            Facebook 停止屏蔽部分区块链广告

                            · 594浏览 · 2019-05-09 17:20:08
                            加载中

                            0评论

                            评论
                            分类专栏
                            小鸟云服务器
                            扫码进入手机网页