本文涉及的PPT地址:
http://slides . vimerzhao . top/006-Android-plugin-tech-share . html
一個
前言
今天想和大家分享的是Android的插件技術(shù),這是一門比較復(fù)雜的知識,在Android中歷史悠久,內(nèi)容也比較復(fù)雜,今天一個小時是完成不了的,所以今天也選擇性的分享一些我認(rèn)為比較重要的內(nèi)容。
今天,我們將大概討論五個部分,即:
插件化的前置知識插件化的發(fā)展歷史Activity的framework源碼與插件化實現(xiàn)各大插件化方案的借鑒與革新對于插件化的幾點思考2
插件的預(yù)先知識
這部分每一個基礎(chǔ)知識都可以單獨拿出來講一個小時,我只簡單提一下,不做深入講解。通常大家都有所了解,足以應(yīng)付以下內(nèi)容。
什么是插件??
依靠安裝,動態(tài)加載Native函數(shù)。
為什么要插(為什么)??
動態(tài)發(fā)布,包大小減少,邏輯解耦,編譯速度提高?
方向問題:這些好處背后的挑戰(zhàn)是什么??
客戶穩(wěn)定性,開發(fā)經(jīng)驗。
插件什么時候(什么時候)出現(xiàn)??
2012年。
插件流行在哪里??
國內(nèi),手淘,攜程,滴滴等大型應(yīng)用
誰在做外掛(誰)??
早期的個人開發(fā)者,后期的大公司。
插件怎么做(怎么做)??
一般來說,插件可以分為Hook學(xué)派和Static Agent學(xué)派,Hook學(xué)派會涉及到更多的基礎(chǔ)知識。
Apk包裝流程(Gradle,aapt)
有些方案會在編譯器中修改插件的字節(jié)碼,有些方案會在編譯器中重新排列資源,所以這部分要理解。
Apk安裝過程(PMS、清單解析、dexopt)
DroidPlugin和VirutalApp之類的方案模擬了在系統(tǒng)中安裝apk的過程,所以這部分需要理解。
四個組成部分的管理(AMS,主要是啟動過程和生命周期)
插件的核心是四個組件的插件,所以這部分一定要理解。
資源加載機(jī)制與圖書館
同上。
儀表板組合儀表/活頁夾
Hook發(fā)送插件來處理system_process流程,而比較成熟的方案會把插件當(dāng)做一個單獨的流程,所以這部分也要理解。
類別加載機(jī)制
加載插件的重要一步是加載插件類,這是任何方案都無法避免的。
反思與動態(tài)主體
Hook發(fā)送插件必須使用的Java技術(shù)。
三
插件的發(fā)展歷史
在下表中,我梳理了插件技術(shù)的各種開源解決方案的出現(xiàn)時間,主要是參考了之前的文章和自己整理的相關(guān)新聞和代碼提交記錄(這部分有點研究史料的感覺)。解決方案的出現(xiàn)不是一蹴而就的,發(fā)布時間和開發(fā)開始時間之間必須有一個跨度。過于注重時間的準(zhǔn)確性意義不大,但是輸出時間語境也可以幫助我們獲得一些信息。
另外之前網(wǎng)上也有一些版本,但是比較老,有一些疏漏。我相信最完美和最新的表格是我編制的。
時間方案主體2012年7月mmin18/AndroidDynamicLoader[1]大眾點評/屠毅敏2013年23Code-2014年初alibaba/atlas[2]阿里/伯奎2014年7月houkx/android-pluginmgr[3]houkx2014年底singwhatiwanna/dynamic-load-apk[4]百度/任玉剛2014年11月Direct-Load-apk[5]羅迪2015年4月HiWong/OpenAtlas[6]BunnyBlue2015年8月DroidPluginTeam/DroidPlugin[7]360/張勇2015年底CtripMobile/DynamicAPK[8]-2015年底wequick/Small[9]林廣亮2016年7月asLody/VirtualApp[10]羅迪2017年6月didi/VirtualAPK[11]-2017年7月Qihoo360/RePlugin[12]-2018年10月ManbangGroup/Phantom[13]-2019年6月Tencent/Shadow[14]-粗略看一下這張表,我個人有以下發(fā)現(xiàn),歡迎補(bǔ)充:
早期野蠻成長,主要以個人名義開源;后來成熟了,主要是以公司的名義開源。前期注重方案的實現(xiàn),后期注重方案的可用性(比如RePlugin口號中提到的“靈活、穩(wěn)定、易用”)。
16年是一個分水嶺,后續(xù)所有方案都是優(yōu)化的(具體優(yōu)化后面會講到),沒有突破性的創(chuàng)新。16年前的描述側(cè)重于框架的特性,并沒有統(tǒng)一使用Plugin Framework這個詞。16年后,插件這個詞深入人心,專注于描述方案的完備性、穩(wěn)定性、靈活性等實際需求。
估計大家下載的時候都有點暈。我們所知道的插件原理其實只是幾個軸。怎么才能消化這么多已經(jīng)出現(xiàn)的方案?
所以我才把內(nèi)容安排定下來。我們先來看看插件的核心能力:如何在框架層處理Activity,然后逐一分析每個方案的借鑒和創(chuàng)新,而不是孤立地看每個方案。我相信這樣會容易很多。
四
框架源代碼和活動的插件實現(xiàn)
為什么要從原則到實施方案重點分析活動?因為它是最重要、最常用、最復(fù)雜的組件,也是插件解決方案的核心,最簡單的app只能是Activity。
開始進(jìn)程
活動的激活過程是我們面試必問的問題,相信大家都清楚。這里我做了一個單步調(diào)試視頻,幫助大家回憶。綜上所述,Activity的激活過程可以用下圖來概括:
圖片來自:https://juejin.im/post/5c7f3471f265da2db5425c4b
活動管理
關(guān)于Activity,我覺得還有一點可以單獨討論,就是它的棧管理機(jī)制。相對完整的插件解決方案無法避免這個問題。例如,在DroidPlugin中有這樣的代碼:
......
& lt活動
Android:name = " . stub . activitysub $ P00 $ standard 00 "
Android:allowTaskReparenting = " true "
Android:excludefromrensets = " true "
android:exported= "false "
Android:hardwareAccelerated = " true "
Android:label = " @ string/stub _ name _ activity "
安卓:launchMode= "標(biāo)準(zhǔn)"
安卓:noHistory= "true "
安卓:主題= "@style/DroidPluginTheme " >;
& lt意圖過濾器>。
& ltaction Android:name = " Android . intent . action . MAIN "/& gt;
& ltcategoryandroid:name = " com . mor goo . droid plugin . category . PROXY _ STUB "/& gt;
& lt/ intent-filter>。
& lt元數(shù)據(jù)
Android:name = " com . mor goo . droid plugin . ACTIVITY _ STUB _ INDEX "
android:value= "0"/>
& lt/ activity>。
& lt活動
Android:name = " . stub . activitysub $ P00 $ SingleInstance 00 "
Android:allowTaskReparenting = " true "
Android:excludefromrensets = " true "
android:exported= "false "
Android:hardwareAccelerated = " true "
Android:label = " @ string/stub _ name _ activity "
Android:launch mode = " SingleInstance "
安卓:主題= "@style/DroidPluginTheme " >;
& lt意圖過濾器>。
& ltaction Android:name = " Android . intent . action . MAIN "/& gt;
& ltcategoryandroid:name = " com . mor goo . droid plugin . category . PROXY _ STUB "/& gt;
& lt/ intent-filter>。
& lt元數(shù)據(jù)
Android:name = " com . mor goo . droid plugin . ACTIVITY _ STUB _ INDEX "
android:value= "0"/>
& lt/ activity>。
......
VirtualAPK和RePlugin里也有類似的。
可見每個計劃都有兼容各種推出的野心~
讓我們看看框架層是如何實現(xiàn)的。借用gityuan和盛達(dá)的兩幅圖,我們可以清楚地看到它的設(shè)計:
具體源代碼在StartActivityChecked [15]中,非常復(fù)雜,這里就不多做分析了,宏觀的指示大家都可以知道。
看了一些源代碼,發(fā)現(xiàn)VirtualApp的實現(xiàn)是最完善的。這里不想具體分析。簡單來說,它也像AMS一樣記錄活動棧的變化(相當(dāng)于在自己的進(jìn)程中拍攝AMS數(shù)據(jù)的快照,不知道能不能直接獲取AMS數(shù)據(jù),應(yīng)該沒有這樣的接口~)。根據(jù)launchMode/Flag,觸發(fā)onNewIntent等回調(diào),可以說是所有方案中最高的,其他方案只能支持相對簡單的launchMode。
五
主要插件方案的借鑒與創(chuàng)新
在開始以下工作之前,有必要區(qū)分本質(zhì)問題和衍生問題:
本質(zhì)問題:如何啟動插件Activity,加載資源等,實現(xiàn)生命周期回調(diào)等。
衍生問題:插件管理、安裝、加載、頁面路由等。
mmin18/AndroidDynamicLoader
插件通過分片的方式實現(xiàn),資源插件通過反射addAssetPath實現(xiàn)
staticymyresources GetResource(MyClassLoader MCL){
......
嘗試{
asset manager am =(asset manager)asset manager。class.newInstance
am . getclass . getmethod(" addAssetPath ",String。類)。invoke(am,path . GetaBSolutePath);
resources super RES = my application . instance . GetReSources;
Resources res = newResources(am,superRes.getDisplayMetrics,SuperRes . GetConfiguration);
......
}
阿里巴巴/地圖集
Hook for框架開始出現(xiàn),對后面的方案影響深遠(yuǎn)。
下圖是博魁當(dāng)時的共享開源版本是17年才公布的,共享時的設(shè)計已經(jīng)大不相同了
houkx/android-pluginmgr
這個方案很有趣,但很少被提及,首先Hook掉ClassLoader,通過DexMaker在客戶端修改插件類(變成一個已經(jīng)在Manifest注冊過的類)但是客戶端做這個事情效率很低,事實上后來的RePlugin、Shadow又走回了這條路,只是放在了編譯期singwhatwanna/動載荷-apk
用靜態(tài)代理實現(xiàn),第一個完成度比較高的插件化方案 但是包括that關(guān)鍵字在內(nèi)的設(shè)計,對開發(fā)侵入性較大直接裝載裝甲運兵車
lody早期的項目,針對上兩種方案的優(yōu)化(開發(fā)體驗) 試圖通過把StubActivity的數(shù)據(jù)給插件Activity,讓插件Activity能像正常Activit一樣,參見下面的核心代碼:publicationdispatchproxitoplugin {
嘗試{
//開始將插件偽裝成實體活動
pluginRef。set( "mBase ",代理);
pluginRef。set( "mDecor ",proxyRef。get(" MDecor "));
pluginRef。set( "mTitleColor ",proxyRef。get(" MTitleColor ");
......
儀器儀表= proxyRef。get(" mInstrumentation ");
pluginRef。set( "mInstrumentation ",new pluginsrument(instrumentation));
......
pluginRef。設(shè)置(“多線程”,proxyRef。get(" MuiThread ");
pluginRef。set( "mHandler ",proxyRef。get(" mHandler "));
pluginRef。set( "mInstanceTracker ",proxyRef。get(" mInstanceTracker ");
......
} catch(ReflectException e) {
e.printStackTrace
}
}
HiWong/OpenAtlas
作者通過逆向手淘代碼,實現(xiàn)的“山寨版”atlas,由于一些原因刪庫了,后來變成了ACDD,算是OpenAtlas的優(yōu)化版 公開了一個hook編譯工具aapt來解決資源沖突的實現(xiàn),對后面的方案影響深遠(yuǎn) 比較遺憾的是插件必須注冊在Manifest中,這和插件化的精神有所背離,所以對外宣稱是容器化,即容器內(nèi)的Bundle可以升級更新?繞過Activity啟動檢查的方案初具雛形,開始Hook一些我們耳熟能詳?shù)姆椒ǎ瑓⒖己诵拇a:/ ****
*
*公共活動結(jié)果執(zhí)行開始活動(上下文誰,IBinder
*上下文線程、內(nèi)部令牌、活動目標(biāo)、意圖意圖、內(nèi)部
* RequestCode);
* ***/
公共工具掛鉤(工具,上下文上下文)
this.context = context
this.mBase =儀器儀表;
嘗試{
minstrumentioninvoke = Hack . into(" Android . app . instrumentation ");
如果(構(gòu)建。VERSION.SDK_INT >構(gòu)建。版本_代碼。冰淇淋_三明治_MR1) {
mexecstatactivity = minstrumentioninvoke . method(" ExecstartActivity ",Context。類,
IBinder。類,IBinder。班級,活動。類,意圖。類,int。類,捆綁。類);
} else{
mexecstatactivity = minstrumentioninvoke . method(" ExecstartActivity ",Context。類,
IBinder。類,IBinder。班級,活動。類,意圖。類,int。類);
}
......
} catch(HackassertionException e){
e.printStackTrace
} catch(IllegalargumentException e){
e.printStackTrace
}
}
來源:https://Alibaba . github . io/atlas/principal-intro/Runtime _ principal . html
DROIdplugintam/DROIdplugin
第一個Hook流派的完成度非常高的版本,幾乎Hook了AMS、PMS等相關(guān)服務(wù)可以真正地加載一個完全獨立的第三方apk文件從這里開始,插件化具備了成為沙盒/虛擬機(jī)的可能,而不是宿主的能力補(bǔ)充在呼叫AMS之前:
protected booleandlerplaceintentforstartactivityabhigh(Object[]args)throws remote exception {
IntintentoFargindex = FindFirstentinDexiargs(args);
if(args!= null & amp& ampargs.length >。1 & amp& ampintentOfArgIndex & gt。= 0) {
意圖意圖=(意圖)參數(shù)[IntentToFargindex];
//XXX String calling package =(String)args[1];
if(!pluginpatchmanager . GetInstance . CanstartPluginactive(intent)){
pluginpatchmanager . GetInstance . StartPluginactive(意圖);
returnfalse
}
activity info activity info = resolveaactivity(intent);
if(activityInfo!= null & amp& ampisPackagePlugin(activityinfo . package name)){
component name component = SelectProxyActivity(意圖);
if(組件!= null) {
意圖新意圖=新意圖;
嘗試{
class loader plugin lass loader = plugin processmanager . getpluginlassloader(component . getpackagename);
setIntentClassLoader(newIntent,plugin lass loader);
}捕獲(例外e) {
Log.w(TAG,“將類加載器設(shè)置為新意圖失敗”,e);
}
newIntent.setComponent(組件);
newIntent.putExtra(Env。EXTRA_TARGET_INTENT,INTENT);
new ENDENT . SetFlags(ENDENT . GetFlags);
字符串callingPackage =(字符串)參數(shù)[1];
if(textutils . equals(mhostcontext . getpackagename,callingPackage)) {
新意圖。添加標(biāo)志(意圖。FLAG _ ACTIVITY _ NEW _ TASK);
args[IntentToFargindex]= NewIntent;
args[1]= mhostcontext . GetPackageName;
} else{
Log.w(TAG,“startActivity,replace selectProxyActivity fail”);
}
}
}
returntrue
}
AMS回頭了
@覆蓋
publicboolean handleMessage(消息消息){
long b = System.currentTimeMillis
嘗試{
if(!mEnable) {
returnfalse
}
if(PlugInProcessManager . IsprugInProcess(MHostContext)){
if(!plugin manager . GetInstance . Isconnected){
Log.i(TAG,“handleMessage not is connected post and wait,msg=%s”,msg);
MoldHandle . SendMessageDelated(message . get(msg),5);
returntrue
}
}
if(msg.what == LAUNCH_ACTIVITY) {
returnHandleLaunchActivity(msg);
}
if(mCallback!= null) {
returnmcallback . handlemessage(msg);
} else{
returnfalse
}
}最后{
Log.i(TAG," handleMessage(%s,%s) cost %s ms ",msg.what,codeToString(msg.what),(System . CurrentiMemillis-b));
}
}
CtripMobile/DynamicAPK
主要借鑒了OpenAtlasaapt部分有所創(chuàng)新我們很快/很小
提供了一種通過腳本的方式解決資源沖突 Activity依然是通過Hook的方式,沒有特別大的改動asLody/VirtualApp
是對DroidPlugin的大升級,完全實現(xiàn)了虛擬框架,體現(xiàn)在源代碼命名和結(jié)構(gòu)上
didi/VirtualAPK
綜合考慮了大量Hook導(dǎo)致的不穩(wěn)定問題和靜態(tài)代理的開發(fā)侵入性,又開始回歸到僅Hook mInstrumentation 的設(shè)計,主要是execStartActivity 和 newActivity Service主要用兩個真實的Service做轉(zhuǎn)調(diào) ContentProvider通過一個代理Provider進(jìn)行操作的分發(fā)奇虎360/復(fù)制器
更進(jìn)一步,One Hook,結(jié)合編譯期的修改,把插件變成類似靜態(tài)代理的類(像是把houkx/android-pluginmgr的策略換了個地方),同時Hook了ClassLoader使之加載插件的類: 記錄下目標(biāo)頁 ActivityA,替換成已自動注冊在 AndroidManifest 中的坑位 ActivityNS。 在 ClassLoader 中攔截ActivityNS的創(chuàng)建,創(chuàng)建出ActivityA返回。 返回的ActivityA占用著 ActivityNS 這個坑位, 坑位由 Gradle 編譯時自動生成在AndroidManifest中。主要的焦點是穩(wěn)定性和兼容性,文檔也相對完善
問:你和360年前發(fā)行的DroidPlugin的主要區(qū)別是什么?
答:這個問題問得好。很多人都有這個疑問——“為什么要為360開發(fā)兩個不同的插件框架”?其實說到底,最根本的區(qū)別是——目標(biāo)不同:DroidPlugin主要解決的是獨立功能組裝在一起,不需要任何交互就可以快速釋放的問題。
目前市場上的一些雙開放應(yīng)用和DroidPlugin有一些共同點。當(dāng)然,要實現(xiàn)完整的雙開,還是需要大量的修改,比如Native Hook等等。RePlugin解決了每個功能模塊都可以獨立升級的問題,需要與主機(jī)和插件交互耦合。
另外,從技術(shù)層面來說,核心區(qū)別是一個:Hook點數(shù)。DroidPlugin可以使APK“直接在主程序中運行”,而無需任何額外的修改。但是,大量的API(包括AMS、PackageManager等。)都是Hook需要的,在改編上需要做很多工作。
RePlugin只掛接了ClassLoader,所以極其穩(wěn)定,也支持大部分單一產(chǎn)品的特性,但是需要插件做一點修改。幸運的是,作為一個插件開發(fā)者,并不需要太過關(guān)注,因為通過“動態(tài)編譯方案”,開發(fā)者可以達(dá)到“在主程序中運行,無需開發(fā)者修改Java代碼”的效果。可以肯定的是,DroidPlugin也是業(yè)內(nèi)公認(rèn)的優(yōu)秀的插件式免安裝解決方案。相信隨著時間的推移,RePlugin和DroidPlugin會在各自的領(lǐng)域(綜合插件&:應(yīng)用免費安裝)創(chuàng)造屬于自己的世界。被申請人:張
出發(fā)地:https://github.com/Qihoo360/RePlugin/wiki/FAQ
來自:https://github。com/奇虎360/replugin/pull/840看影的時候發(fā)現(xiàn)作者提出了一個改進(jìn),但是沒有被接受,這個沒完沒了的循環(huán)也沒有說明原因~個人感覺這個改變挺好的。
曼邦集團(tuán)/幻影
比360的RePlugin更進(jìn)一步,宣稱零Hook相關(guān)資料較少,暫未深入研究。除了一篇宣傳性質(zhì)的README,沒有什么實質(zhì)資料分析其代碼后發(fā)現(xiàn)還是拓展了RePlugin編譯時替換,靜態(tài)代理
騰訊/影子
同樣是零Hook,同時宣稱支持插件框架本身的動態(tài)化(這個其實也不難) 更像是RePlugin的另一個種實現(xiàn),沒有本質(zhì)的不同六
關(guān)于插件的幾點思考
之前我們快速閱讀了各個插件的主要特性,隱約意識到了。好像是《三國演義》里的那句“長合必分,長合必成”。很多方案看似新穎,但都是在前人的基礎(chǔ)上,根據(jù)自己的業(yè)務(wù)做一些創(chuàng)新。在里程碑上,仁者見仁,智者見智,但他們絕不能被花哨的項目README.md所迷惑
從上下文來看,從靜態(tài)agent和Hook框架的蓬勃發(fā)展,到最近幾年,兩種方案取長補(bǔ)短??偟膩碚f,他們似乎在朝著客戶端輕Hook+編譯時入侵修改的方向前進(jìn)。我來拆分幾個維度,詳細(xì)討論一下。
開發(fā)期或運營期
如果您想像正?;顒右粯訂游覀兊牟寮顒?,不可避免地要進(jìn)行一些侵入性的修改。在我看來,主要分為三個方向:
以dynamic-load-apk為代表的,開發(fā)期侵入 以DroidPlugin為代表的,運行期侵入 以RePlugin、shadow為代表的,編譯期侵入編譯時入侵是開發(fā)時入侵的自動化版本,既避免了不穩(wěn)定的Hook,又保證了開發(fā)人員的體驗;運行時入侵一直是追求穩(wěn)定性的挑戰(zhàn)。
獨立還是合并
類加載器和資源都存在此問題。有些方案會選擇通用的類加載器或資源,這樣會解決插件與主機(jī)、插件與插件之間的潛在沖突。aapt神奇的修正就是處理這種情況。
尤其是Resource,獨立可以避免沖突,共享可以減小插件大小。
我覺得有必要區(qū)分一下場景。DroidPlugin的每個插件都非常獨立,適合單獨加載;如果插件是主機(jī)功能的補(bǔ)充,可以共享。Shawdow甚至通過白名單控制是隔離還是共享ClassLoader。
動態(tài)或插件
插件和動態(tài)有共同點,可以減小主機(jī)大小,動態(tài)釋放等。目前插件的優(yōu)勢是在涉及四個組件時更適合,而動態(tài)則適合純視圖級的修改。
1.《virtualapp 插件化技術(shù)的演進(jìn)之路》援引自互聯(lián)網(wǎng),旨在傳遞更多網(wǎng)絡(luò)信息知識,僅代表作者本人觀點,與本網(wǎng)站無關(guān),侵刪請聯(lián)系頁腳下方聯(lián)系方式。
2.《virtualapp 插件化技術(shù)的演進(jìn)之路》僅供讀者參考,本網(wǎng)站未對該內(nèi)容進(jìn)行證實,對其原創(chuàng)性、真實性、完整性、及時性不作任何保證。
3.文章轉(zhuǎn)載時請保留本站內(nèi)容來源地址,http://f99ss.com/tiyu/1107979.html