一篇文章搞懂熱修復類加載方案原理

ClassLoader 類型

Java 中的 ClassLoader 能夠加載 jar 文件和 Class文件(本質是加載 Class 文件),這一點在 Android 中並不適用,由於不管 DVM 仍是 ART 它們加載的再也不是 Class 文件,而是 dex 文件。java

Android 中的 ClassLoader 類型和 Java 中的 ClassLoader 類型相似,也分爲兩種類型,分別是系統 ClassLoader自定義 ClassLoader。其中 Android 系統 ClassLoader 包括三種分別是 BootClassLoaderPathClassLoaderDexClassLoader,而 Java 系統類加載器也包括3種,分別是 Bootstrap ClassLoaderExtensions ClassLoaderApp ClassLoadergit

BootClassLoader

Android 系統啓動時會使用 BootClassLoader 來預加載經常使用類,與 Java 中的 BootClassLoader 不一樣,它並非由 C/C++ 代碼實現,而是由 Java 實現的,BootClassLoade 的代碼以下所示github

// libcore/ojluni/src/main/java/java/lang/ClassLoader.java
class BootClassLoader extends ClassLoader {

    private static BootClassLoader instance;

    @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
    public static synchronized BootClassLoader getInstance() {
        if (instance == null) {
            instance = new BootClassLoader();
        }

        return instance;
    }

    public BootClassLoader() {
        super(null);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return Class.classForName(name, false, null);
    }

    ...
}
複製代碼

BootClassLoaderClassLoader 的內部類,並繼承自 ClassLoaderBootClassLoader 是一個單例類,須要注意的是 BootClassLoader 的訪問修飾符是默認的,只有在同一個包中才能夠訪問,所以咱們在應用程序中是沒法直接調用的數組

PathClassLoader

Android 系統使用 PathClassLoader 來加載系統類和應用程序的類,若是是加載非系統應用程序類,則會加載 data/app/$packagename下的 dex 文件以及包含 dex 的 apk 文件或 jar 文件,不論是加載哪一種文件,最終都是要加載 dex 文件,在這裏爲了方便理解,咱們將 dex 文件以及包含 dex 的 apk 文件或 jar 文件統稱爲 dex 相關文件。PathClassLoader 不建議開發直接使用。安全

// libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java

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);
    }
}
複製代碼

PathClassLoader繼承自 BaseDexClassLoader,很明顯 PathClassLoader 的方法實現都在 BaseDexClassLoader 中。服務器

PathClassLoader 的構造方法有三個參數:app

  • dexPath:dex 文件以及包含 dex 的 apk 文件或 jar 文件的路徑集合,多個路徑用文件分隔符分隔,默認文件分隔符爲‘:’。
  • librarySearchPath:包含 C/C++ 庫的路徑集合,多個路徑用文件分隔符分隔分割,能夠爲 null
  • parent:ClassLoader 的 parent

DexClassLoader

DexClassLoader 能夠加載 dex 文件以及包含 dex 的 apk 文件或 jar 文件,也支持從 SD 卡進行加載,這也就意味着 DexClassLoader 能夠在應用未安裝的狀況下加載 dex 相關文件。所以,它是熱修復和插件化技術的基礎。ide

public class DexClassLoader extends BaseDexClassLoader {
    
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}
複製代碼

DexClassLoader 構造方法的參數要比 PathClassLoader 多一個 optimizedDirectory 參數,參數 optimizedDirectory 表明什麼呢?應用程序在第一次被加載的時候,爲了提升之後的啓動速度和執行效率,Android 系統會對 dex 相關文件作必定程度的優化,並生成一個 ODEX 文件,此後再運行這個應用程序的時候,只要加載優化過的 ODEX 文件就好了,省去了每次都要優化的時間,而參數 optimizedDirectory 就是表明存儲 ODEX 文件的路徑,這個路徑必須是一個內部存儲路徑。PathClassLoader 沒有參數 optimizedDirectory,這是由於 PathClassLoader 已經默認了參數 optimizedDirectory 的路徑爲:/data/dalvik-cacheDexClassLoader 也繼承自 BaseDexClassLoader ,方法實現也都在 BaseDexClassLoader 中。函數

關於以上 ClassLoader 在 Android 系統中的建立過程,這裏牽扯到 Zygote 進程,非本文的重點,故不在此進行討論。優化

