Java 中的 ClassLoader
能夠加載 jar 文件和 Class文件(本質是加載 Class 文件),這一點在 Android 中並不適用,由於不管 DVM 仍是 ART 它們加載的再也不是 Class 文件,而是 dex 文件。java
Android 中的 ClassLoader
類型和 Java 中的 ClassLoader
類型相似,也分爲兩種類型,分別是系統 ClassLoader
和自定義 ClassLoader
。其中 Android 系統 ClassLoader
包括三種分別是 BootClassLoader
、PathClassLoader
和 DexClassLoader
,而 Java 系統類加載器也包括3種,分別是 Bootstrap ClassLoader
、 Extensions ClassLoader
和 App ClassLoader
。git
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);
}
...
}
複製代碼
BootClassLoader
是 ClassLoader
的內部類,並繼承自 ClassLoader
。 BootClassLoader
是一個單例類,須要注意的是 BootClassLoader
的訪問修飾符是默認的,只有在同一個包中才能夠訪問,所以咱們在應用程序中是沒法直接調用的。數組
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
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-cache
。DexClassLoader
也繼承自 BaseDexClassLoader
,方法實現也都在 BaseDexClassLoader
中。函數
關於以上 ClassLoader
在 Android 系統中的建立過程,這裏牽扯到 Zygote
進程,非本文的重點,故不在此進行討論。優化
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
來完成。
// 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
// 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
加載器dexElements
進行合併,並設置自由的 dexElements
優先級pathList
能夠參考 Github 上的這個項目