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


相关阅读

插件化知识梳理(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 知识梳理系列:

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

推荐阅读更多精彩内容