插件化的誕生是為了解決什么問(wèn)題?
我們不妨好好思考一下,作為客戶端開(kāi)發(fā),平時(shí)工作中是否為這樣的情況發(fā)愁:
所以說(shuō),插件化設(shè)計(jì)之初就是為了不安裝新Apk,從而完成應(yīng)用的更新迭代。
我之前所在的團(tuán)隊(duì)也做了插件化,主要的原因還是包體積的訴求,原因有兩個(gè):
轉(zhuǎn)換率
上圖是2018年谷歌IO披露的包體積與下載轉(zhuǎn)化率之間的關(guān)系,時(shí)至今日,即使我們的網(wǎng)絡(luò)狀況已經(jīng)有了很好的提升,但是優(yōu)化包體積仍然是我們的目標(biāo),比如說(shuō):
所以我們可以看到,pdd這一方面做的很出色,僅有25m。
講插件化之前,我們先科普一下其中的概念。
對(duì)于一個(gè)完整功能的App,我們可以將其劃分成為很多模塊,每個(gè)模塊都可以將其劃分成為一個(gè)Apk。然后將基礎(chǔ)功能的Apk提交給應(yīng)用市場(chǎng)上架,后續(xù)我們可以通過(guò)基礎(chǔ)的Apk,下載其他模塊的Apk,從而完成功能的擴(kuò)展。
基礎(chǔ)分包
在整個(gè)過(guò)程中,我們稱提交給應(yīng)用市場(chǎng)的Apk為宿主,其他模塊的Apk稱之為插件。
相信沒(méi)接觸過(guò)插件化的同學(xué)可能會(huì)有一些疑問(wèn),我們平時(shí)打包的時(shí)候不是都是一個(gè)完整的Apk,為什么可以加載一個(gè)單獨(dú)的Apk?好了,這就是插件化的第一個(gè)難點(diǎn)。
作為一個(gè)Android開(kāi)發(fā),我們都知道Android里面的Davilk和Art虛擬機(jī)和Java虛擬機(jī)不是同一套,所以他們也有著不同的類加載結(jié)構(gòu)。
我們先回顧一下。
Jvm中加載的文件是class文件,再由Jvm翻譯成特定平臺(tái)的機(jī)器碼。使用的類加載器如下:
整個(gè)加載架構(gòu)如下:
Java類加載器結(jié)構(gòu)
雙親委派機(jī)制保證了收到類加載請(qǐng)求的時(shí)候,優(yōu)先讓父類加載器去加載,父類加載器處理不了的時(shí)候,才會(huì)自己去加載,保證了類加載機(jī)制的穩(wěn)定性。
我們上面提過(guò),由于CPU和功耗環(huán)境不一致,Android虛擬機(jī)和Jvm有著很大的不同,Android里面的虛擬機(jī)在4.4.4以后,就是ART虛擬機(jī)了。早期的時(shí)候,安裝的時(shí)候,會(huì)將dex文件直接編譯成.oat這樣的機(jī)器碼,不過(guò)這樣會(huì)有其他問(wèn)題:
于是,在 Android 7.0 以后,第一次啟動(dòng)的時(shí)候,使用 Jit,針對(duì)dex,邊解釋邊執(zhí)行,然后在空閑的時(shí)候,將剩余的 dex 文件編譯成機(jī)器碼。
所以,我們可以注意到,每次應(yīng)用升級(jí)的一段時(shí)間內(nèi),我們的啟動(dòng)時(shí)長(zhǎng)會(huì)出現(xiàn)波動(dòng),過(guò)了幾天以后,又會(huì)達(dá)到穩(wěn)定的狀態(tài)。因此,很多大廠,會(huì)針對(duì)這個(gè)過(guò)程優(yōu)化,如:
我們?cè)賮?lái)看一下類加載機(jī)制,Android里面類加載的單位是dex,類加載器包括:
整個(gè)結(jié)構(gòu)是這樣的:
Android類加載器
如果是指定路徑下的Apk或者jar包,我們需要將 PathClassLoader 替換成 DexClassLoader。到這里,第一個(gè)問(wèn)題的解決思路就很清晰了,我們可以通過(guò) DexClassLoader 加載插件Apk。
DexClassLoader的原理主要是通過(guò)DexPathList管理DexFile列表信息,從而加載到具體的類。
DexClassLoader
基于DexClassLoader,通常有兩種方案:
單個(gè)DexClassLoader指的我們可能有多個(gè)插件Dex,多個(gè)插件Dex使用同一個(gè)DexClassLoader,如圖:
將所有的插件中的類都由統(tǒng)一的DexClassLoader加載。
多個(gè)DexClassLoader指的是對(duì)于多個(gè)插件Dex,每一個(gè)Dex都會(huì)有自己的DexClassLoader,如圖:
多ClassLoader結(jié)構(gòu)
由各自DexClassLoader負(fù)責(zé)相關(guān)的插件的類加載。
看一下各自的優(yōu)缺點(diǎn):
分類 | 優(yōu)點(diǎn) | 缺點(diǎn) |
單DexClassLoader | 類之間不隔離,可以互相調(diào)用 | 需要處理一些適配問(wèn)題,比如不同插件加載了同一庫(kù)的不同版本,可能引發(fā)兼容性問(wèn)題 |
多DexClassLoader | 安全、穩(wěn)定 | 類之間隔離,需要處理互相調(diào)用的問(wèn)題 |
對(duì)于我們安卓系統(tǒng)來(lái)講,僅僅能夠加載插件中的類顯然是不夠的,還要能夠啟動(dòng)插件中的的四大組件,并正確的執(zhí)行四大組件的生命周期,為什么不能夠執(zhí)行四大組件的生命周期呢?
這是因?yàn)椋挥性谖覀兯拗靼蠱anifest文件中注冊(cè)的四大組件才能夠啟動(dòng),如果沒(méi)有注冊(cè),就會(huì)拋出異常,提醒你在Manifest中注冊(cè)。這也是我們遇到的第二個(gè)問(wèn)題。
Android 中四大組件包括Activity、Service、廣播和ContentProvider,我們主要介紹一下Activity。
如果我們想讓對(duì)應(yīng)的Activity啟動(dòng),一般有如下幾種方法:
我們針對(duì)這幾種分別解釋一下。
將所有的四大組件在宿主包中都提前聲明,這是最簡(jiǎn)單粗暴的方式。
但這種方式會(huì)丟失插件化的動(dòng)態(tài)性,也就是說(shuō),如果想在插件包中,加入宿主包沒(méi)有注冊(cè)的Activity,這就會(huì)有問(wèn)題。
那這種方式的優(yōu)點(diǎn)呢?解決包體積的問(wèn)題的同時(shí)不用處理復(fù)雜的組件加載以及伴隨的生命周期的問(wèn)題。
那如果想要保存插件的動(dòng)態(tài)化加載呢?也就是說(shuō)我們想要在插件包中的 Manifest 文件中進(jìn)行注冊(cè)。
默認(rèn)情況下,如果我們啟動(dòng)一個(gè)沒(méi)有在插件 Manifest 中注冊(cè)的的 Activity,會(huì)發(fā)生 error,原因是啟動(dòng)過(guò)程中的 Instrumentation 中的 checkStartActivityResult 方法:
public class Instrumentation { public static void checkStartActivityResult(int res, Object intent) { if (!ActivityManager.isStartResultFatalError(res)) { return; } switch (res) { case ActivityManager.START_INTENT_NOT_RESOLVED: case ActivityManager.START_CLASS_NOT_FOUND: if (intent instanceof Intent && ((Intent)intent).getComponent() != null) throw new ActivityNotFoundException( "Unable to find explicit activity class " + ((Intent)intent).getComponent().toShortString() + "; have you declared this activity in your AndroidManifest.xml?"); throw new ActivityNotFoundException( "No Activity found to handle " + intent); ... } }}
所以我們傳入的Activity必須要在宿主包中注冊(cè),這樣系統(tǒng)才能檢驗(yàn)通過(guò),那怎么才能實(shí)現(xiàn)動(dòng)態(tài)化呢?
答案是使用占位Activity,這其實(shí)就是使用的代理模式。每次需要啟動(dòng)插件中的Activity的時(shí)候,先啟動(dòng)一個(gè)占位Activity實(shí)例,然后在占位Activity實(shí)例里面持有目標(biāo)Activity的實(shí)例對(duì)象,從而通過(guò)反射或者其他方法調(diào)用實(shí)例的生命周期。
生命周期處理
這種方法的問(wèn)題主要如下:
第三種方法我們稱之為欺騙系統(tǒng),具體怎么個(gè)欺騙方法呢?
先看一下具體Activity的啟動(dòng)流程,默認(rèn)大家對(duì)Activity的啟動(dòng)流程比較了解了:
Activity啟動(dòng)流程
我們?cè)谡麄€(gè)過(guò)程中,同樣也需要一個(gè)占位Activity。
使用步驟如下:
最終,系統(tǒng)以為自己調(diào)用的是占位Activity的對(duì)象,并且和實(shí)際上調(diào)用的是PluginActivity進(jìn)行綁定。
在最終使用之前,我們?cè)诓寮械腁ndroid資源文件并不能使用,比如說(shuō)圖片、字符串、布局文件等,原因是插件的資源路徑并沒(méi)有被添加。
Apk安裝以后,我們都是通過(guò) Resource 對(duì)象去訪問(wèn)資源,簡(jiǎn)單看一下 Resouce 的構(gòu)造方法:
@Deprecated public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) { this(null); mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments()); }
可以看到,構(gòu)造函數(shù)中有一個(gè)參數(shù)是 AssetManager,我們可以通過(guò)在 AssetManager 中,加入我們插件的資源地址,就可以訪問(wèn)到插件中的資源。
現(xiàn)在可以訪問(wèn)具體的資源了,和之前的類加載方式類似,也有兩種加載方式:
首先,我們想一下為什么會(huì)有資源沖突問(wèn)題?其實(shí)是因?yàn)樗拗骱筒寮际仟?dú)立編譯的,所以打包的時(shí)候生成的資源Id會(huì)存在相同的情況,這個(gè)時(shí)候,訪問(wèn)的的時(shí)候就存在資源沖突。
我們項(xiàng)目之前采用的 Qigsaw 方案,所以簡(jiǎn)單介紹一下合并式的方案,資源id是8位16進(jìn)制數(shù)表示:
Qigsaw
如上圖:
所以我們對(duì)不同的插件包,進(jìn)行打包的時(shí)候,前面的PP字段,可以進(jìn)行依次遞減,可以避免資源沖突的問(wèn)題。常用的方案有:
Qigsaw使用的第一種方案。
本文是一篇入門插件化的文章,主要回答了插件化是什么,有什么難點(diǎn),又是怎么解決的,其中沒(méi)有涉及到很多代碼,非常適合入門。
本文鏈接:http://www.tebozhan.com/showinfo-26-112727-0.html面試官:你對(duì)插件化有什么了解?
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com