ClassLoader 繼承關係

  • ClassLoader 是一個抽象類,其中定義了 ClassLoader 的主要功能。BootClassLoader 是它的內部類。
  • SecureClassLoader類和 JDK8 中的 SecureClassLoader 類的代碼是同樣的,它繼承了抽象類 ClassLoaderSecureClassLoader 並非 ClassLoader 的實現類,而是拓展了 ClassLoader 類加入了權限方面的功能,增強了 ClassLoader 的安全性。
  • URLClassLoader 類和 JDK8 中的 URLClassLoader 類的代碼是同樣的,它繼承自 SecureClassLoader,用來經過URl路徑從 jar 文件和文件夾中加載類和資源。
  • BaseDexClassLoader 繼承自 ClassLoader,是抽象類 ClassLoader 的具體實現類,PathClassLoaderDexClassLoader 都繼承它。

下面看看運行一個 Android 程序須要用到幾種類型的類加載器

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var classLoader = this.classLoader

        // 打印 ClassLoader 繼承關係
        while (classLoader != null) {
            Log.d("MainActivity", classLoader.toString())
            classLoader = classLoader.parent
        }
    }
}
複製代碼

MainActivity 的類加載器打印出來,而且打印當前類加載器的父加載器,直到沒有父加載器,則終止循環。打印結果以下:

com.zhgqthomas.github.hotfixdemo D/MainActivity: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.zhgqthomas.github.hotfixdemo-2/base.apk"],nativeLibraryDirectories=[/data/app/com.zhgqthomas.github.hotfixdemo-2/lib/arm64, /oem/lib64, /system/lib64, /vendor/lib64]]]

com.zhgqthomas.github.hotfixdemo D/MainActivity: java.lang.BootClassLoader@4d7e926
複製代碼

能夠看到有兩種類加載器,一種是 PathClassLoader,另外一種則是 BootClassLoaderDexPathList 中包含了不少路徑,其中 /data/app/com.zhgqthomas.github.hotfixdemo-2/base.apk 就是示例應用安裝在手機上的位置。

雙親委託模式

類加載器查找 Class 所採用的是雙親委託模式,所謂雙親委託模式就是首先判斷該 Class 是否已經加載,若是沒有則不是自身去查找而是委託給父加載器進行查找,這樣依次的進行遞歸,直到委託到最頂層的BootstrapClassLoader,若是 BootstrapClassLoader 找到了該 Class,就會直接返回,若是沒找到,則繼續依次向下查找,若是還沒找到則最後會交由自身去查找。 這是 JDK 中 ClassLoader 的實現邏輯,Android 中的 ClassLoaderfindBootstrapClassOrNull 方法的邏輯處理上存在差別。

// ClassLoader.java

    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) {
                long t0 = System.nanoTime();
                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.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                }
            }
            return c;
    }
複製代碼

上面的代碼很容易理解,首先會查找加載類是否已經被加載了,若是是直接返回,不然委託給父加載器進行查找,直到沒有父加載器則會調用 findBootstrapClassOrNull 方法。

下面看一下 findBootstrapClassOrNullJDKAndroid 中分別是如何實現的

// JDK ClassLoader.java

    private Class<?> findBootstrapClassOrNull(String name)
    {
        if (!checkName(name)) return null;

        return findBootstrapClass(name);
    }

複製代碼

JDKfindBootstrapClassOrNull 會最終交由 BootstrapClassLoader 去查找 Class 文件,上面提到過 BootstrapClassLoader 是由 C++ 實現的,因此 findBootstrapClass 是一個 native 的方法

// JDK ClassLoader.java

    private native Class<?> findBootstrapClass(String name);
複製代碼

在 Android 中 findBootstrapClassOrNull 的實現跟 JDK 是有差異的

// Android 
    private Class<?> findBootstrapClassOrNull(String name)
    {
        return null;
    }
複製代碼

Android 中由於不須要使用到 BootstrapClassLoader 因此該方法直接返回來 null

正是利用類加載器查找 Class 採用的雙親委託模式,因此能夠利用反射修改類加載器加載 dex 相關文件的順序,從而達到熱修復的目的

類加載過程

