其他有關插件化的文章歡迎大家觀閱
插件化踩坑之路——Small和Atlas方案對比
Android插件化基礎篇—— class 文件
Android插件化基礎篇 — dex 文件
Android 插件化基礎——虛擬機
Android 和 Java 平臺的類加載平臺區別較大,是我們基礎篇的重點,我們將從三個方面來講解 ClassLoader。
Java 中的 ClassLoader 回顧
之前的文章中,我們已經看過這張圖了,那篇文章中也簡單的講解了類的加載流程,加載流程兩個平臺差不多,如何大家還不太熟悉可以去上面給出的虛擬機文章中再復習一下。
Android 中的 ClassLoader 詳解
Android 中的 ClassLoader 種類
Android 中的 ClassLoader 有以下幾種類型:
- BootClassLoader
- PathClassLoader
- DexClassLoader
- BaseDexClassLoader
BootClassLoader 作用和 Java 中的 Bootstrap ClassLoader 作用是類似的,是用來加載 Framework 層的字節碼文件的。
PathClassLoader 作用和 Java 中的 App ClassLoader 作用有點類似,用來加載已經安裝到系統中的 APK 文件中的 Class 文件。
DexClassLoader 和 Java 中的 Custom ClassLoader 作用類似,用來加載指定目錄中的字節碼文件。
BaseDexClassLoader 是一個父類,DexClassLoader 和 PathClassLoader 都是它的子類。
一個 App 至少需要 BootClassLoader 和 PathClassLoader 才能運行。為了證明這一點,我們寫一個簡單的頁面,在 MainActivity
的 onCreate()
方法中寫下如下代碼:
ClassLoader classLoader = getClassLoader();
if (classLoader != null) {
Log.e("weaponzhi", "classLoader: " + classLoader.toString());
while (classLoader.getParent() != null) {
classLoader = classLoader.getParent();
Log.e("weaponzhi","classLoader: "+classLoader.toString());
}
}
最后我們發現輸出dalvik.system.PathClassLoader
和java.lang.BootClassLoader
。當然不同機子可能輸出的結果不同,但至少會有這兩個 ClassLoader。BootClassLoader 負責加載 framework 字節碼文件,所以每個應用都是需要的,而 PathClassLoader 用來加載已安裝 Apk 的字節碼文件,這些東西都是一個應用啟動的必要東西。
Android 中 ClassLoader 特點及作用
Android 中的 ClassLoader 最大的特點就是雙親代理模型。雙親代理模型主要分三個過程:在加載字節碼的時候,會詢問當前 ClassLoader 是否已經加載過,如果加載過則直接返回,不再重復加載,如果沒有的話,會查詢 parent 是否加載過,如果加載過,就直接返回 parent 加載的字節碼文件。如果整個繼承線路上的 ClassLoader 都沒有加載,執行類才會由當前 ClassLoader 類進行真正加載。
這樣做的好處是,如果一個類被位于樹中任意 ClassLoader 節點加載過,那么以后整個系統生命周期中,這個類都將不會被加載,大大提高了加載類的效率。由于這樣的特點,就給我們 ClassLoader 帶來了兩個作用。
第一個作用就是類加載的共享功能。當一個 framework 層中的類被頂層 ClassLoader 加載過,那么這個類就會被緩存在內存里,以后任何需要用到底地方都不會重新加載了。
第二個作用就是類加載的隔離功能。不同繼承路線上的 ClassLoader 加載的類肯定不是同一個類,這樣就有一定的安全性,避免了用戶自己寫一些代碼冒充核心類庫來訪問這些類庫中核心代碼和變量。
所以如何判斷兩個類是同一個類呢,不僅需要工程中的包名類名一致,還需要由同一個 ClassLoader 加載的,這三條同時滿足才能說是一個類。
Android ClassLoader 源碼講解
我們下面就來通過源碼來看看 Android ClassLoader 到底是如何實現雙親代理模式的。
首先我們進入 ClassLoader.java 這個類,查找它最核心的方法 loadClass()
看看它是怎么實現的
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 1.查看 class 是否已經被加載過
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//2.如果沒有被加載過,則判斷 parent ClassLoader 有沒有加載過
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
}
//3.如果類沒有被加載過,那么就通過當前 ClassLoader 來加載
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;
}
我在代碼中注釋已經比較清楚了,源碼中首先會判斷當前的 ClassLoader 有沒有加載過這個類,如果沒有加載過,再會看看 parent ClassLoader 有沒有加載過,如果整個繼承線路走過后 class 依然為 null,則再回到當前 ClassLoader 通過 findClass()
方法來加載 class。
好,現在讓我們繼續跟蹤 findClass()
方法,進去后發現這個方法是個空實現,說明真正的實現代碼都在 ClassLoader 的子類中實現,我們在 Android Studio 中,查找類似 PathClassLoader 這樣的類是無法看到代碼的,所以我們可以通過源碼網站 AndroidXRef 或者其他觀看源碼的方式來查看下 Android 幾個 ClassLoader 的具體實現。
打開 DexClassLoader
發現很簡單,類中只有一個構造方法,繼承自 BaseDexClassLoader
,下面我們來看看這個構造方法。
public DexClassLoader(String dexPath, String optimizedDirectory,String libraryPath,ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
參數dexpath
指定我們要加載的 dex 文件路徑,optimizedDirectory
指定該 dex 文件要被拷貝到哪個路徑中,一般是應用程序內部路徑。
DexClassLoader
類上有一段官方注釋:
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.
這段注釋的意思就是,DexClassLoader 可以加載一些 jar 包和 apk 包里面的 dex 文件,可以用來加載一些并沒有安裝到系統應用中的類。所以,DexClassLoader 是動態加載的核心。
下面我們再來看看 PathClassLoader 是如何實現的,它同樣也是繼承于 BaseDexClassLoader,并且也重寫了構造方法。
public PathClassLoader(String dexPath, String libraryPath,ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
我們可以看到,它和 DexClassLoader 的區別就在于少了一個 optimizedDirectory 的參數,所以 PathClassLoader 沒有辦法加載沒有安裝到系統中的應用的類。
我們發現,這兩個 ClassLoader 并沒有什么具體實現,真正的實現都是在他們的父類 BaseDexClassLoader
中,所以我們下面看一下它的實現。
public class BaseDexClassLoader extends ClassLoader{
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath,File optimizedDirectory,
String libraryPath,ClassLoader parent){
super(parent);
this.pathList = new DexPathList(this,dexPath,libraryPath,optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throw ClassNotFoundException{
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name,suppressedExceptions);
if (c == null){
ClassNotFoundException cnfe = new ClassNorFoundException("xxx");
for (Throwable t : suppressedExceptions){
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
}
我們通過構造方法可以觀察到,如果 optimizedDirectory 為空,那么代表這是 PathClassLoader,不為空則是 DexClassLoader,findClass()
方法雖然我們終于看到了實現,但發現真正的實現還沒有在這里,而是在 DexPathList
對象的findClass()
方法中,不要氣餒,結果就在前方,我們繼續跟進!
DexPathList
這個類代碼比較多,我們來從它的成員變量中開始,挑重點看。
final class DexPathList{
private static final String DEX_SUFFIX = ".dex";
private final ClassLoader definingContext;
private final Element[] dexElements;
...
public DexPathList(ClassLoader definingContext,
String dexPath,String libraryPath,File optimizedDirectory){
...
this.dexElements = makeDexElements(splitDexPath(dexPath),optimizedDirectory,suppressedException);
...
}
public Class findClass(String name,List<Throwable> suppressed){
for (Element element : dexElements){
DexFile dex = element.dexFile;
if(dex != null){
Class clazz = dex.loadClassBinaryName(name,definingContext,suppressed);
if(clazz != null){
return clazz;
}
}
}
}
}
我們關注幾個點,一個是 DEX_SUFFIX
這個成員變量,代表 dex 文件后綴,方便后面的一些文件處理判斷使用。 definingContext
就是在初始化的時候傳進來的 ClassLoader,dexElements
DexPathList 中一個靜態內部類對象數組,在構造方法中初始化,這個對象數組是 findClass()
的關鍵參數,通過遍歷獲取 Elements 中的 DexFile 對象,調用 DexFile 的 loadClassBinaryName()
方法,完成 class 文件的獲取。
static class Element{
private final File file;
private final boolean isDirectory;
private final File zip;
private final DexFile dexFile;
public Element(File file,boolean isDirectory,File zip,DexFile dexFile){
this.dir = dir;
this.isDirectory = isDirectory;
this.zip = zip;
this.dexFIle = dexFIle;
}
}
Element 就是 dexElements 對象數組存儲的具體靜態內部類,該類我只是簡單列舉下它的成員變量。dexElements 在 DexPathList 的構造方法中初始化,我們來細致的看下 makeDexElements
方法,該方法直接指向 makeElements()
方法,源碼如下:
private static Element[] makeElements(List<File> files,File optimizedDirectory,
List<IOException> suppressedExceptions,
boolean ignoreDexFiles,
ClassLoader loader){
Element[] elements = new Element[file.size()];
int elementsPos = 0;
for (File file : files){
File zip = null;
File dir = new File("");
DexFile dex = null;
String path = file.getPath();
String name = file.getName();
//1
if (path.contains(zipSeparator)){
...
//2
}else if(file.isDirectory()){
elements[elementsPos++] == new Element(file,true,null,null);
//3
}else if (file.isFile()){
//4
if(!ignoreDexFiles && name.endsWith(DEX_SUFFIX)){
dex = loadDexFile(file,optimizedDirectory,loader,elements);
//5
}else{
zip = file;
//6
if(!ignoreDexFiles){
dex = loadDexFile(file,optimizedDirectory,loader,elements);
}
}
}
}
}
這里我省略掉了一些代碼,只看重點。其中注釋中第一個和第二個 if 語句中的代碼的作用是如果路徑是文件夾的話,就繼續向下遞歸,第三個判斷是否是文件,如果是,進入第四個,判斷文件是否是以 .dex
為后綴的,如果是的話標明這個文件就是我們需要加載的 dex 文件,通過 loadDexFile()
方法來加載 DexFile 對象。如果是文件,并且是個壓縮文件的話,就會進入第五個 if 語句中,同樣會通過 loadDexFile()
來進行 DexFile 加載。下面來看一下 loadDexFile()
方法實現。
private static DexFile loadDexFile(File file,File optimizedDirectory,Classloader loader,
Element[] elements) throw IOException{
if(optimizedDirectory == null){
return new DexFile(file,loader,elements);
}else{
String optimizedPath = optimizedPathFor(file,optimizedDirectory);
}
}
如果optimizedDirectory
為空,說明文件就是 dex 文件,那么直接創建 DexFile 對象即可,如果不為空,則調用 loadDex() 方法,將它解壓然后獲取內部真正的 DexFile。所以 makeElements() 就是通過文件獲取 dex 文件,轉化為 Elements 對象數組,然后給findClass()
方法使用。
loadClassBinaryName()
方法再往下走就是 native 方法了,我們就無法繼續看了,大概可以想像這個 native 方法就是通過 C、C++去查找 dex 指定 name 相關的東西,然后將它拼成 class 字節碼,最后返回給我們。
整體的源碼我們大概就看過了,實際上不是很復雜,只是嵌套很多,真正復雜的地方都在 native 中了,所以我們看源碼一定要耐心細心,不能懼怕,看不懂就多看幾遍,學習一下他們的編程思路和設計思想,對我們能力提高有極大幫助。
Android 中的動態加載比 Java 程序復雜在哪里
Android 中的動態加載在我們之前源碼分析之后,感覺看起來不是很復雜,只要利用好幾個 ClassLoader ,整體的思路還是比較清晰的,但在實際設計的時候遠遠沒有那么簡單,主要是因為 Android 有他的復雜性:
- 有許多組件類,比如四大組件,都是需要注冊才能使用的。需要在 AndoridManifest 注冊才能使用。
- 資源的動態加載非常復雜。Android 的資源很特殊,都是通過 id 注冊的,通過 id 從 Resource 實例中獲取對應的資源,如果是動態加載的新類,資源 id 就會找不到,總而言之就是資源也是需要動態注冊的。
- Android 每個版本對于類和資源加載的方式都是不同的,適配也是一個極為頭疼的問題。
以上難點總結起來可以用一句話概括:「Android 程序運行需要一個上下文環境」。上下文環境可以給組件提供需要的功能,比如主題、資源、查詢組件等。那么我們如何給動態加載的組件和類提供上下文環境呢,其實這就是第三方動態加載庫主要解決的問題,也是非常復雜的,像 Tinker 和 Atlas 這些比較成熟的動態加載方案都是以解決這些問題作為核心而設計的,我們個人要解決可能比較困難,但我們可以通過使用和閱讀源碼,來學習他們的實現原理,大致了解即可。
下一篇文章我們將利用我們學到的 ClassLoader 相關知識,自己嘗試寫一個簡單的插件加載 demo 和插件管理器。
本文部分內容參考于慕課網實戰課程「Android 應用發展趨勢必備武器 熱修復與插件化」,有興趣的朋友可以付費學習。
插件化實戰課程