淺談Android插件化

插件化秧饮,一個陌生有熟悉的名詞簇搅,從我們學(xué)習(xí)Android伊始浅缸,總能隱約聽到關(guān)于它的消息,從360的RePlugin膘盖,到DiDi的VirtualAPK更新?lián)Q代,再到Tencent Shadow橫空出世尤误,可以說插件化已經(jīng)從一個劍走偏鋒的黑科技侠畔,蛻變成了獨步天下的高級技能,其中蘊含的各種思想和變化损晤,也成了考核高級開發(fā)工程師無法避開的++障礙++软棺,今天我們就來簡單的說一下這些『高深』的技術(shù)。

1. 什么是插件化

插件化尤勋,通俗來說就是一種動態(tài)加載技術(shù)喘落。我們可以通過一個已安裝的Apk來加載本地的apk文件,通過這種動態(tài)的加載技術(shù)來實現(xiàn)應(yīng)用功能的拓展最冰、動態(tài)更新瘦棋、灰度發(fā)布、A/B Test等功能暖哨,本質(zhì)上宿主以及所有的插件都是Apk赌朋,只不過宿主可以把其他的插件Apk加載并且運行起來。

插件化

2. 插件化的原理

這里我們不去把四大組件的插件化都介紹一遍鹿蜀,而是把Activity的原理講明白箕慧,其實原理都是大同小異的,大家能理解一個其他的也不在話下茴恰。

2.1 Apk是什么颠焦?

Apk作為Android系統(tǒng)中的安裝文件,其本質(zhì)就是一個壓縮包往枣,你可以直接把它當(dāng)做一個zip包伐庭,里面有各種二進(jìn)制和資源文件,只是Android系統(tǒng)通過有規(guī)律的加載并運行這些文件分冈,并把這些在手機屏幕上顯示出來圾另。

在繼續(xù)往下看之前,我們要拋開原先的一些『概念』雕沉,插件化里面的Activity不是一個界面集乔,而是一個class文件,Service也不是服務(wù)坡椒,也是一個Class文件扰路,Receiver尤溜、Provider以及各種資源都是這樣,只有以文件的角度去看待汗唱,才能以系統(tǒng)的角度去思考宫莱。簡單的說Apk里的文件分為兩類 Class文件 和 資源文件。加載這兩種文件也需要不同的方式哩罪,Class文件用ClassLoader加載授霸,資源文件用AssetManager加載。

2.2 Activity啟動

我們明明說的是插件化际插,為什么先扯到了Activity的啟動上了呢碘耳?其實這里才是插件化里的最核心的地方,因為所有的插件化框架都是依托源碼才做出來的腹鹉,我們要做的相當(dāng)于一個『小系統(tǒng)』藏畅,如果我們都不能明白源碼時如何實現(xiàn)的,我們又怎么能在其基礎(chǔ)上構(gòu)建我們的框架呢功咒?所以我們在繼續(xù)往下之前愉阎,必須要把啟動的過程都縷清楚,這樣在面對眾多的插件化框架時才能有條不紊發(fā)去分析力奋。

2.2.1 入門

Activity的啟動流程可以簡單的理解為一個進(jìn)程間的通信過程榜旦,只不過一端是我們的App,另一端是Android系統(tǒng)景殷。試想我們現(xiàn)有的邏輯溅呢,如果想要打開一個界面,就是調(diào)用startActivity()方法猿挚,我們就要把這個Class交給系統(tǒng)咐旧,系統(tǒng)驗證通過,實例化這個界面并且交還給我們绩蜻,我們才可以使用铣墨。這就是一個進(jìn)程間通訊的過程,由于Android IPC使用Binder作為進(jìn)程間通訊的主要手段办绝,我們甚至可以直接把它看作是一個C/S的模型伊约,我們的App就是客戶端而系統(tǒng)是服務(wù)端,我們所做的也只是一個請求孕蝉,而系統(tǒng)也為我們做了大部分的事情屡律,比如Class的加載、實例化降淮、Activity生命周期的控制超埋、權(quán)限的管理等等。