經過上面分析可知

  • PathClassLoader 能夠加載 Android 系統中的 dex 文件
  • DexClassLoader 能夠加載任意目錄的 dex/zip/apk/jar 文件,可是要指定optimizedDirectory

經過代碼可知這兩個類只是繼承了 BaseDexClassLoader,具體的實現依舊是由 BaseDexClassLoader 來完成。

BaseDexClassLoader

// libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java

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

    /** * @hide */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent, boolean isTrusted) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);

        if (reporter != null) {
            reportClassLoaderChain();
        }
    }
    
    ...
    
    public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        super(parent);
        this.pathList = new DexPathList(this, dexFiles);
    }
    
    ...
}
複製代碼

經過 BaseDexClassLoader 構造方法能夠知道,最重要的是去初始化 pathList 也就是 DexPathList 這個類,該類主要是用於管理 dex 相關文件

// libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions); // 查找邏輯交給 DexPathList
        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 中最重要的是這個 findClass 方法,這個方法用來加載 dex 文件中對應的 class 文件。而最終是交由 DexPathList 類來處理實現 findClass

DexPathList

// libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

final class DexPathList {
    ...

    /** class definition context */
    private final ClassLoader definingContext;

    /** * List of dex/resource (class path) elements. * Should be called pathElements, but the Facebook app uses reflection * to modify 'dexElements' (http://b/7726934). */
    private Element[] dexElements;
    
    ...
    
    
    DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
       ...

        this.definingContext = definingContext;

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // save dexPath for BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
        ...
    }

}
複製代碼

查看 DexPathList 核心構造函數的代碼可知,DexPathList 類經過 Element 來存儲 dex 路徑 ,而且經過 makeDexElements 函數來加載 dex 相關文件,並返回 Element 集合

// libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

    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;
      /* * 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();

              DexFile dex = null;
              if (name.endsWith(DEX_SUFFIX)) { // 判斷是不是 dex 文件
                  // 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 { // 若是是 apk, jar, zip 等文件
                  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);
                  }

                    // 將 dex 文件或壓縮文件包裝成 Element 對象,並添加到 Element 集合中
                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      elements[elementsPos++] = new Element(dex, file);
                  }
              }
              if (dex != null && isTrusted) {
                dex.setTrusted();
              }
          } else {
              System.logW("ClassLoader referenced unknown path: " + file);
          }
      }
      if (elementsPos != elements.length) {
          elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }
複製代碼

整體來講,DexPathList 的構造函數是將 dex 相關文件(多是 dex、apk、jar、zip , 這些類型在一開始時就定義好了)封裝成一個 Element 對象,最後添加到 Element 集合中

其實,Android 的類加載器不論是 PathClassLoader,仍是 DexClassLoader,它們最後只認 dex 文件,而 loadDexFile是加載 dex 文件的核心方法,能夠從 jar、apk、zip 中提取出 dex

// libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

    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;
    }
複製代碼

DexPathList 的構造函數中已經初始化了 dexElements,因此這個方法就很好理解了,只是對 Element 數組進行遍歷,一旦找到類名與 name 相同的類時,就直接返回這個 class,找不到則返回 null

熱修復實現

經過上面的分析能夠知道運行一個 Android 程序是使用到 PathClassLoader,即 BaseDexClassLoader,而 apk 中的 dex 相關文件都會存儲在 BaseDexClassLoaderpathList 對象的 dexElements 屬性中。

那麼熱修復的原理就是將改好 bug 的 dex 相關文件放進 dexElements 集合的頭部,這樣遍歷時會首先遍歷修復好的 dex 並找到修復好的類,由於類加載器的雙親委託模式,舊 dex 中的存有 bug 的 class 是沒有機會上場的。這樣就能實如今沒有發佈新版本的狀況下,修復現有的 bug class

手動實現熱修復功能

根據上面熱修復的原理,對應的思路可概括以下

  1. 建立 BaseDexClassLoader 的子類 DexClassLoader 加載器
  2. 加載修復好的 class.dex (服務器下載的修復包)
  3. 將自有的和系統的 dexElements 進行合併,並設置自由的 dexElements 優先級
  4. 經過反射技術,賦值給系統的 pathList

熱修復 Demo 推薦

能夠參考 Github 上的這個項目

參考

相關文章
相關標籤/搜索