一篇文章搞懂熱修復原理
Java 中的 ClassLoader
可以載入 jar 檔案和 Class檔案(本質是載入 Class 檔案),這一點在 Android 中並不適用,因為無論 DVM 還是 ART 它們載入的不再是 Class 檔案,而是 dex 檔案。
Android 中的 ClassLoader
型別和 Java 中的 ClassLoader
型別類似,也分為兩種型別,分別是 系統 ClassLoader
和 自定義 ClassLoader
。其中 Android 系統 ClassLoader
包括三種分別是 BootClassLoader
、 PathClassLoader
和 DexClassLoader
,而 Java 系統類載入器也包括3種,分別是 Bootstrap ClassLoader
、 Extensions ClassLoader
和 App ClassLoader
。
BootClassLoader
Android 系統啟動時會使用 BootClassLoader
來預載入常用類,與 Java 中的 BootClassLoader
不同,它並是由 C/C++ 程式碼實現,而是由 Java 實現的,1BootClassLoade1 的程式碼如下所示
// 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); } ... } 複製程式碼
BootClassLoader
是 ClassLoader
的內部類,並繼承自 ClassLoader
。 BootClassLoader
是一個單例類, 需要注意的是 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
的構造方法有三個引數:
- dexPath:dex 檔案以及包含 dex 的 apk 檔案或 jar 檔案的路徑集合,多個路徑用檔案分隔符分隔,預設檔案分隔符為‘:’。
- librarySearchPath:包含 C/C++ 庫的路徑集合,多個路徑用檔案分隔符分隔分割,可以為 null
- parent:ClassLoader 的 parent
DexClassLoader
DexClassLoader
可以載入 dex 檔案以及包含 dex 的 apk 檔案或 jar 檔案,也支援從 SD 卡進行載入,這也就意味著 DexClassLoader
可以在應用未安裝的情況下載入 dex 相關檔案。 因此,它是熱修復和外掛化技術的基礎。
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-cache
。 DexClassLoader
也繼承自 BaseDexClassLoader
,方法實現也都在 BaseDexClassLoader
中。
關於以上 ClassLoader
在 Android 系統中的建立過程,這裡牽扯到 Zygote
程序,非本文的重點,故不在此進行討論。
ClassLoader 繼承關係

-
ClassLoader
是一個抽象類,其中定義了ClassLoader
的主要功能。BootClassLoader
是它的內部類。 -
SecureClassLoader
類和JDK8
中的SecureClassLoader
類的程式碼是一樣的,它繼承了抽象類ClassLoader
。SecureClassLoader
並不是ClassLoader
的實現類,而是拓展了ClassLoader
類加入了許可權方面的功能,加強了ClassLoader
的安全性。 -
URLClassLoader
類和JDK8
中的URLClassLoader
類的程式碼是一樣的,它繼承自SecureClassLoader
,用來通過URl路徑從 jar 檔案和資料夾中載入類和資源。 -
BaseDexClassLoader
繼承自ClassLoader
,是抽象類ClassLoader
的具體實現類,PathClassLoader
和DexClassLoader
都繼承它。
下面看看執行一個 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
,另一種則是 BootClassLoader
。 DexPathList
中包含了很多路徑,其中 /data/app/com.zhgqthomas.github.hotfixdemo-2/base.apk
就是示例應用安裝在手機上的位置。
雙親委託模式
類載入器查詢 Class 所採用的是雙親委託模式,**所謂雙親委託模式就是首先判斷該 Class 是否已經載入,如果沒有則不是自身去查詢而是委託給父載入器進行查詢,這樣依次的進行遞迴,直到委託到最頂層的 BootstrapClassLoader
,如果 BootstrapClassLoader
找到了該 Class,就會直接返回,如果沒找到,則繼續依次向下查詢,如果還沒找到則最後會交由自身去查詢。這是 JDK 中 ClassLoader
的實現邏輯,Android 中的 ClassLoader
在 findBootstrapClassOrNull
方法的邏輯處理上存在差異。
// 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
方法。
下面看一下 findBootstrapClassOrNull
在 JDK
和 Android
中分別是如果實現的
// JDK ClassLoader.java private Class<?> findBootstrapClassOrNull(String name) { if (!checkName(name)) return null; return findBootstrapClass(name); } 複製程式碼
JDK
中 findBootstrapClassOrNull
會最終交由 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 相關檔案都會儲存在 BaseDexClassLoader
的 pathList
物件的 dexElements
屬性中。
那麼熱修復的原理就是將改好 bug 的 dex 相關檔案放進 dexElements
集合的頭部,這樣遍歷時會首先遍歷修復好的 dex 並找到修復好的類,因為類載入器的雙親委託模式,舊 dex 中的存有 bug 的 class 是沒有機會上場的。這樣就能實現在沒有釋出新版本的情況下,修復現有的 bug class
手動實現熱修復功能
根據上面熱修復的原理,對應的思路可歸納如下
- 建立
BaseDexClassLoader
的子類DexClassLoader
載入器 - 載入修復好的 class.dex (伺服器下載的修復包)
- 將自有的和系統的
dexElements
進行合併,並設定自由的dexElements
優先順序 - 通過反射技術,賦值給系統的
pathList