美文网首页Android 插件化Android知识Android开发
插件化知识梳理(8) - 类的动态加载源码分析

插件化知识梳理(8) - 类的动态加载源码分析

作者: 泽毛 | 来源:发表于2017-06-10 23:07 被阅读142次

    相关阅读

    插件化知识梳理(1) - Small 框架之如何引入应用插件
    插件化知识梳理(2) - Small 框架之如何引入公共库插件
    插件化知识梳理(3) - Small 框架之宿主分身
    插件化知识梳理(4) - Small 框架之如何实现插件更新
    插件化知识梳理(5) - Small 框架之如何不将插件打包到宿主中
    插件化知识梳理(6) - Small 源码分析之 Hook 原理
    插件化知识梳理(7) - 类的动态加载入门
    插件化知识梳理(8) - 类的动态加载源码分析
    插件化知识梳理(9) - 资源的动态加载示例及源码分析
    插件化知识梳理(10) - Service 插件化实现及原理


    一、前言

    插件化知识梳理(7) - 类的动态加载入门 中,我们通过一个例子来演示了如何通过PathClassLoaderDexClassLoader实现类的动态加载。今天这篇文章,我们一起来对这个类加载内部的实现源码进行一次简单的走读。源码的地址为 地址 ,友情提示,需要翻墙。

    二、源码解析

    整个加载过程设计到的类,如下图所示:


    2.1 BaseDexClassLoader

    从上面的图中,我们可以看到DexClassLoaderPathClassLoader,都是BaseDexClassLoader的子类,而当我们调用以上两个类的构造方法时,其实都是调用了super()方法,也就是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());
            }
        }
    
    • dexPath:包含了类和资源的jar/apk文件,也就是上一篇例子当中的plugin_dex.jar,如果有多个文件,那么用File.pathSeparator进行分割,如果我们传入的是jar/apk文件,那么它会先将里面的.dex文件解压到内存当中,而如果是.dex文件,那么将不会有这一过程。
    • optimizedDirectory:这个参数目前已经被废弃了,没有什么作用。
    • librarySearchPathNative库的路径,同样可以用File.pathSeparator进行分割。
    • parent:父加载器的实例。

    DexClassLoaderPathClassLoader的区别就在于后者不支持传入optimizedDirectory这个参数,现在看来,对于最新的源码,这个参数已经被废弃了,那么这两个类其实是一样的。但是具体的实现,还是要看手机的安卓版本。

    2.2 DexPathList

    在前面的例子当中,获得PathClassLoader/DexClassLoader实例之后,调用了loadClass方法,它其实调用的是基类ClassLoader中的方法:

        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
        {
                //先查看该类是否已经被加载过
                Class c = findLoadedClass(name);
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {
                        if (parent != null) {
                            //优先调用父加载器进行加载.
                            c = parent.loadClass(name, false);
                        } else {
                            //2.如果没有父加载器,那么使用 bootstrap 进行加载。
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                         
                    }
                    if (c == null) {
                        //调用 findClass 方法。
                        long t1 = System.nanoTime();
                        c = findClass(name);
                    }
                }
                return c;
        }
    

    对于BaseDexClassLoader,最终会走到他们重写的findClass方法,而该方法又会去通过pathList去寻找,如果找不到,那么就会抛出异常,

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

    并且其它的公有方法,都是通过pathList去寻找的,因此这个pathList是如何构成的就是我们分析源码的关键。

        public DexPathList(ClassLoader definingContext, String dexPath,
                String librarySearchPath, File optimizedDirectory) {
            this.definingContext = definingContext;
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    
            //dex/resource (class path) elements
            this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
            //application native library directories
            this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
    
            //system native library directories
            this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);
            List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
            allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
    
            //native library path elements
            this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
    
            if (suppressedExceptions.size() > 0) {
                this.dexElementsSuppressedExceptions = suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
            } else {
                dexElementsSuppressedExceptions = null;
            }
        }
    

    在上面的构造方法中,最关键的就是通过makeDexElementsmakePathElements来构建dexElementsnativeLibraryPathElements,它们两个分别为ElementNativeLibraryElement类型的数组,在 插件化知识梳理(7) - 类的动态加载入门 中,这两个变量的值为:

    2.3 makeDexElements

    下面,我们来看一下makeDexElements的内部实现逻辑:

        private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
                List<IOException> suppressedExceptions, ClassLoader loader) {
          Element[] elements = new Element[files.size()];
          int elementsPos = 0;
          /*
           * Open all files and load the (direct or contained) dex files up front.
           */
          for (File file : files) {
              if (file.isDirectory()) {
                  // We support directories for looking up resources. Looking up resources in
                  // directories is useful for running libcore tests.
                  elements[elementsPos++] = new Element(file);
              } else if (file.isFile()) {
                  String name = file.getName();
                  if (name.endsWith(DEX_SUFFIX)) {
                      // Raw dex file (not inside a zip/jar).
                      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 {
                      DexFile dex = null;
                      try {
                          dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      } catch (IOException suppressed) {
                          /*
                           * 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);
                      }
                      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;
        }
    

    这里面,最终会创建一个和files相等大小的elements数组,其最终目的是为每个Element中的dexFile赋值,而dexFile则是通过loadDexFile方法创建的。

        private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader, Element[] elements) throws IOException {
            if (optimizedDirectory == null) {
                return new DexFile(file, loader, elements);
            } else {
                String optimizedPath = optimizedPathFor(file, optimizedDirectory);
                return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
            }
        }
    

    这里面,会根据optimizedDirectory的区别,来调用DexFile不同的函数,我们先看静态方法,可以看到,它里面也是调用了new DexFile来返回一个DexFile对象:

        static DexFile loadDex(String sourcePathName, String outputPathName,
            int flags, ClassLoader loader, DexPathList.Element[] elements) 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.
             */
            return new DexFile(sourcePathName, outputPathName, flags, loader, elements);
        }
    

    这里面,又会调用openDex方法,得到一个mCookie变量,在前面的例子中,这个mCookie是一个long型的对象,对于里面的内部实现,可以参见这篇文章 跟踪源码分析 Android DexClassLoader 加载机制


    以上就是整个构建pathList的过程,下面,我们来看一下前面所说的DexFileListfindClass的过程。

    2.4 寻找 Class 的过程

    DexFileList中寻找类的代码如下:

        public Class<?> findClass(String name, List<Throwable> suppressed) {
            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;
        }
    

    它会遍历先前构建的Element数组,调用每个的findClass方法,直到找到为止,而Element中的该方法,则会调用在2.3中创建的DexFileloadClassBinaryName来查找该Class对象:

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

    三、小结

    经过一次简单的源码走读,我们可以知道,DexClassLoader/PathClassLoader的内部,为每一个传入的jar/apk/dex文件,都创建了一个Element变量,它们被保存在DexFileList当中,而每一个Element变量中,又包含了一个关键的DexFile类,之后我们通过DexClassLoader/PathClassLoader寻找类或者资源时,其实最终都是调用了DexFile中的Native方法,如果有兴趣的同学可以去研究这些方法的内部实现。

    最后,简单地提一下,在Small的源码当中,并没有直接使用DexClassLoader/PathClassLoader,它首先是直接调用了DexFile的静态方法来为每一个插件创建一个DexFile


    之后,再通过反射,将这些DexFile加入到宿主的ClassLoader当中,而不是像我们之前那样,为每一个插件都创建一个ClassLoader

    该方法中,会像DexFileList中所做的那样,通过makeDexElement方法,为每一个DexFile创建一个Element对象:

    最后,再将这个对象加入到pathList变量中:

    更多文章,欢迎访问我的 Android 知识梳理系列:

    相关文章

      网友评论

        本文标题:插件化知识梳理(8) - 类的动态加载源码分析

        本文链接:https://www.haomeiwen.com/subject/klmqqxtx.html