1. 程式人生 > >Android ClassLoader淺析

Android ClassLoader淺析

前言

最近在看Tinker的原理,發現核心是通過ClassLoader做的,由於之前也從未接觸過ClassLoader趁著上週末看了安卓ClassLoader相關原始碼,這裡分享一發安卓的ClassLoader和熱更新的實現原理。

ClassLoader

首先我們要知道,程式在執行時要把對應的類載入到記憶體,在安卓上來說就是把Dex檔案中的類載入到記憶體,這個載入流程是通過ClassLoader實現的。因此如果我們想要動態載入自己的類,就得從ClassLoader上做文章,那麼接下我們先看看安卓的ClassLoader

public abstract class ClassLoader
{ private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent; } protected ClassLoader(ClassLoader parent) { this(checkCreateClassLoader(), parent); } protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader(
)); } public static ClassLoader getSystemClassLoader() { return SystemClassLoader.loader; } static private class SystemClassLoader { public static ClassLoader loader = ClassLoader.createSystemClassLoader(); } private static ClassLoader createSystemClassLoader
() { String classPath = System.getProperty("java.class.path", "."); String librarySearchPath = System.getProperty("java.library.path", ""); return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance()); } public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { try { if (parent != null) { 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) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); } } return c; } protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } }

可以看到在構造方法的時候我們需要傳入一個父ClassLoader,預設沒傳情況下是以PathClassLoader作為父載入器的。

然後載入類是呼叫的loadClass()方法,他是先判斷是本ClassLoader是否已經載入過,如果沒載入過就讓父載入器載入,然後父載入器看自己是否載入過,如果沒載入過讓祖父載入器載入,然後祖父載入器沒找到,父載入器在找,父載入器沒找到,子載入器再找,這也就是我們常說的雙親委派模式。

採取雙親委派模式主要有兩點好處:

  1. 避免重複載入,如果已經載入過一次Class,就不需要再次載入,而是先從快取中直接讀取。
  2. 更加安全,如果不使用雙親委託模式,就可以自定義一個String類來替代系統的String類,這顯然會造成安全隱患,採用雙親委託模式會使得系統的String類在Java虛擬機器啟動時就被載入,也就無法自定義String類來替代系統的String類,除非我們修改類載入器搜尋類的預設演算法。還有一點,只有兩個類名一致並且被同一個類載入器載入的類,Java虛擬機器才會認為它們是同一個類。

那麼下面的重點是看Android的類載入器如何重寫findClass()方法來載入類的。

Android ClassLoader繼承關係

從上圖可以看到與ClassLoader相關的一共有7個,大致可以分為如下3類

  • BootClassLoaderClassLoader內部類,是Android中所有ClassLoader的parent,可見性為包內可見我們無法使用。
  • BaseDexClassLoaderPathClassLoaderDexClassLoaderInMemoryDexClassLoader的父類,類載入的主要邏輯都是在BaseDexClassLoader完成的。
  • SecureClassLoader繼承了抽象類ClassLoader,拓展了ClassLoader類加入了許可權方面的功能,加強了安全性,其子類URLClassLoader是用URL路徑從jar檔案中載入類和資源,我們用不上。

我們重點關注與我們熱更新相關的PathClassLoaderDexClassLoader

  • PathClassLoader只能載入已經安裝到Android系統中的apk檔案(/data/app目錄),是Android預設使用的類載入器。
  • DexClassLoader可以載入任意目錄下的dex/jar/apk/zip檔案,比PathClassLoader更靈活,是實現熱修復的重點。

這裡我們可以驗證下PathClassLoader是安卓預設使用的類載入器。在ActivityonCreate()方法中列印它的類載入器。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.i("zhuliyuan", this.getClass().getClassLoader().toString());
    }

原始碼檢視(基於API28)

/**
 * Provides a simple {@link ClassLoader} implementation that operates on a list
 * of files and directories in the local file system, but does not attempt to
 * load classes from the network. Android uses this class for its system class
 * loader and for its application class loader(s).
 */
public class PathClassLoader extends BaseDexClassLoader {
    
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent){
        super(dexPath, null, librarySearchPath, parent);
   }
}

/**
 * A class loader that loads classes from {@code .jar} and {@code .apk} files
 * containing a {@code classes.dex} entry. This can be used to execute code not
 * installed as part of an application.
*/
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
                          String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

從註釋上可以看出PathClassLoader是用於系統類和應用程式類的載入,DexClassLoader可以用來載入任意目錄的dex。具體實現還得看BaseDexClassLoader的構造方法。

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                              String librarySearchPath, ClassLoader parent) {
        this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
    }

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                              String librarySearchPath, ClassLoader parent, boolean isTrusted) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
        ...
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        ...
        Class c = pathList.findClass(name, suppressedExceptions);
        ...
        return c;
    }
    
    @Override
    protected URL findResource(String name) {
        return pathList.findResource(name);
    }
    
    @Override
    public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }
}

可以看出在構造方法中建立了一個DexPathList物件賦值給了pathList欄位,然後findxxx()方法都是從DexPathList中查詢。

BaseDexClassLoader的建構函式包含四個引數:

  • dexPath:包含類和資源的jar / apk檔案列表,由 File.pathSeparator分隔,在Android上預設為:。
  • optimizedDirectory:由於dex檔案被包含在APK或者Jar檔案中,因此在裝載目標類之前需要先從APK或Jar檔案中解壓出dex檔案,該引數就是制定解壓出的dex 檔案存放的路徑。這也是對apk中dex根據平臺進行ODEX優化的過程。自API26開始無效。
  • librarySearchPath:指目標類中所使用的C/C++庫存放的路徑,可以為null。
  • parent:父ClassLoader引用。

接下來我們檢視DexPathList的構造方法和findxxx()方法。

final class DexPathList {
	private Element[] dexElements;
    
    DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
        ...
        // 載入dexPath路徑下的dex和resource
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
        ...
    }
    
        private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;
      for (File file : files) {
          if (file.isDirectory()) {
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();

              DexFile dex = null;
              if (name.endsWith(DEX_SUFFIX)) {
                  // Raw dex file (not inside a zip/jar).
                  try {
                      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 {
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  } catch (IOException suppressed) {
                      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);
          }
      }
      return elements;
    }
}

構造方法中呼叫makeDexElements()方法獲取到了Element[]陣列賦值給了dexElements變數。

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

findClass()方法則是變數在構造方法初始化好的Element[]陣列中從前往後遍歷找到我們需要的類。

這裡我們總結下類載入的過程

  • PathClassLoaderDexClassLoader呼叫了父類BaseDexClassLoader的構造方法。
  • BaseDexClassLoader在構造方法中建立了DexPathList物件並賦值給pathList欄位,載入類的findxxx()方法都是呼叫DexPathList類的findxxx()方法來實現內的載入。
  • DexPathList在構造方法中呼叫makeDexElements()方法建立了Element[]陣列賦值給dexElements欄位,findClass()方法就是從前往後遍歷Element[]陣列找到我們要的class.

熱更新實現原理

上面已經分析了類的載入流程,那麼要想實現熱更新我們需要拿到PathClassLoader中的DexPathList物件然後在拿到他當中的Element[]陣列將我們已經修復的Element插入到陣列的前面,這樣在類載入時候就會先拿到我們已經修復的class了達到動態替換的效果。

載入我們自己的dex檔案需要用到DexClassLoader,然後通過反射取出DexClassLoader中的DexPathList物件中的Element[]陣列插入到PathClassLoaderElement[]陣列前面即可。

熱更新實際例子我會在另一篇Android 手動實現熱更新介紹。