插件化概述
??插件化技術(shù)最初源于免安裝運(yùn)行apk的想法几莽,這個(gè)免安裝的apk可以理解為插件禾酱。支持插件化的app可以在運(yùn)行時(shí)加載和運(yùn)行插件生巡,這樣便可以將app中一些不常用的功能模塊做成插件资厉,一方面減小了安裝包的大小泄朴,另一方面可以實(shí)現(xiàn)app功能的動態(tài)擴(kuò)展。插件化從最開始提出至今已經(jīng)發(fā)展的非常成熟了棒妨,也涌現(xiàn)出了非常多的開源框架踪古,從最開始的Dynamic-load-apk到后來比較有名的RePlugin、VirtualApk券腔,還有前段時(shí)間騰訊開源的Shadow伏穆,開發(fā)者們也可以根據(jù)自身的需求以及框架的特性,選擇最合適的框架纷纫。但是無論哪種框架枕扫,想要實(shí)現(xiàn)插件化,主要需要解決下面三個(gè)問題:
- 插件類的加載
- 四大組件生命周期的管理
- 插件資源的加載
??接下來會針對上面幾個(gè)問題進(jìn)行分析辱魁,我是基于滴滴開源的VirtualApk框架進(jìn)行分析烟瞧,其他框架的實(shí)現(xiàn)與VirtualApk會有不同,但是插件化的原理大體是相似的染簇,尤其是類加載以及資源加載参滴,所以如果熟悉了一款插件化框架以后再去閱讀其他的插件化框架都是比較容易的。
VirtualApk倉庫鏈接
插件類的加載
??要想理解插件類加載的原理锻弓,必須要先對Java以及Android的類加載機(jī)制有所了解卵洗。這里我不打算深入地講這個(gè)問題,畢竟我們的主題不是這個(gè)弥咪,對Android類加載機(jī)制還不了解的同學(xué)可以先看看相關(guān)的資料,我稍微提一下和插件化相關(guān)的一些知識十绑。
Android類加載基礎(chǔ)
??Android是通過ClassLoader來完成類加載的聚至,Android中的ClassLoader與Java中的有一定的區(qū)別,在Android中主要三種類型的ClassLoader本橙,分別是BootClassLoader扳躬、PathClassLoader以及DexClassLoader。其中BootClassLoader用于加載系統(tǒng)類甚亭,PathClassLoader和DexClassLoader都是用于加載應(yīng)用程序類的贷币,且它們都繼承自BaseDexClassLoader,它們的類加載實(shí)現(xiàn)都在BaseDexClassLoader的findClass()
中亏狰,這個(gè)方法我們后面會提到役纹。
??這里說到了PathClassLoader和DexClassLoader,有必要說一下它們之間的區(qū)別暇唾。網(wǎng)上很多資料都流傳著PathClassLoader只能加載已安裝的apk促脉,而DexClassLoader可以加載任意的apk辰斋,其實(shí)這種說法是錯(cuò)誤的。我們以Android7.0為例看一下DexClassLoader和PathClassLoader的源碼瘸味,如下所示:
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
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);
}
}
??可以看到DexClassLoader和PathClassLoader的源碼非常簡單宫仗,只有構(gòu)造方法,因?yàn)轭惣虞d相關(guān)的代碼都在其父類中旁仿。它們之間的區(qū)別就在于調(diào)用父類構(gòu)造方法的時(shí)候傳遞的第二個(gè)參數(shù)藕夫,DexClassLoader傳遞的是一個(gè)File類型的對象,PathClassLoader固定傳遞null枯冈。這個(gè)參數(shù)會一直傳遞到native層進(jìn)行處理毅贮,具體邏輯比較復(fù)雜,最終這個(gè)路徑是用來存放dex2oat的產(chǎn)物.odex文件的霜幼,如果傳遞的是null嫩码,就會存放在默認(rèn)目錄下(/data/dalvik-cache)。所以optimizedDirectory的傳遞并不影響dex的加載罪既,因此DexClassLoader和PathClassLoader都可以加載任意的apk铸题。另外值得一提的一點(diǎn)就是,optimizedDirectory在8.1以上的系統(tǒng)中被廢棄了琢感,我們可以看一下Android8.1中DexClassLoader的源碼丢间,如下所示:
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
??可以看到在Android8.1的源碼中DexClassLoader的optimizedDirectory同樣固定傳遞null,因此可以認(rèn)為在Android8.1及以上的系統(tǒng)中驹针,DexClassLoader和PathClassLoader是沒有區(qū)別的烘挫。
??Android中默認(rèn)是使用PathClassLoader來進(jìn)行類的加載,當(dāng)需要加載一個(gè)類的時(shí)候柬甥,都會通過一個(gè)默認(rèn)的PathClassLoader進(jìn)行加載饮六,這個(gè)ClassLoader我們可以通過Context.getClassLoader()
獲取到。和Java一樣的是苛蒲,Android中ClassLoader同樣是遵循雙親委派模型的卤橄,其中PathClassLoader的父類加載器是BootClassLoader,BootClassLoader沒有父類加載器臂外。所以如果我們要加載的類是系統(tǒng)類窟扑,最終會由BootClassLoader完成加載,如果要加載的是應(yīng)用程序類漏健,則會交由PathClassLoader完成加載嚎货。那么PathClassLoader是如何完成類加載的呢?
??前面已經(jīng)說到了PathClassLoader和DexClassLoader都繼承自BaseDexClassLoader且它們的類加載實(shí)現(xiàn)都在其父類的findClass()
中蔫浆,因此我們看看該方法殖属,如下所示:
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
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;
}
??可以看出其內(nèi)部是通過調(diào)用pathList的findClass()
完成類加載的,pathList是一個(gè)DexPathList類型的成員變量克懊,因此我們再看一下DexPathList的findClass()
的實(shí)現(xiàn)忱辅,如下所示:
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;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
??DexPathList的內(nèi)部有一個(gè)成員變量dexElements七蜘,它是一個(gè)Element類型的數(shù)組,Element的內(nèi)部有一個(gè)DexFile類型的成員墙懂,而DexFile就是用于加載一個(gè)dex文件的橡卤。調(diào)用DexPathList的findClass()
時(shí),會遍歷dexElements损搬,依次從每個(gè)Element中取出DexFile碧库,調(diào)用DexFile的loadClassBinaryName()
,該方法內(nèi)部會調(diào)用native方法從其對應(yīng)的dex文件中完成類的加載巧勤,更深入的過程我們就不需要關(guān)注了嵌灰。
??看到這里我們對Android的類加載機(jī)制有了一定的了解,正常情況下Element數(shù)組只會包含宿主的dex信息颅悉,而我們的插件存放的位置可以是任意的沽瞭,系統(tǒng)也并不知道插件的存在,所以正常情況下插件類是無法被加載的剩瓶,因此我們需要特殊的處理解決插件類加載的問題驹溃。
VirtualApk源碼實(shí)現(xiàn)
??接下來我們就通過VirtualApk源碼來看一下VirtualApk是如何實(shí)現(xiàn)插件類的加載的。當(dāng)我們需要加載一個(gè)插件的時(shí)候延曙,會調(diào)用PluginManager的loadPlugin()
豌鹤,該方法如下所示:
public void loadPlugin(File apk) throws Exception {
if (null == apk) {
throw new IllegalArgumentException("error : apk is null.");
}
if (!apk.exists()) {
InputStream in = new FileInputStream(apk);
in.close();
}
LoadedPlugin plugin = createLoadedPlugin(apk);
if (null == plugin) {
throw new RuntimeException("Can't load plugin which is invalid: " + apk.getAbsolutePath());
}
mPlugins.put(plugin.getPackageName(), plugin);
synchronized (mCallbacks) {
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).onAddedLoadedPlugin(plugin);
}
}
}
??該方法首先判斷了我們要加載的插件文件是否存在,如果存在則調(diào)用createLoadedPlugin()
創(chuàng)建一個(gè)封裝了插件信息的LoadedPlugin對象枝缔,并將這個(gè)對象根據(jù)插件包名添加到mPlugins保存起來布疙,createLoadedPlugin()
內(nèi)部就直接通過構(gòu)造方法創(chuàng)建了一個(gè)LoadedPlugin對象,因此我們看一下LoadedPlugin的構(gòu)造方法愿卸,方法如下:
public LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception {
...
mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK); // 1
...
mResources = createResources(context, getPackageName(), apk); // 2
mClassLoader = createClassLoader(context, apk, mNativeLibDir, context.getClassLoader()); // 3
...
invokeApplication(); // 4
}
??LoadedPlugin的構(gòu)造方法比較長灵临,我們只看一下我們關(guān)心的部分。注釋1處調(diào)用了PackageParserCompat.parsePackage()
趴荸,這個(gè)方法會調(diào)用PackageParser的parsePackage()
去解析我們插件APK俱诸,得到一個(gè)包含在插件AndroidManifest文件中聲明的Application以及四大組件信息的Package對象,這個(gè)Package對象在四大組件插件化的實(shí)現(xiàn)上起著非常重要的作用赊舶,在四大組件插件化的章節(jié)我們會再次提到它。其實(shí)我們在安裝一個(gè)apk時(shí)赶诊,系統(tǒng)的PackageManagerService也會調(diào)用PackageParser的parsePackage()
去解析我們的apk笼平,這個(gè)過程是一樣的。接著在注釋2出調(diào)用createResources()
創(chuàng)建用于加載插件資源的Resource對象舔痪,這個(gè)過程我們會在資源的插件化章節(jié)中進(jìn)行分析寓调。注釋3調(diào)用createClassLoader()
創(chuàng)建了一個(gè)ClassLoader對象,最后在注釋4處調(diào)用invokeApplication()
根據(jù)注釋1解析到的Application信息實(shí)例化一個(gè)表示插件的Application對象并調(diào)用它的onCreate()
方法锄码。那我們這里重點(diǎn)關(guān)注一下createClassLoader()
的實(shí)現(xiàn)夺英,方法如下所示:
protected ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) throws Exception {
File dexOutputDir = getDir(context, Constants.OPTIMIZE_DIR);
String dexOutputPath = dexOutputDir.getAbsolutePath();
DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent); // 1
if (Constants.COMBINE_CLASSLOADER) { // 2
DexUtil.insertDex(loader, parent, libsDir); // 3
}
return loader;
}
??可以看到方法在注釋1處創(chuàng)建了一個(gè)DexClassLoader晌涕,創(chuàng)建DexClassLoader時(shí)傳遞的第一個(gè)參數(shù)表示dex文件的路徑,這里傳遞了插件apk的絕對路徑痛悯,第四個(gè)參數(shù)表示其父類加載器余黎,這里傳遞的parent是前面通過Context.getClassLoader()
獲取到的,這個(gè)ClassLoader我們知道就是PathClassLoader载萌,這樣形成了DexClassLoader -> PathClassLoader -> BootClassLoader的類加載結(jié)構(gòu)惧财,當(dāng)用插件對應(yīng)的DexClassLoader進(jìn)行加載時(shí),根據(jù)雙親委派模型扭仁,會先交給宿主的PathClassLoader進(jìn)行加載并繼續(xù)向上傳遞垮衷。那么是不是宿主類會由PathClassLoader加載而插件類由DexClassLoader加載呢?答案是不一定乖坠,我們先繼續(xù)往下看搀突,注釋2處判斷Constants.COMBINE_CLASSLOADER
,若為真則執(zhí)行注釋3處代碼熊泵,Constants.COMBINE_CLASSLOADER
是個(gè)常量為true仰迁,因此默認(rèn)情況下都會執(zhí)行注釋3的代碼,除非修改源碼重新編譯戈次。而我們前面說到的問題正是由注釋3代碼是否執(zhí)行決定,因此我們需要知道該行代碼究竟做了什么绊寻,如下所示:
public static void insertDex(DexClassLoader dexClassLoader, ClassLoader baseClassLoader, File nativeLibsDir) throws Exception {
Object baseDexElements = getDexElements(getPathList(baseClassLoader));
Object newDexElements = getDexElements(getPathList(dexClassLoader));
Object allDexElements = combineArray(baseDexElements, newDexElements);
Object pathList = getPathList(baseClassLoader);
Reflector.with(pathList).field("dexElements").set(allDexElements);
insertNativeLibrary(dexClassLoader, baseClassLoader, nativeLibsDir);
}
??我們前面講到了PathClassLoader內(nèi)部有一個(gè)DexPathList類型的成員變量悬秉,DexPathList內(nèi)部又有一個(gè)Element類型的數(shù)組澄步,每個(gè)Element都對應(yīng)一個(gè)dex文件,類加載最終就是通過這個(gè)Element內(nèi)的DexFile進(jìn)行加載的村缸。那么如果我們構(gòu)造一個(gè)對應(yīng)插件dex的Element對象武氓,并把它添加到PathClassLoader的Element數(shù)組中梯皿,PathClassLoader不就可以加載插件中的類了嗎?VirtualApk這里正是用了這種思路县恕,上面代碼看到的getPathList()
和getDexElements()
都是通過反射獲取DexPathList對象以及Element數(shù)組。上面代碼首先獲取了baseClassLoader的Element數(shù)組忠烛,這個(gè)baseClassLoader就是宿主的PathClassLoader;接著再獲取dexClassLoader的Element數(shù)組冤议,這個(gè)dexClassLoader是為插件創(chuàng)建的DexClassLoader,創(chuàng)建DexClassLoader時(shí)傳遞了插件的路徑堪滨,DexClassLoader的Element數(shù)組內(nèi)包含了對應(yīng)我們插件的dex文件的Element對象尸疆;接著調(diào)用combineArray()
將前面獲得的兩個(gè)數(shù)組合并生成新的Element數(shù)組;最后再通過反射將這個(gè)新的Element數(shù)組賦值給宿主的PathClassLoader犯眠。
??那么接下來分析一下上述代碼對類加載流程的影響症革,首先需要明確的一點(diǎn)是噪矛,當(dāng)需要加載一個(gè)類的時(shí)候,除非我們顯式指定了用哪個(gè)類加載器加載(例如我們執(zhí)行classLoader.loadClass("")
)残炮,否則都會通過加載當(dāng)前類的類加載器進(jìn)行加載缩滨,例如我們想要在宿主的Activity加載一個(gè)插件類的時(shí)候(執(zhí)行Class.forName("")
),就會調(diào)用PathClassLoader的loadClass()
進(jìn)行加載苞冯。
??我們分以下幾種情況分別看看類加載的過程是怎樣的:
1. Constants.COMBINE_CLASSLOADER
為true
??此時(shí)會執(zhí)行DexUtil.insertDex()
侧巨,宿主的PathClassLoader既可以加載宿主類也可以加載插件類司忱。
- 在宿主中加載一個(gè)普通的插件類時(shí)(非四大組件啟動),會通過加載當(dāng)前宿主類的ClassLoader即PathClassLoader進(jìn)行加載禁添,由于PathClassLoader可以加載插件類桨踪,因此會由PathClassLoader完成加載芹啥。
- 在宿主中啟動一個(gè)插件的四大組件,這時(shí)候就會由插件對應(yīng)的DexClassLoader進(jìn)行加載(這個(gè)過程在下一節(jié)會詳細(xì)介紹汽纠,現(xiàn)在只要知道是調(diào)用DexClassLoader的
loadClass()
進(jìn)行加載就可以了)虱朵,這個(gè)過程遵循雙親委派模型,最終會先由宿主的PathClassLoader進(jìn)行加載絮宁,顯然PathClassLoader是可以完成加載的服协。 - 在插件類中加載類(本插件的類或是其他插件的類或是宿主類)偿荷,因?yàn)椴寮愂怯伤拗鞯腜athClassLoader加載,因此在加載任意的類時(shí)忍饰,都會調(diào)用PathClassLoader進(jìn)行加載寺庄,由于PathClassLoader包含了宿主以及各個(gè)插件的dex,因此都會由PathClassLoader完成加載饶深。
??那么我們總結(jié)一下逛拱,如果Constants.COMBINE_CLASSLOADER
為true朽合,所有的應(yīng)用類最終都會由PathClassLoader完成加載,DexClassLoader并沒有參與到任何類的實(shí)際加載過程中宪彩。另外在這種場景下讲婚,宿主和插件以及插件與插件之間是可以相互依賴的,整體是一個(gè)單ClassLoader架構(gòu)活合。
2. Constants.COMBINE_CLASSLOADER
為false
??此時(shí)不會執(zhí)行DexUtil.insertDex()
,因此宿主的PathClassLoader只能加載宿主類留晚,插件類只能由插件對應(yīng)的DexClassLoader加載告嘲。
- 在宿主中加載一個(gè)普通的插件類(非四大組件啟動)橄唬,會通過加載當(dāng)前宿主類的ClassLoader即PathClassLoader進(jìn)行加載,此時(shí)PathClassLoader無法加載插件類宏邮,因此會拋出ClassNotFoundException缸血。
- 在宿主中啟動一個(gè)插件的四大組件捎泻,這時(shí)候就會由插件對應(yīng)的DexClassLoader進(jìn)行加載,這時(shí)是可以完成類加載的郎汪,因此宿主可以正常啟動插件的四大組件闯狱。
- 在插件中加載本插件的類,此時(shí)會由加載本插件類的ClassLoader即插件對應(yīng)的DexClassLoader進(jìn)行加載照筑,這個(gè)過程顯然是沒有問題的瘦陈。
- 在插件中加載宿主類晨逝,此時(shí)會由加載本插件類的ClassLoader即插件對應(yīng)的DexClassLoader進(jìn)行加載,但根據(jù)雙親委派模型支鸡,會先交由宿主的PathClassLoader進(jìn)行加載,PathClassLoader可以完成宿主類的加載刘急。
- 在插件中加載其他插件的類(非四大組件啟動)浸踩,會通過加載當(dāng)前插件類的DexClassLoader進(jìn)行加載检碗,這個(gè)DexClassLoader只能加載本插件码邻,其父加載器PathClassLoader也無法加載插件類像屋,因此無法加載其他插件類,會拋出ClassNotFoundException奏甫。
- 在插件中啟動一個(gè)其他插件的四大組件凌受,這時(shí)候會由要啟動的四大組件所在的插件對應(yīng)的DexClassLoader進(jìn)行加載(這里還是先放一下,下一節(jié)會說到的)挠进,因此可以正常進(jìn)行類加載并啟動其他插件的四大組件誊册。
??這里也是總結(jié)下案怯,如果Constants.COMBINE_CLASSLOADER
為false,宿主與插件以及插件與插件之間的四大組件是可以正常啟動的于宙,插件可以調(diào)用宿主的類悍汛,但是宿主沒法加載并調(diào)用插件類离咐,插件之間的類也是無法相互加載調(diào)用的奉件∠孛玻可以看到這種場景下是多ClassLoader架構(gòu)的凑懂,宿主有專用的PathClassLoader,每個(gè)插件也有對應(yīng)的DexClassLoader摆碉,相比前一種場景宿主與插件之間的隔離性會更好巷帝,健壯性也會更好扫夜,例如當(dāng)不同插件依賴了同一類庫的不同版本時(shí),它們是可以相互共存的堕阔,因?yàn)椴煌惣虞d器加載出的類不被認(rèn)為是同一個(gè)望侈。
??到這里我們再回顧一下之前留下的問題脱衙,是不是宿主類會由PathClassLoader加載而插件類由DexClassLoader加載呢?想必看到這就很清晰了退唠,答案是不一定荤胁,取決于Constants.COMBINE_CLASSLOADER
的值仅政,如果為true所有的應(yīng)用程序類都會由宿主的PathClassLoader加載,插件的DexClassLoader沒有實(shí)際參與到類加載流程中滩愁;若為false硝枉,宿主的PathClassLoader只加載宿主類,插件類由插件對應(yīng)的DexClassLoader負(fù)責(zé)加載正压。
??通過上面的分析责球,VirtualApk對插件類加載的處理也都完成了雏逾,經(jīng)過上面的處理后,在需要加載一個(gè)類時(shí)都會自動地找到對應(yīng)的類加載器進(jìn)行加載。其實(shí)這里我覺得VirtualApk的實(shí)現(xiàn)不是特別完美笛匙,因?yàn)樵?code>Constants.COMBINE_CLASSLOADER為true的情況下犀变,宿主和插件之間可以完全的相互調(diào)用获枝,但是宿主和所有的插件都用同一個(gè)PathClassLoader加載健壯性會比較差;Constants.COMBINE_CLASSLOADER
為false時(shí)宿主和插件用單獨(dú)ClassLoader進(jìn)行加載健壯性變好了嚣崭,但相互之間的調(diào)用變得困難懦傍。那么有沒有一種方案既可以實(shí)現(xiàn)宿主和插件之間用不同的ClassLoader進(jìn)行加載粗俱,還能夠讓宿主與插件之間的調(diào)用沒有限制呢?
??答案是有的签财,出現(xiàn)宿主與插件之間不能相互調(diào)用的原因是加載類所需的ClassLoader并不在當(dāng)前的類加載結(jié)構(gòu)上偏塞,比如宿主想要加載插件的類烛愧,會調(diào)用PathClassLoader進(jìn)行加載掂碱,PathClassLoader的類加載結(jié)構(gòu)是PathClassLoader -> BootClassLoader疼燥,而加載插件所需的DexClassLoader并不在其中蚁堤,所以無法加載披诗。因此可以通過自定義一個(gè)ClassLoader,通過反射形成PathClassLoader -> 自定義ClassLoader -> BootClassLoader類加載結(jié)構(gòu)剥槐,這個(gè)ClassLoader不負(fù)責(zé)具體的類加載宪摧,只是接管了類加載流程几于,這樣就可以在自定義ClassLoader內(nèi)挑選合適的ClassLoader進(jìn)行加載,這樣就解決了上面的問題朽砰,感興趣的同學(xué)可以思考下如何實(shí)現(xiàn)瞧柔。
??插件類的成功加載也為后邊解決四大組件的插件化問題奠定了基礎(chǔ)睦裳,由于四大組件都不是普通類推沸,創(chuàng)建出實(shí)例它們還不能正常工作,它們需要頻繁與AMS通信肺素,且有復(fù)雜的生命周期需要處理宇驾,所以下一節(jié)我們將解決四大組件插件化的問題课舍。