其他有關(guān)插件化的文章歡迎大家觀閱
插件化踩坑之路——Small和Atlas方案對比
Android插件化基礎(chǔ)篇—— class 文件
Android插件化基礎(chǔ)篇 — dex 文件
Android 插件化基礎(chǔ)——虛擬機(jī)
Android 和 Java 平臺(tái)的類加載平臺(tái)區(qū)別較大患整,是我們基礎(chǔ)篇的重點(diǎn)舵稠,我們將從三個(gè)方面來講解 ClassLoader。
Java 中的 ClassLoader 回顧
之前的文章中,我們已經(jīng)看過這張圖了贩疙,那篇文章中也簡單的講解了類的加載流程惭笑,加載流程兩個(gè)平臺(tái)差不多坯约,如何大家還不太熟悉可以去上面給出的虛擬機(jī)文章中再復(fù)習(xí)一下。
Android 中的 ClassLoader 詳解
Android 中的 ClassLoader 種類
Android 中的 ClassLoader 有以下幾種類型:
- BootClassLoader
- PathClassLoader
- DexClassLoader
- BaseDexClassLoader
BootClassLoader 作用和 Java 中的 Bootstrap ClassLoader 作用是類似的漓糙,是用來加載 Framework 層的字節(jié)碼文件的。
PathClassLoader 作用和 Java 中的 App ClassLoader 作用有點(diǎn)類似烘嘱,用來加載已經(jīng)安裝到系統(tǒng)中的 APK 文件中的 Class 文件昆禽。
DexClassLoader 和 Java 中的 Custom ClassLoader 作用類似,用來加載指定目錄中的字節(jié)碼文件蝇庭。
BaseDexClassLoader 是一個(gè)父類醉鳖,DexClassLoader 和 PathClassLoader 都是它的子類。
一個(gè) App 至少需要 BootClassLoader 和 PathClassLoader 才能運(yùn)行遗契。為了證明這一點(diǎn)辐棒,我們寫一個(gè)簡單的頁面,在 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());
}
}
最后我們發(fā)現(xiàn)輸出dalvik.system.PathClassLoader
和java.lang.BootClassLoader
牍蜂。當(dāng)然不同機(jī)子可能輸出的結(jié)果不同漾根,但至少會(huì)有這兩個(gè) ClassLoader。BootClassLoader 負(fù)責(zé)加載 framework 字節(jié)碼文件鲫竞,所以每個(gè)應(yīng)用都是需要的辐怕,而 PathClassLoader 用來加載已安裝 Apk 的字節(jié)碼文件,這些東西都是一個(gè)應(yīng)用啟動(dòng)的必要東西从绘。
Android 中 ClassLoader 特點(diǎn)及作用
Android 中的 ClassLoader 最大的特點(diǎn)就是雙親代理模型寄疏。雙親代理模型主要分三個(gè)過程:在加載字節(jié)碼的時(shí)候,會(huì)詢問當(dāng)前 ClassLoader 是否已經(jīng)加載過僵井,如果加載過則直接返回陕截,不再重復(fù)加載,如果沒有的話批什,會(huì)查詢 parent 是否加載過农曲,如果加載過,就直接返回 parent 加載的字節(jié)碼文件驻债。如果整個(gè)繼承線路上的 ClassLoader 都沒有加載乳规,執(zhí)行類才會(huì)由當(dāng)前 ClassLoader 類進(jìn)行真正加載。
這樣做的好處是合呐,如果一個(gè)類被位于樹中任意 ClassLoader 節(jié)點(diǎn)加載過暮的,那么以后整個(gè)系統(tǒng)生命周期中,這個(gè)類都將不會(huì)被加載淌实,大大提高了加載類的效率冻辩。由于這樣的特點(diǎn)猖腕,就給我們 ClassLoader 帶來了兩個(gè)作用。
第一個(gè)作用就是類加載的共享功能微猖。當(dāng)一個(gè) framework 層中的類被頂層 ClassLoader 加載過谈息,那么這個(gè)類就會(huì)被緩存在內(nèi)存里,以后任何需要用到底地方都不會(huì)重新加載了凛剥。
第二個(gè)作用就是類加載的隔離功能侠仇。不同繼承路線上的 ClassLoader 加載的類肯定不是同一個(gè)類,這樣就有一定的安全性犁珠,避免了用戶自己寫一些代碼冒充核心類庫來訪問這些類庫中核心代碼和變量逻炊。
所以如何判斷兩個(gè)類是同一個(gè)類呢,不僅需要工程中的包名類名一致犁享,還需要由同一個(gè) ClassLoader 加載的余素,這三條同時(shí)滿足才能說是一個(gè)類。
Android ClassLoader 源碼講解
我們下面就來通過源碼來看看 Android ClassLoader 到底是如何實(shí)現(xiàn)雙親代理模式的炊昆。
首先我們進(jìn)入 ClassLoader.java 這個(gè)類桨吊,查找它最核心的方法 loadClass()
看看它是怎么實(shí)現(xiàn)的
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 1.查看 class 是否已經(jīng)被加載過
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.如果類沒有被加載過凤巨,那么就通過當(dāng)前 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;
}
我在代碼中注釋已經(jīng)比較清楚了视乐,源碼中首先會(huì)判斷當(dāng)前的 ClassLoader 有沒有加載過這個(gè)類,如果沒有加載過敢茁,再會(huì)看看 parent ClassLoader 有沒有加載過佑淀,如果整個(gè)繼承線路走過后 class 依然為 null,則再回到當(dāng)前 ClassLoader 通過 findClass()
方法來加載 class彰檬。
好伸刃,現(xiàn)在讓我們繼續(xù)跟蹤 findClass()
方法,進(jìn)去后發(fā)現(xiàn)這個(gè)方法是個(gè)空實(shí)現(xiàn)逢倍,說明真正的實(shí)現(xiàn)代碼都在 ClassLoader 的子類中實(shí)現(xiàn)捧颅,我們在 Android Studio 中,查找類似 PathClassLoader 這樣的類是無法看到代碼的较雕,所以我們可以通過源碼網(wǎng)站 AndroidXRef 或者其他觀看源碼的方式來查看下 Android 幾個(gè) ClassLoader 的具體實(shí)現(xiàn)隘道。
打開 DexClassLoader
發(fā)現(xiàn)很簡單,類中只有一個(gè)構(gòu)造方法郎笆,繼承自 BaseDexClassLoader
,下面我們來看看這個(gè)構(gòu)造方法忘晤。
public DexClassLoader(String dexPath, String optimizedDirectory,String libraryPath,ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
參數(shù)dexpath
指定我們要加載的 dex 文件路徑宛蚓,optimizedDirectory
指定該 dex 文件要被拷貝到哪個(gè)路徑中,一般是應(yīng)用程序內(nèi)部路徑设塔。
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 文件,可以用來加載一些并沒有安裝到系統(tǒng)應(yīng)用中的類。所以痕钢,DexClassLoader 是動(dòng)態(tài)加載的核心图柏。
下面我們再來看看 PathClassLoader 是如何實(shí)現(xiàn)的,它同樣也是繼承于 BaseDexClassLoader任连,并且也重寫了構(gòu)造方法蚤吹。
public PathClassLoader(String dexPath, String libraryPath,ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
我們可以看到,它和 DexClassLoader 的區(qū)別就在于少了一個(gè) optimizedDirectory 的參數(shù)随抠,所以 PathClassLoader 沒有辦法加載沒有安裝到系統(tǒng)中的應(yīng)用的類裁着。
我們發(fā)現(xiàn),這兩個(gè) ClassLoader 并沒有什么具體實(shí)現(xiàn)拱她,真正的實(shí)現(xiàn)都是在他們的父類 BaseDexClassLoader
中二驰,所以我們下面看一下它的實(shí)現(xiàn)。
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;
}
}
我們通過構(gòu)造方法可以觀察到秉沼,如果 optimizedDirectory 為空桶雀,那么代表這是 PathClassLoader,不為空則是 DexClassLoader唬复,findClass()
方法雖然我們終于看到了實(shí)現(xiàn)矗积,但發(fā)現(xiàn)真正的實(shí)現(xiàn)還沒有在這里,而是在 DexPathList
對象的findClass()
方法中盅抚,不要?dú)怵H漠魏,結(jié)果就在前方,我們繼續(xù)跟進(jìn)妄均!
DexPathList
這個(gè)類代碼比較多柱锹,我們來從它的成員變量中開始,挑重點(diǎn)看丰包。
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;
}
}
}
}
}
我們關(guān)注幾個(gè)點(diǎn)禁熏,一個(gè)是 DEX_SUFFIX
這個(gè)成員變量,代表 dex 文件后綴邑彪,方便后面的一些文件處理判斷使用瞧毙。 definingContext
就是在初始化的時(shí)候傳進(jìn)來的 ClassLoader,dexElements
DexPathList 中一個(gè)靜態(tài)內(nèi)部類對象數(shù)組寄症,在構(gòu)造方法中初始化宙彪,這個(gè)對象數(shù)組是 findClass()
的關(guān)鍵參數(shù),通過遍歷獲取 Elements 中的 DexFile 對象有巧,調(diào)用 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 對象數(shù)組存儲(chǔ)的具體靜態(tài)內(nèi)部類篮迎,該類我只是簡單列舉下它的成員變量男图。dexElements 在 DexPathList 的構(gòu)造方法中初始化示姿,我們來細(xì)致的看下 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);
}
}
}
}
}
這里我省略掉了一些代碼栈戳,只看重點(diǎn)。其中注釋中第一個(gè)和第二個(gè) if 語句中的代碼的作用是如果路徑是文件夾的話难裆,就繼續(xù)向下遞歸子檀,第三個(gè)判斷是否是文件,如果是差牛,進(jìn)入第四個(gè)命锄,判斷文件是否是以 .dex
為后綴的,如果是的話標(biāo)明這個(gè)文件就是我們需要加載的 dex 文件偏化,通過 loadDexFile()
方法來加載 DexFile 對象脐恩。如果是文件,并且是個(gè)壓縮文件的話侦讨,就會(huì)進(jìn)入第五個(gè) if 語句中驶冒,同樣會(huì)通過 loadDexFile()
來進(jìn)行 DexFile 加載。下面來看一下 loadDexFile()
方法實(shí)現(xiàn)韵卤。
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 文件,那么直接創(chuàng)建 DexFile 對象即可沈条,如果不為空需忿,則調(diào)用 loadDex() 方法,將它解壓然后獲取內(nèi)部真正的 DexFile蜡歹。所以 makeElements() 就是通過文件獲取 dex 文件屋厘,轉(zhuǎn)化為 Elements 對象數(shù)組,然后給findClass()
方法使用月而。
loadClassBinaryName()
方法再往下走就是 native 方法了汗洒,我們就無法繼續(xù)看了,大概可以想像這個(gè) native 方法就是通過 C父款、C++去查找 dex 指定 name 相關(guān)的東西溢谤,然后將它拼成 class 字節(jié)碼,最后返回給我們憨攒。
整體的源碼我們大概就看過了世杀,實(shí)際上不是很復(fù)雜,只是嵌套很多肝集,真正復(fù)雜的地方都在 native 中了玫坛,所以我們看源碼一定要耐心細(xì)心,不能懼怕包晰,看不懂就多看幾遍湿镀,學(xué)習(xí)一下他們的編程思路和設(shè)計(jì)思想,對我們能力提高有極大幫助伐憾。
Android 中的動(dòng)態(tài)加載比 Java 程序復(fù)雜在哪里
Android 中的動(dòng)態(tài)加載在我們之前源碼分析之后勉痴,感覺看起來不是很復(fù)雜,只要利用好幾個(gè) ClassLoader 树肃,整體的思路還是比較清晰的蒸矛,但在實(shí)際設(shè)計(jì)的時(shí)候遠(yuǎn)遠(yuǎn)沒有那么簡單,主要是因?yàn)?Android 有他的復(fù)雜性:
- 有許多組件類胸嘴,比如四大組件雏掠,都是需要注冊才能使用的。需要在 AndoridManifest 注冊才能使用劣像。
- 資源的動(dòng)態(tài)加載非常復(fù)雜乡话。Android 的資源很特殊,都是通過 id 注冊的耳奕,通過 id 從 Resource 實(shí)例中獲取對應(yīng)的資源绑青,如果是動(dòng)態(tài)加載的新類,資源 id 就會(huì)找不到屋群,總而言之就是資源也是需要?jiǎng)討B(tài)注冊的闸婴。
- Android 每個(gè)版本對于類和資源加載的方式都是不同的,適配也是一個(gè)極為頭疼的問題芍躏。
以上難點(diǎn)總結(jié)起來可以用一句話概括:「Android 程序運(yùn)行需要一個(gè)上下文環(huán)境」邪乍。上下文環(huán)境可以給組件提供需要的功能,比如主題对竣、資源庇楞、查詢組件等。那么我們?nèi)绾谓o動(dòng)態(tài)加載的組件和類提供上下文環(huán)境呢柏肪,其實(shí)這就是第三方動(dòng)態(tài)加載庫主要解決的問題姐刁,也是非常復(fù)雜的,像 Tinker 和 Atlas 這些比較成熟的動(dòng)態(tài)加載方案都是以解決這些問題作為核心而設(shè)計(jì)的烦味,我們個(gè)人要解決可能比較困難聂使,但我們可以通過使用和閱讀源碼,來學(xué)習(xí)他們的實(shí)現(xiàn)原理谬俄,大致了解即可柏靶。
下一篇文章我們將利用我們學(xué)到的 ClassLoader 相關(guān)知識(shí),自己嘗試寫一個(gè)簡單的插件加載 demo 和插件管理器溃论。
本文部分內(nèi)容參考于慕課網(wǎng)實(shí)戰(zhàn)課程「Android 應(yīng)用發(fā)展趨勢必備武器 熱修復(fù)與插件化」屎蜓,有興趣的朋友可以付費(fèi)學(xué)習(xí)。
插件化實(shí)戰(zhàn)課程