外掛化知識梳理(8) 類的動態載入原始碼分析

澤毛發表於2017-12-13

一、前言

外掛化知識梳理(7) - 類的動態載入入門 中,我們通過一個例子來演示瞭如何通過PathClassLoaderDexClassLoader實現類的動態載入。今天這篇文章,我們一起來對這個類載入內部的實現原始碼進行一次簡單的走讀。原始碼的地址為 地址 ,友情提示,需要翻牆。

二、原始碼解析

整個載入過程設計到的類,如下圖所示:

外掛化知識梳理(8)   類的動態載入原始碼分析

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) - 類的動態載入入門 中,這兩個變數的值為:

外掛化知識梳理(8)   類的動態載入原始碼分析

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 載入機制

外掛化知識梳理(8)   類的動態載入原始碼分析
以上就是整個構建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

外掛化知識梳理(8)   類的動態載入原始碼分析
之後,再通過反射,將這些DexFile加入到宿主的ClassLoader當中,而不是像我們之前那樣,為每一個外掛都建立一個ClassLoader
外掛化知識梳理(8)   類的動態載入原始碼分析
該方法中,會像DexFileList中所做的那樣,通過makeDexElement方法,為每一個DexFile建立一個Element物件:
外掛化知識梳理(8)   類的動態載入原始碼分析
最後,再將這個物件加入到pathList變數中:
外掛化知識梳理(8)   類的動態載入原始碼分析


更多文章,歡迎訪問我的 Android 知識梳理系列:

相關文章