簡易流程
2.2.1 進(jìn)階

上述的流程只是讓大家有一個大致的印象,想要了解更多就的去分析源碼了霍殴。

Activity啟動流程

上圖是基于Android 7.1 Activity啟動流程的整理窍蓝,要用文字的方式去講清楚這么一件事,其實并不容易繁成,何況是這么一件挺麻煩的事,這幅圖也只是一個參考淑玫,方便我們的講解巾腕,請大家一定要對照圖和下面的方法引用圖去看一下源碼,只有先把源碼看明白絮蒿,才能對插件化有一個自己的認(rèn)識尊搬。

對于Activity的啟動來說,其實每個版本的差別并不大土涝,但是低版本的源碼封裝的沒那么復(fù)雜佛寿,更便于我們閱讀。


Activity啟動方法調(diào)用圖
2.2.3 思考

在看完源碼之后但壮,就要開始真正的思考了冀泻,如果讓你去實現(xiàn)一個插件化的框架你要從哪里開始呢?

自然是先打一個插件Apk的包蜡饵,試著去打開并加載弹渔。那么,用什么去打開溯祸?用什么去加載呢肢专?

ClassLoader

其實在Android中,Activity也是通過ClassLoader來實例化的焦辅,只不過和我們認(rèn)為的Java中不太一樣博杖。Android里并沒有完全使用Java的加載模型但是借鑒了相似的思路。在Android中ClassLoader分為3種筷登,每一種ClassLoader分別加載不同的文件剃根。

  1. BootClassLoader:為系統(tǒng)預(yù)加載使用
  2. PathClassLoader:給程序、系統(tǒng)程序仆抵、應(yīng)用程序 加載class
  3. DexClassLoader:加載apk跟继、zip 文件

一般而言,Boot是系統(tǒng)用的镣丑,Path是App用的舔糖,Dex是用戶用的。有了DexClassLoader莺匠,我們就可以把Apk加載到內(nèi)存中使用了金吗。但是這種方法過于簡單粗暴,并且在實例化之后丟失了Context的環(huán)境,而丟失上下文的后果就是摇庙,我們所熟知的大部分方法都無法使用了旱物,比如:findViewById()、startActivity()卫袒、startService()等等宵呛,要解決這個問題,我們就得從ClassLoader的底層加載去入手了夕凝。

我們看Activity啟動流程圖中的方法9和10宝穗,可以看出在performLaunchActivity中獲取ClassLoader,Class的其實是在Instrumentation中通過newInstance()創(chuàng)建的码秉,這里我們具體了解一下這一段的調(diào)用流程逮矛。

我們從ActivityThread.performLaunchActivity() 方法出發(fā),找到ClassLoader转砖,逐步向上找去须鼎,發(fā)現(xiàn)其實這個ClassLoader其實就是PathClassLoader。

在PathClassLoader中府蔗,真正的加載其實都是通過BaseDexClassLoader來進(jìn)行的晋控,而BaseDexClassLoader中有一個pathList字段,這個變量相等于一個Dex數(shù)組礁竞,各種Dex的信息都在里面糖荒,而DexClassLoader的父類也是BaseDexClassLoader,這里就是一個完美的Hook點模捂,既然可以獲取相同的Dex數(shù)據(jù)捶朵,那也能把類似的數(shù)據(jù)拼接到一起。


ClassLoader UML圖

分析PathClassLoader的加載流程狂男,我們通過Hook综看,從而把插件的Class直接『掛載』到上面,從而讓插件里的Class無縫接入到主App中岖食。


Dex加載流程
資源文件的加載

