插件化概述
??插件化技術(shù)最初源于免安裝運行apk的想法,這個免安裝的apk可以理解為插件。支持插件化的app可以在運行時加載和運行插件,這樣便可以將app中一些不常用的功能模塊做成插件,一方面減小了安裝包的大小,另一方面可以實現(xiàn)app功能的動態(tài)擴(kuò)展。插件化從最開始提出至今已經(jīng)發(fā)展的非常成熟了,也涌現(xiàn)出了非常多的開源框架,從最開始的Dynamic-load-apk到后來比較有名的RePlugin、VirtualApk,還有前段時間騰訊開源的Shadow,開發(fā)者們也可以根據(jù)自身的需求以及框架的特性,選擇最合適的框架。但是無論哪種框架,想要實現(xiàn)插件化,主要需要解決下面三個問題:
- 插件類的加載
- 四大組件生命周期的管理
- 插件資源的加載
??接下來會針對上面幾個問題進(jìn)行分析,我是基于滴滴開源的VirtualApk框架進(jìn)行分析,其他框架的實現(xiàn)與VirtualApk會有不同,但是插件化的原理大體是相似的,尤其是類加載以及資源加載,所以如果熟悉了一款插件化框架以后再去閱讀其他的插件化框架都是比較容易的。
VirtualApk倉庫鏈接
插件類的加載
??要想理解插件類加載的原理,必須要先對Java以及Android的類加載機制有所了解。這里我不打算深入地講這個問題,畢竟我們的主題不是這個,對Android類加載機制還不了解的同學(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,它們的類加載實現(xiàn)都在BaseDexClassLoader的findClass()中,這個方法我們后面會提到。
??這里說到了PathClassLoader和DexClassLoader,有必要說一下它們之間的區(qū)別。網(wǎng)上很多資料都流傳著PathClassLoader只能加載已安裝的apk,而DexClassLoader可以加載任意的apk,其實這種說法是錯誤的。我們以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)造方法,因為類加載相關(guān)的代碼都在其父類中。它們之間的區(qū)別就在于調(diào)用父類構(gòu)造方法的時候傳遞的第二個參數(shù),DexClassLoader傳遞的是一個File類型的對象,PathClassLoader固定傳遞null。這個參數(shù)會一直傳遞到native層進(jìn)行處理,具體邏輯比較復(fù)雜,最終這個路徑是用來存放dex2oat的產(chǎn)物.odex文件的,如果傳遞的是null,就會存放在默認(rèn)目錄下(/data/dalvik-cache)。所以optimizedDirectory的傳遞并不影響dex的加載,因此DexClassLoader和PathClassLoader都可以加載任意的apk。另外值得一提的一點就是,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)需要加載一個類的時候,都會通過一個默認(rèn)的PathClassLoader進(jìn)行加載,這個ClassLoader我們可以通過Context.getClassLoader()獲取到。和Java一樣的是,Android中ClassLoader同樣是遵循雙親委派模型的,其中PathClassLoader的父類加載器是BootClassLoader,BootClassLoader沒有父類加載器。所以如果我們要加載的類是系統(tǒng)類,最終會由BootClassLoader完成加載,如果要加載的是應(yīng)用程序類,則會交由PathClassLoader完成加載。那么PathClassLoader是如何完成類加載的呢?
??前面已經(jīng)說到了PathClassLoader和DexClassLoader都繼承自BaseDexClassLoader且它們的類加載實現(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是一個DexPathList類型的成員變量,因此我們再看一下DexPathList的findClass()的實現(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)部有一個成員變量dexElements,它是一個Element類型的數(shù)組,Element的內(nèi)部有一個DexFile類型的成員,而DexFile就是用于加載一個dex文件的。調(diào)用DexPathList的findClass()時,會遍歷dexElements,依次從每個Element中取出DexFile,調(diào)用DexFile的loadClassBinaryName(),該方法內(nèi)部會調(diào)用native方法從其對應(yīng)的dex文件中完成類的加載,更深入的過程我們就不需要關(guān)注了。
??看到這里我們對Android的類加載機制有了一定的了解,正常情況下Element數(shù)組只會包含宿主的dex信息,而我們的插件存放的位置可以是任意的,系統(tǒng)也并不知道插件的存在,所以正常情況下插件類是無法被加載的,因此我們需要特殊的處理解決插件類加載的問題。
VirtualApk源碼實現(xiàn)
??接下來我們就通過VirtualApk源碼來看一下VirtualApk是如何實現(xiàn)插件類的加載的。當(dāng)我們需要加載一個插件的時候,會調(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)建一個封裝了插件信息的LoadedPlugin對象,并將這個對象根據(jù)插件包名添加到mPlugins保存起來,createLoadedPlugin()內(nèi)部就直接通過構(gòu)造方法創(chuàng)建了一個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(),這個方法會調(diào)用PackageParser的parsePackage()去解析我們插件APK,得到一個包含在插件AndroidManifest文件中聲明的Application以及四大組件信息的Package對象,這個Package對象在四大組件插件化的實現(xiàn)上起著非常重要的作用,在四大組件插件化的章節(jié)我們會再次提到它。其實我們在安裝一個apk時,系統(tǒng)的PackageManagerService也會調(diào)用PackageParser的parsePackage()去解析我們的apk,這個過程是一樣的。接著在注釋2出調(diào)用createResources()創(chuàng)建用于加載插件資源的Resource對象,這個過程我們會在資源的插件化章節(jié)中進(jìn)行分析。注釋3調(diào)用createClassLoader()創(chuàng)建了一個ClassLoader對象,最后在注釋4處調(diào)用invokeApplication()根據(jù)注釋1解析到的Application信息實例化一個表示插件的Application對象并調(diào)用它的onCreate()方法。那我們這里重點關(guān)注一下createClassLoader()的實現(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)建了一個DexClassLoader,創(chuàng)建DexClassLoader時傳遞的第一個參數(shù)表示dex文件的路徑,這里傳遞了插件apk的絕對路徑,第四個參數(shù)表示其父類加載器,這里傳遞的parent是前面通過Context.getClassLoader()獲取到的,這個ClassLoader我們知道就是PathClassLoader,這樣形成了DexClassLoader -> PathClassLoader -> BootClassLoader的類加載結(jié)構(gòu),當(dāng)用插件對應(yīng)的DexClassLoader進(jìn)行加載時,根據(jù)雙親委派模型,會先交給宿主的PathClassLoader進(jìn)行加載并繼續(xù)向上傳遞。那么是不是宿主類會由PathClassLoader加載而插件類由DexClassLoader加載呢?答案是不一定,我們先繼續(xù)往下看,注釋2處判斷Constants.COMBINE_CLASSLOADER,若為真則執(zhí)行注釋3處代碼,Constants.COMBINE_CLASSLOADER是個常量為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)部有一個DexPathList類型的成員變量,DexPathList內(nèi)部又有一個Element類型的數(shù)組,每個Element都對應(yīng)一個dex文件,類加載最終就是通過這個Element內(nèi)的DexFile進(jìn)行加載的。那么如果我們構(gòu)造一個對應(yīng)插件dex的Element對象,并把它添加到PathClassLoader的Element數(shù)組中,PathClassLoader不就可以加載插件中的類了嗎?VirtualApk這里正是用了這種思路,上面代碼看到的getPathList()和getDexElements()都是通過反射獲取DexPathList對象以及Element數(shù)組。上面代碼首先獲取了baseClassLoader的Element數(shù)組,這個baseClassLoader就是宿主的PathClassLoader;接著再獲取dexClassLoader的Element數(shù)組,這個dexClassLoader是為插件創(chuàng)建的DexClassLoader,創(chuàng)建DexClassLoader時傳遞了插件的路徑,DexClassLoader的Element數(shù)組內(nèi)包含了對應(yīng)我們插件的dex文件的Element對象;接著調(diào)用combineArray()將前面獲得的兩個數(shù)組合并生成新的Element數(shù)組;最后再通過反射將這個新的Element數(shù)組賦值給宿主的PathClassLoader。
??那么接下來分析一下上述代碼對類加載流程的影響,首先需要明確的一點是,當(dāng)需要加載一個類的時候,除非我們顯式指定了用哪個類加載器加載(例如我們執(zhí)行classLoader.loadClass("")),否則都會通過加載當(dāng)前類的類加載器進(jìn)行加載,例如我們想要在宿主的Activity加載一個插件類的時候(執(zhí)行Class.forName("")),就會調(diào)用PathClassLoader的loadClass()進(jìn)行加載。
??我們分以下幾種情況分別看看類加載的過程是怎樣的:
1. Constants.COMBINE_CLASSLOADER為true
??此時會執(zhí)行DexUtil.insertDex(),宿主的PathClassLoader既可以加載宿主類也可以加載插件類。
- 在宿主中加載一個普通的插件類時(非四大組件啟動),會通過加載當(dāng)前宿主類的ClassLoader即PathClassLoader進(jìn)行加載,由于PathClassLoader可以加載插件類,因此會由PathClassLoader完成加載。
- 在宿主中啟動一個插件的四大組件,這時候就會由插件對應(yīng)的DexClassLoader進(jìn)行加載(這個過程在下一節(jié)會詳細(xì)介紹,現(xiàn)在只要知道是調(diào)用DexClassLoader的
loadClass()進(jìn)行加載就可以了),這個過程遵循雙親委派模型,最終會先由宿主的PathClassLoader進(jìn)行加載,顯然PathClassLoader是可以完成加載的。 - 在插件類中加載類(本插件的類或是其他插件的類或是宿主類),因為插件類是由宿主的PathClassLoader加載,因此在加載任意的類時,都會調(diào)用PathClassLoader進(jìn)行加載,由于PathClassLoader包含了宿主以及各個插件的dex,因此都會由PathClassLoader完成加載。
??那么我們總結(jié)一下,如果Constants.COMBINE_CLASSLOADER為true,所有的應(yīng)用類最終都會由PathClassLoader完成加載,DexClassLoader并沒有參與到任何類的實際加載過程中。另外在這種場景下,宿主和插件以及插件與插件之間是可以相互依賴的,整體是一個單ClassLoader架構(gòu)。
2. Constants.COMBINE_CLASSLOADER為false
??此時不會執(zhí)行DexUtil.insertDex(),因此宿主的PathClassLoader只能加載宿主類,插件類只能由插件對應(yīng)的DexClassLoader加載。
- 在宿主中加載一個普通的插件類(非四大組件啟動),會通過加載當(dāng)前宿主類的ClassLoader即PathClassLoader進(jìn)行加載,此時PathClassLoader無法加載插件類,因此會拋出ClassNotFoundException。
- 在宿主中啟動一個插件的四大組件,這時候就會由插件對應(yīng)的DexClassLoader進(jìn)行加載,這時是可以完成類加載的,因此宿主可以正常啟動插件的四大組件。
- 在插件中加載本插件的類,此時會由加載本插件類的ClassLoader即插件對應(yīng)的DexClassLoader進(jìn)行加載,這個過程顯然是沒有問題的。
- 在插件中加載宿主類,此時會由加載本插件類的ClassLoader即插件對應(yīng)的DexClassLoader進(jìn)行加載,但根據(jù)雙親委派模型,會先交由宿主的PathClassLoader進(jìn)行加載,PathClassLoader可以完成宿主類的加載。
- 在插件中加載其他插件的類(非四大組件啟動),會通過加載當(dāng)前插件類的DexClassLoader進(jìn)行加載,這個DexClassLoader只能加載本插件,其父加載器PathClassLoader也無法加載插件類,因此無法加載其他插件類,會拋出ClassNotFoundException。
- 在插件中啟動一個其他插件的四大組件,這時候會由要啟動的四大組件所在的插件對應(yīng)的DexClassLoader進(jìn)行加載(這里還是先放一下,下一節(jié)會說到的),因此可以正常進(jìn)行類加載并啟動其他插件的四大組件。
??這里也是總結(jié)下,如果Constants.COMBINE_CLASSLOADER為false,宿主與插件以及插件與插件之間的四大組件是可以正常啟動的,插件可以調(diào)用宿主的類,但是宿主沒法加載并調(diào)用插件類,插件之間的類也是無法相互加載調(diào)用的??梢钥吹竭@種場景下是多ClassLoader架構(gòu)的,宿主有專用的PathClassLoader,每個插件也有對應(yīng)的DexClassLoader,相比前一種場景宿主與插件之間的隔離性會更好,健壯性也會更好,例如當(dāng)不同插件依賴了同一類庫的不同版本時,它們是可以相互共存的,因為不同類加載器加載出的類不被認(rèn)為是同一個。
??到這里我們再回顧一下之前留下的問題,是不是宿主類會由PathClassLoader加載而插件類由DexClassLoader加載呢?想必看到這就很清晰了,答案是不一定,取決于Constants.COMBINE_CLASSLOADER的值,如果為true所有的應(yīng)用程序類都會由宿主的PathClassLoader加載,插件的DexClassLoader沒有實際參與到類加載流程中;若為false,宿主的PathClassLoader只加載宿主類,插件類由插件對應(yīng)的DexClassLoader負(fù)責(zé)加載。
??通過上面的分析,VirtualApk對插件類加載的處理也都完成了,經(jīng)過上面的處理后,在需要加載一個類時都會自動地找到對應(yīng)的類加載器進(jìn)行加載。其實這里我覺得VirtualApk的實現(xiàn)不是特別完美,因為在Constants.COMBINE_CLASSLOADER為true的情況下,宿主和插件之間可以完全的相互調(diào)用,但是宿主和所有的插件都用同一個PathClassLoader加載健壯性會比較差;Constants.COMBINE_CLASSLOADER為false時宿主和插件用單獨ClassLoader進(jìn)行加載健壯性變好了,但相互之間的調(diào)用變得困難。那么有沒有一種方案既可以實現(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并不在其中,所以無法加載。因此可以通過自定義一個ClassLoader,通過反射形成PathClassLoader -> 自定義ClassLoader -> BootClassLoader類加載結(jié)構(gòu),這個ClassLoader不負(fù)責(zé)具體的類加載,只是接管了類加載流程,這樣就可以在自定義ClassLoader內(nèi)挑選合適的ClassLoader進(jìn)行加載,這樣就解決了上面的問題,感興趣的同學(xué)可以思考下如何實現(xiàn)。
??插件類的成功加載也為后邊解決四大組件的插件化問題奠定了基礎(chǔ),由于四大組件都不是普通類,創(chuàng)建出實例它們還不能正常工作,它們需要頻繁與AMS通信,且有復(fù)雜的生命周期需要處理,所以下一節(jié)我們將解決四大組件插件化的問題。