ClassLoader 型別
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
熱修復 Demo 推薦
可以參考 Github 上的這個專案