AssetManager本就是個十分強大的資源管理器红碑,只是有些功能沒有對我們開放,我們需要做的只是通過一些方法(反射)把Apk里的資源加進(jìn)去就好了泡垃。
在AssetManager的源碼中析珊,mStringBlocks就是用來保存資源文件的變量,我們通過addAssetPath()方法把插件的路徑加載進(jìn)來蔑穴,之后反射調(diào)用ensureStringBlocks()確保文件都加載進(jìn)來忠寻,最后構(gòu)造一個Resources在工程中使用即可。

    // 執(zhí)行此 public final int addAssetPath(String path) 方法存和,能把插件的路徑添加進(jìn)去
    Method method = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
    method.setAccessible(true);
    method.invoke(assetManager, file.getAbsolutePath());

    // 實例化 ensureStringBlocks()
    Method ensureStringBlocksMethod = assetManager.getClass().getDeclaredMethod("ensureStringBlocks");
    ensureStringBlocksMethod.setAccessible(true);
    // 執(zhí)行了ensureStringBlocks  初始化 string.xml  color.xml  anim.xml 等文件
    ensureStringBlocksMethod.invoke(assetManager); 

    // 加載插件資源
    Resources r = getResources(); // 拿到宿主的配置信息
    resources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration());
啟動

現(xiàn)在Class和Recourse都已經(jīng)準(zhǔn)備好了奕剃,可以試著去啟動了衷旅。

但是當(dāng)你啟動了之后你會遇到一個不可回避的問題,這就是我們可能時常忘記的Manifest聲明錯誤纵朋。原因很顯然柿顶,我們加載的是插件中的Activity,這個類甚至都不在系統(tǒng)里操软,更不要說在Manifest里面聲明了嘁锯。


ActivityNotFoundException

如果沒有源碼的話,我們的插件化之路可能已經(jīng)要以失敗告終了聂薪,好在Android是開源的猪钮,我們可以去研究為什么會報這個錯誤,或許可以解決這個限制胆建。

你可以直接使用打印的錯誤在源碼里面搜索,最后會在Instrumentation的 checkStartActivityResult方法中找到這段話肘交,如果我們向上尋找的話就會發(fā)現(xiàn)笆载,這個方法的調(diào)用者就是execStartActivity()。

ActivityNotFoundException

也許你會想到一個方案涯呻,就是事先在Manifest中聲明好呀凉驻,這樣不就很容易的解決了?我想在插件化發(fā)展中一定有這樣的過程复罐,這是一個簡單且行之有效的方法涝登,但是我們所熟知的框架里面卻沒有一個這樣實現(xiàn)的,因為這是不實用的效诅。試想胀滚,如果要用個插件化,這個App必然是有眾多邏輯與界面乱投,有多個業(yè)務(wù)的航母級應(yīng)用咽笼,開發(fā)人員幾十上百個,如果要這樣去開發(fā)就違背了我們插件化的初衷戚炫,在開發(fā)難度上也并沒有降低剑刑。

于是,大家就開始思考解決方案双肤,最先出現(xiàn)的就是——占位式插件化施掏。

預(yù)先在Manifest中聲明一個ProxyActivity,所有的界面都以這個Activity作為跳板啟動(把目標(biāo)Activity的class路徑放在extra里面)茅糜,在打開ProxyActivity之后再通過ClassLoader加載并實例化目標(biāo)Activity七芭,這個Activity就啟動成功了。

至此限匣,我們的插件化框架就完成了抖苦,這個思路可以讓這個『原始』的框架在Android9.0上運行毁菱,這是其他很多利用反射框架不可企及的,但是慢慢的你也會發(fā)現(xiàn)它的很多缺點锌历,就是上文說提及的『侵入性』太強贮庞。因為這個Activity是完全不受系統(tǒng)管理的(這個Activity是我們自己實例化的),我們需要在ProxyActivity中接管Activity的生命周期究西,我們無法去管理Activity的啟動棧了窗慎,我們甚至無法使用Context了。

顯然卤材,開發(fā)者和使用者對這樣的實現(xiàn)方式并不滿意遮斥,隨后便有了更加便于開發(fā)的Hook式。

我們知道ActivityNotFoundException這個錯誤是在哪里拋出來的扇丛,也知道原本的代碼术吗,那么我們?yōu)槭裁床蝗ダ@過它?或者讓系統(tǒng)不去執(zhí)行這個方法呢帆精?

