導(dǎo)語(yǔ) 插件化技術(shù)最早從2012年誕生至今,已經(jīng)走過了5個(gè)年頭霍殴。從最初只支持Activity的動(dòng)態(tài)加載發(fā)展到可以完全模擬app運(yùn)行時(shí)的沙箱系統(tǒng)媒惕,各種開源項(xiàng)目層出不窮,在此挑選了幾個(gè)代表性的框架来庭,總結(jié)其中的技術(shù)原理妒蔚。由于本人水平有限,插件化框架又相當(dāng)復(fù)雜月弛,文中若有錯(cuò)誤或者不準(zhǔn)確的地方望高手指點(diǎn)肴盏。
內(nèi)容概要
一、發(fā)展歷史
插件化技術(shù)最初源于免安裝運(yùn)行apk的想法帽衙,這個(gè)免安裝的apk可以理解為插件菜皂。支持插件化的app可以在運(yùn)行時(shí)加載和運(yùn)行插件,這樣便可以將app中一些不常用的功能模塊做成插件厉萝,一方面減小了安裝包的大小恍飘,另一方面可以實(shí)現(xiàn)app功能的動(dòng)態(tài)擴(kuò)展。想要實(shí)現(xiàn)插件化冀泻,主要是解決下面三個(gè)問題:
- 插件中代碼的加載和與主工程的互相調(diào)用
- 插件中資源的加載和與主工程的互相訪問
- 四大組件生命周期的管理
下面是比較出名的幾個(gè)開源的插件化框架常侣,按照出現(xiàn)的時(shí)間排序。研究它們的實(shí)現(xiàn)原理弹渔,可以大致看出插件化技術(shù)的發(fā)展胳施,根據(jù)實(shí)現(xiàn)原理我把這幾個(gè)框架劃分成了三代。
發(fā)展 | 1 | 2 | 3 |
---|---|---|---|
第一代 | dynamic-load-apk | 早期DroidPlugin | |
第二代 | VirtualAPK | Small | RePlugin |
第三代 | VirtualApp | Atlas |
第一代:dynamic-load-apk最早使用ProxyActivity這種靜態(tài)代理技術(shù)肢专,由ProxyActivity去控制插件中PluginActivity的生命周期舞肆。該種方式缺點(diǎn)明顯焦辅,插件中的activity必須繼承PluginActivity,開發(fā)時(shí)要小心處理context椿胯。而DroidPlugin通過Hook系統(tǒng)服務(wù)的方式啟動(dòng)插件中的Activity筷登,使得開發(fā)插件的過程和開發(fā)普通的app沒有什么區(qū)別,但是由于hook過多系統(tǒng)服務(wù)哩盲,異常復(fù)雜且不夠穩(wěn)定前方。
第二代:為了同時(shí)達(dá)到插件開發(fā)的低侵入性(像開發(fā)普通app一樣開發(fā)插件)和框架的穩(wěn)定性,在實(shí)現(xiàn)原理上都是趨近于選擇盡量少的hook廉油,并通過在manifest中預(yù)埋一些組件實(shí)現(xiàn)對(duì)四大組件的插件化惠险。另外各個(gè)框架根據(jù)其設(shè)計(jì)思想都做了不同程度的擴(kuò)展,其中Small更是做成了一個(gè)跨平臺(tái)抒线,組件化的開發(fā)框架班巩。
第三代:VirtualApp比較厲害,能夠完全模擬app的運(yùn)行環(huán)境嘶炭,能夠?qū)崿F(xiàn)app的免安裝運(yùn)行和雙開技術(shù)抱慌。Atlas是阿里去年開源出來(lái)的一個(gè)結(jié)合組件化和熱修復(fù)技術(shù)的一個(gè)app基礎(chǔ)框架,其廣泛的應(yīng)用與阿里系的各個(gè)app眨猎,其號(hào)稱是一個(gè)容器化框架抑进。
下面詳細(xì)介紹插件化框架的原理,分別對(duì)應(yīng)著實(shí)現(xiàn)插件化的三個(gè)核心問題宵呛。
二单匣、基本原理
2.1 類加載
外部apk中類的加載
Android中常用的有兩種類加載器,DexClassLoader和PathClassLoader宝穗,它們都繼承于BaseDexClassLoader户秤。
// DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
// PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
區(qū)別在于調(diào)用父類構(gòu)造器時(shí),DexClassLoader多傳了一個(gè)optimizedDirectory參數(shù)逮矛,這個(gè)目錄必須是內(nèi)部存儲(chǔ)路徑鸡号,用來(lái)緩存系統(tǒng)創(chuàng)建的Dex文件。而PathClassLoader該參數(shù)為null须鼎,只能加載內(nèi)部存儲(chǔ)目錄的Dex文件鲸伴。
所以我們可以用DexClassLoader去加載外部的apk,用法如下
//第一個(gè)參數(shù)為apk的文件目錄
//第二個(gè)參數(shù)為內(nèi)部存儲(chǔ)目錄
//第三個(gè)為庫(kù)文件的存儲(chǔ)目錄
//第四個(gè)參數(shù)為父加載器
new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent)
雙親委托機(jī)制
ClassLoader調(diào)用loadClass方法加載類
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
//首先從已經(jīng)加載的類中查找
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
//如果沒有加載過晋控,先調(diào)用父加載器的loadClass
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
//父加載器都沒有加載汞窗,則嘗試加載
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
可以看出ClassLoader加載類時(shí),先查看自身是否已經(jīng)加載過該類赡译,如果沒有加載過會(huì)首先讓父加載器去加載仲吏,如果父加載器無(wú)法加載該類時(shí)才會(huì)調(diào)用自身的findClass方法加載,該機(jī)制很大程度上避免了類的重復(fù)加載。
DexClassLoader的DexPathList
DexClassLoader重載了findClass方法裹唆,在加載類時(shí)會(huì)調(diào)用其內(nèi)部的DexPathList去加載誓斥。DexPathList是在構(gòu)造DexClassLoader時(shí)生成的,其內(nèi)部包含了DexFile许帐。如下圖所示
DexPathList的loadClass會(huì)去遍歷DexFile直到找到需要加載的類
public Class findClass(String name, List<Throwable> suppressed) {
//循環(huán)dexElements劳坑,調(diào)用DexFile.loadClassBinaryName加載class
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;
}
有一種熱修復(fù)技術(shù)正是利用了DexClassLoader的加載機(jī)制,將需要替換的類添加到dexElements的前面成畦,這樣系統(tǒng)會(huì)使用先找到的修復(fù)過的類距芬。
2.2 單DexClassLoader與多DexClassLoader
通過給插件apk生成相應(yīng)的DexClassLoader便可以訪問其中的類,這邊又有兩種處理方式羡鸥,有單DexClassLoader和多DexClassLoader兩種結(jié)構(gòu)蔑穴。
多DexClassLoader
對(duì)于每個(gè)插件都會(huì)生成一個(gè)DexClassLoader,當(dāng)加載該插件中的類時(shí)需要通過對(duì)應(yīng)DexClassLoader加載惧浴。這樣不同插件的類是隔離的,當(dāng)不同插件引用了同一個(gè)類庫(kù)的不同版本時(shí)奕剃,不會(huì)出問題衷旅。RePlugin采用的是該方案。
單DexClassLoader
將插件的DexClassLoader中的pathList合并到主工程的DexClassLoader中纵朋。這樣做的好處時(shí)柿顶,可以在不同的插件以及主工程間直接互相調(diào)用類和方法,并且可以將不同插件的公共模塊抽出來(lái)放在一個(gè)common插件中直接供其他插件使用操软。Small采用的是這種方式嘁锯。
互相調(diào)用
插件和主工程的互相調(diào)用涉及到以下兩個(gè)問題
插件調(diào)用主工程
- 在構(gòu)造插件的ClassLoader時(shí)會(huì)傳入主工程的ClassLoader作為父加載器,所以插件是可以直接可以通過類名引用主工程的類聂薪。
主工程調(diào)用插件
- 若使用多ClassLoader機(jī)制家乘,主工程引用插件中類需要先通過插件的ClassLoader加載該類再通過反射調(diào)用其方法。插件化框架一般會(huì)通過統(tǒng)一的入口去管理對(duì)各個(gè)插件中類的訪問藏澳,并且做一定的限制仁锯。
- 若使用單ClassLoader機(jī)制,主工程則可以直接通過類名去訪問插件中的類翔悠。該方式有個(gè)弊病业崖,若兩個(gè)不同的插件工程引用了一個(gè)庫(kù)的不同版本,則程序可能會(huì)出錯(cuò)蓄愁,所以要通過一些規(guī)范去避免該情況發(fā)生双炕。
2.3 資源加載
Android系統(tǒng)通過Resource對(duì)象加載資源,下面代碼展示了該對(duì)象的生成過程
//創(chuàng)建AssetManager對(duì)象
AssetManager assets = new AssetManager();
//將apk路徑添加到AssetManager中
if (assets.addAssetPath(resDir) == 0){
return null;
}
//創(chuàng)建Resource對(duì)象
r = new Resources(assets, metrics, getConfiguration(), compInfo);
因此撮抓,只要將插件apk的路徑加入到AssetManager中妇斤,便能夠?qū)崿F(xiàn)對(duì)插件資源的訪問。
具體實(shí)現(xiàn)時(shí),由于AssetManager并不是一個(gè)public的類趟济,需要通過反射去創(chuàng)建乱投,并且部分Rom對(duì)創(chuàng)建的Resource類進(jìn)行了修改,所以需要考慮不同Rom的兼容性顷编。
資源路徑的處理
和代碼加載相似戚炫,插件和主工程的資源關(guān)系也有兩種處理方式
- 合并式:addAssetPath時(shí)加入所有插件和主工程的路徑
- 獨(dú)立式:各個(gè)插件只添加自己apk路徑
方式 | 優(yōu)點(diǎn) | 缺點(diǎn) |
---|---|---|
合并式 | 插件和主工程能夠直接相互訪問資源 | 會(huì)引入資源沖突 |
獨(dú)立式 | 資源隔離,不存在資源沖突 | 資源共享比較麻煩 |
合并式由于AssetManager中加入了所有插件和主工程的路徑媳纬,因此生成的Resource可以同時(shí)訪問插件和主工程的資源吃度。但是由于主工程和各個(gè)插件都是獨(dú)立編譯的,生成的資源id會(huì)存在相同的情況鸟辅,在訪問時(shí)會(huì)產(chǎn)生資源沖突橄杨。
獨(dú)立式時(shí),各個(gè)插件的資源是互相隔離的素挽,不過如果想要實(shí)現(xiàn)資源的共享蔑赘,必須拿到對(duì)應(yīng)的Resource對(duì)象。
Context的處理
通常我們通過Context對(duì)象訪問資源预明,光創(chuàng)建出Resource對(duì)象還不夠缩赛,因此還需要一些額外的工作。 對(duì)資源訪問的不同實(shí)現(xiàn)方式也需要不同的額外工作撰糠。以VirtualAPK的處理方式為例
第一步:創(chuàng)建Resource
if (Constants.COMBINE_RESOURCES) {
//插件和主工程資源合并時(shí)需要hook住主工程的資源
Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath());
ResourcesManager.hookResources(context, resources);
return resources;
} else {
//插件資源獨(dú)立酥馍,該resource只能訪問插件自己的資源
Resources hostResources = context.getResources();
AssetManager assetManager = createAssetManager(context, apk);
return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
第二步:hook主工程的Resource
對(duì)于合并式的資源訪問方式,需要替換主工程的Resource阅酪,下面是具體替換的代碼旨袒。
public static void hookResources(Context base, Resources resources) {
try {
ReflectUtil.setField(base.getClass(), base, "mResources", resources);
Object loadedApk = ReflectUtil.getPackageInfo(base);
ReflectUtil.setField(loadedApk.getClass(), loadedApk, "mResources", resources);
Object activityThread = ReflectUtil.getActivityThread(base);
Object resManager = ReflectUtil.getField(activityThread.getClass(), activityThread, "mResourcesManager");
if (Build.VERSION.SDK_INT < 24) {
Map<Object, WeakReference<Resources>> map = (Map<Object, WeakReference<Resources>>) ReflectUtil.getField(resManager.getClass(), resManager, "mActiveResources");
Object key = map.keySet().iterator().next();
map.put(key, new WeakReference<>(resources));
} else {
// still hook Android N Resources, even though it's unnecessary, then nobody will be strange.
Map map = (Map) ReflectUtil.getFieldNoException(resManager.getClass(), resManager, "mResourceImpls");
Object key = map.keySet().iterator().next();
Object resourcesImpl = ReflectUtil.getFieldNoException(Resources.class, resources, "mResourcesImpl");
map.put(key, new WeakReference<>(resourcesImpl));
}
} catch (Exception e) {
e.printStackTrace();
}
}
注意下上述代碼hook了幾個(gè)地方,包括以下幾個(gè)hook點(diǎn)
- 替換了主工程context中LoadedApk的mResource對(duì)象
- 將新的Resource添加到主工程ActivityThread的mResourceManager中术辐,并且根據(jù)Android版本做了不同處理
第三步:關(guān)聯(lián)resource和Activity
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
activity.setIntent(intent);
//設(shè)置Activity的mResources屬性砚尽,Activity中訪問資源時(shí)都通過mResources
ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
上述代碼是在Activity創(chuàng)建時(shí)被調(diào)用的(后面會(huì)介紹如何hook Activity的創(chuàng)建過程),在activity被構(gòu)造出來(lái)后术吗,需要替換其中的mResources為插件的Resource尉辑。由于獨(dú)立式時(shí)主工程的Resource不能訪問插件的資源,所以如果不做替換较屿,會(huì)產(chǎn)生資源訪問錯(cuò)誤隧魄。
做完以上工作后,則可以在插件的Activity中放心的使用setContentView隘蝎,inflater等方法加載布局了购啄。
資源沖突
合并式的資源處理方式,會(huì)引入資源沖突嘱么,原因在于不同插件中的資源id可能相同狮含,所以解決方法就是使得不同的插件資源擁有不同的資源id。
資源id是由8位16進(jìn)制數(shù)表示,表示為0xPPTTNNNN几迄。PP段用來(lái)區(qū)分包空間蔚龙,默認(rèn)只區(qū)分了應(yīng)用資源和系統(tǒng)資源,TT段為資源類型映胁,NNNN段在同一個(gè)APK中從0000遞增木羹。如下表所示
類別 | PP段 | TT段 | NNNN段 |
---|---|---|---|
應(yīng)用資源 | 0x7f | 04 | 0000 |
系統(tǒng)資源 | 0x01 | 04 | 0000 |
所以思路是修改資源ID的PP段,對(duì)于不同的插件使用不同的PP段解孙,從而區(qū)分不同插件的資源坑填。具體實(shí)現(xiàn)方式有兩種
- 修改aapt源碼,編譯期修改PP段弛姜。
- 修改resources.arsc文件脐瑰,該文件列出了資源id到具體資源路徑的映射。
具體實(shí)現(xiàn)可以分別參考Atlas框架和Small框架廷臼。推薦第二種方式苍在,不用入侵原有的編譯流程。
三荠商、四大組件支持
Android開發(fā)中有一些特殊的類忌穿,是由系統(tǒng)創(chuàng)建的,并且由系統(tǒng)管理生命周期结啼。如常用的四大組件,Activity屈芜,Service郊愧,BroadcastReceiver和ContentProvider。 僅僅構(gòu)造出這些類的實(shí)例是沒用的井佑,還需要管理組件的生命周期属铁。其中以Activity最為復(fù)雜,不同框架采用的方法也不盡相同躬翁。下面以Activity為例詳細(xì)介紹插件化如何支持組件生命周期的管理焦蘑。 大致分為兩種方式:
- ProxyActivity代理
- 預(yù)埋StubActivity,hook系統(tǒng)啟動(dòng)Activity的過程
3.1 ProxyActivity代理
ProxyActivity代理的方式最早是由dynamic-load-apk提出的盒发,其思想很簡(jiǎn)單例嘱,在主工程中放一個(gè)ProxyActivy,啟動(dòng)插件中的Activity時(shí)會(huì)先啟動(dòng)ProxyActivity宁舰,在ProxyActivity中創(chuàng)建插件Activity拼卵,并同步生命周期。下圖展示了啟動(dòng)插件Activity的過程蛮艰。
- 首先需要通過統(tǒng)一的入口(如圖中的PluginManager)啟動(dòng)插件Activity腋腮,其內(nèi)部會(huì)將啟動(dòng)的插件Activity信息保存下來(lái),并將intent替換為啟動(dòng)ProxyActivity的intent。
- ProxyActivity根據(jù)插件的信息拿到該插件的ClassLoader和Resource即寡,通過反射創(chuàng)建PluginActivity并調(diào)用其onCreate方法徊哑。
- PluginActivty調(diào)用的setContentView被重寫了,會(huì)去調(diào)用ProxyActivty的setContentView聪富。由于ProxyActivity重寫了getResource返回的是插件的Resource莺丑,所以setContentView能夠訪問到插件中的資源。同樣findViewById也是調(diào)用ProxyActivity的善涨。
- ProxyActivity中的其他生命周期回調(diào)函數(shù)中調(diào)用相應(yīng)PluginActivity的生命周期窒盐。
代理方式的關(guān)鍵總結(jié)起來(lái)有下面兩點(diǎn):
- ProxyActivity中需要重寫getResouces,getAssets钢拧,getClassLoader方法返回插件的相應(yīng)對(duì)象蟹漓。生命周期函數(shù)以及和用戶交互相關(guān)函數(shù),如onResume源内,onStop葡粒,onBackPressedon,KeyUponWindow膜钓,F(xiàn)ocusChanged等需要轉(zhuǎn)發(fā)給插件嗽交。
- PluginActivity中所有調(diào)用context的相關(guān)的方法,如setContentView颂斜,getLayoutInflater夫壁,getSystemService等都需要調(diào)用ProxyActivity的相應(yīng)方法。
該方式有幾個(gè)明顯缺點(diǎn):
- 插件中的Activity必須繼承PluginActivity沃疮,開發(fā)侵入性強(qiáng)盒让。
- 如果想支持Activity的singleTask,singleInstance等launchMode時(shí)司蔬,需要自己管理Activity棧邑茄,實(shí)現(xiàn)起來(lái)很繁瑣。
- 插件中需要小心處理Context俊啼,容易出錯(cuò)肺缕。
- 如果想把之前的模塊改造成插件需要很多額外的工作。
該方式雖然能夠很好的實(shí)現(xiàn)啟動(dòng)插件Activity的目的授帕,但是由于開發(fā)式侵入性很強(qiáng)同木,dynamic-load-apk之后的插件化方案很少繼續(xù)使用該方式,而是通過hook系統(tǒng)啟動(dòng)Activity的過程豪墅,讓啟動(dòng)插件中的Activity像啟動(dòng)主工程的Activity一樣簡(jiǎn)單泉手。
3.2 hook方式
在介紹hook方式之前,先用一張圖簡(jiǎn)要的介紹下系統(tǒng)是如何啟動(dòng)一個(gè)Activity的偶器。
上圖列出的是啟動(dòng)一個(gè)Activity的主要過程斩萌,具體步驟如下:
- Activity1調(diào)用startActivity缝裤,實(shí)際會(huì)調(diào)用Instrumentation類的execStartActivity方法,Instrumentation是系統(tǒng)用來(lái)監(jiān)控Activity運(yùn)行的一個(gè)類颊郎,Activity的整個(gè)生命周期都有它的影子憋飞。
- 通過跨進(jìn)程的binder調(diào)用,進(jìn)入到ActivityManagerService中姆吭,其內(nèi)部會(huì)處理Activity棧榛做。之后又通過跨進(jìn)程調(diào)用進(jìn)入到Activity2所在的進(jìn)程中。
- ApplicationThread是一個(gè)binder對(duì)象内狸,其運(yùn)行在binder線程池中检眯,內(nèi)部包含一個(gè)H類,該類繼承于類Handler昆淡。ApplicationThread將啟動(dòng)Activity2的信息通過H對(duì)象發(fā)送給主線程锰瘸。
- 主線程拿到Activity2的信息后,調(diào)用Instrumentation類的newActivity方法昂灵,其內(nèi)通過ClassLoader創(chuàng)建Activity2實(shí)例避凝。
下面介紹如何通過hook的方式啟動(dòng)插件中的Activity,需要解決以下兩個(gè)問題
- 插件中的Activity沒有在AndroidManifest中注冊(cè)眨补,如何繞過檢測(cè)管削。
- 如何構(gòu)造Activity實(shí)例,同步生命周期
解決方法有很多種撑螺,以VirtualAPK為例含思,核心思路如下:
- 先在Manifest中預(yù)埋StubActivity,啟動(dòng)時(shí)hook上圖第1步甘晤,將Intent替換成StubActivity茸俭。
- hook第10步,通過插件的ClassLoader反射創(chuàng)建插件Activity
- 之后Activity的所有生命周期回調(diào)都會(huì)通知給插件Activity
下面具體分析整個(gè)過程涉及到的代碼:
替換系統(tǒng)Instrumentation
VirtualAPK在初始化時(shí)會(huì)調(diào)用hookInstrumentationAndHandler安皱,該方法hook了系統(tǒng)的Instrumentaiton類,由上文可知該類和Activity的啟動(dòng)息息相關(guān)艇炎。
private void hookInstrumentationAndHandler() {
try {
//獲取Instrumentation對(duì)象
Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext);
//構(gòu)造自定義的VAInstrumentation
final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation);
//設(shè)置ActivityThread的mInstrumentation和mCallBack
Object activityThread = ReflectUtil.getActivityThread(this.mContext);
ReflectUtil.setInstrumentation(activityThread, instrumentation);
ReflectUtil.setHandlerCallback(this.mContext, instrumentation);
this.mInstrumentation = instrumentation;
} catch (Exception e) {
e.printStackTrace();
}
}
該段代碼將主線程中的Instrumentation對(duì)象替換成了自定義的VAInstrumentation類酌伊。在啟動(dòng)和創(chuàng)建插件activity時(shí),該類都會(huì)偷偷做一些手腳缀踪。
hook activity啟動(dòng)過程
VAInstrumentation類重寫了execStartActivity方法居砖,圖 3.2中的第一步。
public ActivityResult execStartActivity(
//省略了無(wú)關(guān)參數(shù)
Intent intent) {
//轉(zhuǎn)換隱式intent
mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
if (intent.getComponent() != null) {
//替換intent中啟動(dòng)Activity為StubActivity
this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
}
//調(diào)用父類啟動(dòng)Activity的方法
}
public void markIntentIfNeeded(Intent intent) {
if (intent.getComponent() == null) {
return;
}
String targetPackageName = intent.getComponent().getPackageName();
String targetClassName = intent.getComponent().getClassName();
// search map and return specific launchmode stub activity
if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
intent.putExtra(Constants.KEY_IS_PLUGIN, true);
intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
dispatchStubActivity(intent);
}
}
execStartActivity中會(huì)先去處理隱式intent驴娃,如果該隱式intent匹配到了插件中的Activity奏候,將其轉(zhuǎn)換成顯式。之后通過markIntentIfNeeded將待啟動(dòng)的的插件Activity替換成了預(yù)先在AndroidManifest中占坑的StubActivity唇敞,并將插件Activity的信息保存到該intent中蔗草。其中有個(gè)dispatchStubActivity函數(shù)咒彤,會(huì)根據(jù)Activity的launchMode選擇具體啟動(dòng)哪個(gè)StubActivity。VirtualAPK為了支持Activity的launchMode在主工程的AndroidManifest中對(duì)于每種啟動(dòng)模式的Activity都預(yù)埋了多個(gè)坑位咒精。
hook Activity的創(chuàng)建過程
上一步欺騙了系統(tǒng)镶柱,讓系統(tǒng)以為自己?jiǎn)?dòng)的是一個(gè)正常的Activity。當(dāng)來(lái)到圖 3.2的第10步時(shí)模叙,再將插件的Activity換回來(lái)歇拆。此時(shí)調(diào)用的是VAInstrumentation類的newActivity方法。
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent){
try {
cl.loadClass(className);
} catch (ClassNotFoundException e) {
//通過LoadedPlugin可以獲取插件的ClassLoader和Resource
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
//獲取插件的主Activity
String targetClassName = PluginUtil.getTargetActivity(intent);
if (targetClassName != null) {
//傳入插件的ClassLoader構(gòu)造插件Activity
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
activity.setIntent(intent);
//設(shè)置插件的Resource范咨,從而可以支持插件中資源的訪問
try {
ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
} catch (Exception ignored) {
// ignored.
}
return activity;
}
}
return mBase.newActivity(cl, className, intent);
}
由于AndroidManifest中預(yù)埋的StubActivity并沒有具體的實(shí)現(xiàn)類故觅,所以此時(shí)會(huì)發(fā)生ClassNotFoundException。之后在處理異常時(shí)取出插件Activity的信息渠啊,通過插件的ClassLoader反射構(gòu)造插件的Activity输吏。
一些額外操作
插件Activity構(gòu)造出來(lái)后,為了能夠保證其正常運(yùn)行還要做些額外的工作昭抒。VAInstrumentation類在圖3.2中的第11步中也做了一些處理评也。
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
final Intent intent = activity.getIntent();
if (PluginUtil.isIntentFromPlugin(intent)) {
Context base = activity.getBaseContext();
try {
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
ReflectUtil.setField(base.getClass(), base, "mResources", plugin.getResources());
ReflectUtil.setField(ContextWrapper.class, activity, "mBase", plugin.getPluginContext());
ReflectUtil.setField(Activity.class, activity, "mApplication", plugin.getApplication());
ReflectUtil.setFieldNoException(ContextThemeWrapper.class, activity, "mBase", plugin.getPluginContext());
// set screenOrientation
ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
activity.setRequestedOrientation(activityInfo.screenOrientation);
}
} catch (Exception e) {
e.printStackTrace();
}
}
mBase.callActivityOnCreate(activity, icicle);
}
這段代碼主要是將Activity中的Resource,Context等對(duì)象替換成了插件的相應(yīng)對(duì)象灭返,保證插件Activity在調(diào)用涉及到Context的方法時(shí)能夠正確運(yùn)行盗迟。
經(jīng)過上述步驟后,便實(shí)現(xiàn)了插件Activity的啟動(dòng)熙含,并且該插件Activity中并不需要什么額外的處理罚缕,和常規(guī)的Activity一樣。那問題來(lái)了怎静,之后的onResume邮弹,onStop等生命周期怎么辦呢?答案是所有和Activity相關(guān)的生命周期函數(shù)蚓聘,系統(tǒng)都會(huì)調(diào)用插件中的Activity腌乡。原因在于AMS在處理Activity時(shí),通過一個(gè)token表示具體Activity對(duì)象夜牡,而這個(gè)token正是和啟動(dòng)Activity時(shí)創(chuàng)建的對(duì)象對(duì)應(yīng)的与纽,而這個(gè)Activity被我們替換成了插件中的Activity,所以之后AMS的所有調(diào)用都會(huì)傳給插件中的Activity塘装。
小結(jié)
VirtualAPK通過替換了系統(tǒng)的Instrumentation急迂,hook了Activity的啟動(dòng)和創(chuàng)建,省去了手動(dòng)管理插件Activity生命周期的繁瑣蹦肴,讓插件Activity像正常的Activity一樣被系統(tǒng)管理僚碎,并且插件Activity在開發(fā)時(shí)和常規(guī)一樣,即能獨(dú)立運(yùn)行又能作為插件被主工程調(diào)用阴幌。
其他插件框架在處理Activity時(shí)思想大都差不多勺阐,無(wú)非是這兩種方式之一或者兩者的結(jié)合卷中。在hook時(shí),不同的框架可能會(huì)選擇不同的hook點(diǎn)皆看。如360的RePlugin框架選擇hook了系統(tǒng)的ClassLoader仓坞,即圖3.2中構(gòu)造Activity2的ClassLoader,在判斷出待啟動(dòng)的Activity是插件中的時(shí)腰吟,會(huì)調(diào)用插件的ClassLoader構(gòu)造相應(yīng)對(duì)象无埃。另外RePlugin為了系統(tǒng)穩(wěn)定性,選擇了盡量少的hook毛雇,因此它并沒有選擇hook系統(tǒng)的startActivity方法來(lái)替換intent嫉称,而是通過重寫Activity的startActivity,因此其插件Activity是需要繼承一個(gè)類似PluginActivity的基類的灵疮。不過RePlugin提供了一個(gè)Gradle插件將插件中的Activity的基類換成了PluginActivity织阅,用戶在開發(fā)插件Activity時(shí)也是沒有感知的。
3.3 其他組件
四大組件中Activity的支持是最復(fù)雜的震捣,其他組件的實(shí)現(xiàn)原理要簡(jiǎn)單很多荔棉,簡(jiǎn)要概括如下
- Service:Service和Activity的差別在于,Activity的生命周期是由用戶交互決定的蒿赢,而Service的生命周期是我們通過代碼主動(dòng)調(diào)用的润樱,且Service實(shí)例和manifest中注冊(cè)的是一一對(duì)應(yīng)的。實(shí)現(xiàn)Service插件化的思路是通過在manifest中預(yù)埋StubService羡棵,hook系統(tǒng)startService等調(diào)用替換啟動(dòng)的Service壹若,之后在StubService中創(chuàng)建插件Service,并手動(dòng)管理其生命周期皂冰。
- BroadCastReceiver:解析插件的manifest店展,將靜態(tài)注冊(cè)的廣播轉(zhuǎn)為動(dòng)態(tài)注冊(cè)。
- ContentProvider:類似于Service的方式秃流,對(duì)插件ContentProvider的所有調(diào)用都會(huì)通過一個(gè)在manifest中占坑的ContentProvider分發(fā)赂蕴。
四、發(fā)展方向
通過對(duì)插件化技術(shù)的學(xué)習(xí)舶胀,可以看出目前插件化技術(shù)的兩個(gè)發(fā)展方向
結(jié)合組件化技術(shù)睡腿,成為一個(gè)中大型app的基礎(chǔ)框架
以Small和阿里的Atlas為代表,利用了插件化技術(shù)對(duì)復(fù)雜工程的模塊進(jìn)行解耦峻贮,將app分成主工程和多個(gè)插件模塊。主工程在運(yùn)行期間動(dòng)態(tài)加載相應(yīng)模塊的插件運(yùn)行应闯,并負(fù)責(zé)插件模塊的管理工作纤控。各個(gè)插件可以獨(dú)立開發(fā)和運(yùn)行,也可以依賴主工程或者其他插件碉纺。下面是基于Atlas的手淘app的框架圖
其中的獨(dú)立bundle即是一個(gè)插件船万,手淘中的首頁(yè)刻撒,詳情頁(yè),掃碼耿导,支付等都做成了單獨(dú)的bundle声怔,并且首頁(yè)bundle還可以依賴于定位bundle。而主工程中則包含了各種基礎(chǔ)功能庫(kù)供各個(gè)bundle調(diào)用舱呻,并且包含了對(duì)bundle的安裝醋火,運(yùn)行,版本管理箱吕,安全校驗(yàn)等運(yùn)行期的管理工作芥驳。
組件化技術(shù)是利用gradle腳本實(shí)現(xiàn)的編譯期的功能解耦,而Atlas是利用插件化技術(shù)實(shí)現(xiàn)了一套運(yùn)行期的功能解耦茬高,所以其也號(hào)稱是動(dòng)態(tài)組件化技術(shù)兆旬。
app沙盒系統(tǒng),完全模擬app的運(yùn)行環(huán)境
以VirtualAPP為代表怎栽,在應(yīng)用層構(gòu)建了一個(gè)虛擬的app運(yùn)行環(huán)境丽猬,實(shí)現(xiàn)了免安裝運(yùn)行apk,應(yīng)用雙開等黑科技熏瞄。另外作為應(yīng)用開發(fā)者也需要注意我們的應(yīng)用可能會(huì)運(yùn)行在一個(gè)虛擬的環(huán)境下脚祟,對(duì)于支付,登錄等功能要特別注意其安全性巴刻。
最后用VirtualAPP的作者Lody的一句話結(jié)束本篇文章愚铡,相信插件化技術(shù)還會(huì)繼續(xù)發(fā)展壯大下去。
“插件化技術(shù)的成熟程度雖然在最近幾年呈上升趨勢(shì)胡陪,但是總體而言仍然處于初沥寥、中級(jí)階段。
App沙盒技術(shù)的出現(xiàn)就是插件化發(fā)展的創(chuàng)新和第一階段的產(chǎn)物柠座。在未來(lái)邑雅,我相信很多插件化技
術(shù)會(huì)被更多的應(yīng)用,如果插件化穩(wěn)定到了一定的程度妈经,甚至可以顛覆App開發(fā)的方式淮野。”
參考
2.Android apk動(dòng)態(tài)加載機(jī)制的研究