Hook式的插件化實現(xiàn)起來稍顯復(fù)雜较屿,說復(fù)雜也只是因為要看的源碼有點多。
這里我們不去深究代碼實現(xiàn)卓练,大家講起來都差不多隘蝎,我們的目標(biāo)是把思路搞明白,如果想深入研究的話可以去看下深入理解Android插件化技術(shù)

插件化Activity啟動流程

可以看到襟企,Hook的方式也是需要ProxyActivity的嘱么,只不過使用的地方不一樣,我們原來是直接啟動ProxyActivity顽悼,但是現(xiàn)在這部分工作被Hook做了曼振,既然有替換的過程,必然也有還原的地方蔚龙,這個點就是在newActivity的Handler中拴测。通過這個『神不知鬼不覺?』的過程,我們用另一種方式實現(xiàn)了插件化府蛇。

但是這樣的方式也是有不足的地方的集索,Hook本就是『不安全』的,源碼中更改了一個字段汇跨,刪除了某個方法务荆,都會造成不可預(yù)料的后果,事實也確實是這樣穷遂,每個版本的啟動過程都會有更改函匕,而我們只能提前去適配新版本,避免出現(xiàn)問題蚪黑。

3. 總結(jié)

這里只是說明了插件化的基本原理盅惜,其實完整的插件化框架還有很多東西中剩,四大組件、Activity棧抒寂、插件中的組件相互啟動结啼、各個版本適配……如果你有興趣不如去自己試一試,相信對你的成長有很大幫助屈芜。

一個好的插件化框架是需求足夠且充足的前置知識郊愧,比如ClassLoader的加載,Hook井佑、動態(tài)代理属铁、Activity的啟動流程等等。如果大家想學(xué)習(xí)FrameWork這是一個很好的切入點躬翁,因為大多數(shù)Hook的代碼焦蘑,都得你去閱讀源碼之后才能下手,而這無論是對于四大組件的啟動流程盒发,還是個版本之間的差異喇肋,你都需要把這些做到了如指掌。

原始的插件化還是需要借助各種反射的邏輯迹辐,尋找我們可能去著手的Hook點去做,但是隨著Google從9.0開始對于『危險代碼』的緊縮和Android版本之間的兼容性問題甚侣,Hook的方式也慢慢顯露出各種問題明吩,而騰訊基于無反射的實現(xiàn),相信是未來插件化的發(fā)展方向殷费。

源碼地址:https://github.com/devilsen/PluginTest

參考 深入理解Android插件化技術(shù)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末印荔,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子详羡,更是在濱河造成了極大的恐慌仍律,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件实柠,死亡現(xiàn)場離奇詭異水泉,居然都是意外死亡,警方通過查閱死者的電腦和手機窒盐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門草则,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蟹漓,你說我怎么就攤上這事炕横。” “怎么了葡粒?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵份殿,是天一觀的道長膜钓。 經(jīng)常有香客問我,道長卿嘲,這世上最難降的妖魔是什么颂斜? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮腔寡,結(jié)果婚禮上焚鲜,老公的妹妹穿的比我還像新娘。我一直安慰自己放前,他們只是感情好忿磅,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著凭语,像睡著了一般葱她。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上似扔,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天吨些,我揣著相機與錄音,去河邊找鬼炒辉。 笑死豪墅,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的黔寇。 我是一名探鬼主播偶器,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼缝裤!你這毒婦竟也來了屏轰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤憋飞,失蹤者是張志新(化名)和其女友劉穎霎苗,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體榛做,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡唁盏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了检眯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片升敲。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖轰传,靈堂內(nèi)的尸體忽然破棺而出驴党,到底是詐尸還是另有隱情,我是刑警寧澤获茬,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布港庄,位于F島的核電站倔既,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏鹏氧。R本人自食惡果不足惜渤涌,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望把还。 院中可真熱鬧实蓬,春花似錦、人聲如沸吊履。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽艇炎。三九已至酌伊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間缀踪,已是汗流浹背居砖。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留驴娃,地道東北人奏候。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像唇敞,于是被迫代替她去往敵國和親蔗草。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容