插件化技術可以說是Android高級工程師所必須具備的技能之一,從2012年插件化概念的提出(Android版本),到2016年插件化的百花爭艷,可以說,插件化技術引領著Android技術的進步。本篇文章轉載自騰訊bugly,覺得寫得不錯,轉載分享給大家。
插件化提要
可以說,插件化技術涉及得非常廣泛,其中最核心的就是Android的類加載機制和反射機制,相關原理請大家自行百度。
插件化發展歷史
插件化技術最初源于免安裝運行apk的想法,這個免安裝的apk可以理解為插件。支持插件化的app可以在運行時加載和運行插件,這樣便可以將app中一些不常用的功能模塊做成插件,一方面減小了安裝包的大小,另一方面可以實現app功能的動態擴展。想要實現插件化,主要是解決下面三個問題:
插件中代碼的加載和與主工程的互相調用
插件中資源的加載和與主工程的互相訪問
四大組件生命周期的管理
下面是比較出名的幾個開源的插件化框架,按照出現的時間排序。研究它們的實現原理,可以大致看出插件化技術的發展,根據實現原理可以將這幾個框架劃分成了三代。
第一代:dynamic-load-apk最早使用ProxyActivity這種靜態代理技術,由ProxyActivity去控制插件中PluginActivity的生命周期。該種方式缺點明顯,插件中的activity必須繼承PluginActivity,開發時要小心處理context。而DroidPlugin通過Hook系統服務的方式啟動插件中的Activity,使得開發插件的過程和開發普通的app沒有什么區別,但是由于hook過多系統服務,異常復雜且不夠穩定。
第二代:為了同時達到插件開發的低侵入性(像開發普通app一樣開發插件)和框架的穩定性,在實現原理上都是趨近于選擇盡量少的hook,并通過在manifest中預埋一些組件實現對四大組件的插件化。另外各個框架根據其設計思想都做了不同程度的擴展,其中Small更是做成了一個跨平臺,組件化的開發框架。
第三代:VirtualApp比較厲害,能夠完全模擬app的運行環境,能夠實現app的免安裝運行和雙開技術。Atlas是阿里今年開源出來的一個結合組件化和熱修復技術的一個app基礎框架,其廣泛的應用與阿里系的各個app,其號稱是一個容器化框架。
插件化原理
類加載
Android中常用的有兩種類加載器,DexClassLoader和PathClassLoader,它們都繼承于BaseDexClassLoader。相關源碼如下:
區別在于調用父類構造器時,DexClassLoader多傳了一個optimizedDirectory參數,這個目錄必須是內部存儲路徑,用來緩存系統創建的Dex文件。而PathClassLoader該參數為null,只能加載內部存儲目錄的Dex文件。所以我們可以用DexClassLoader去加載外部的apk,用法如下:
其實,關于類加載更詳細的內容,筆者也深入剖析過,可以查看下面的鏈接:類加載機制詳解
雙親委托機制
ClassLoader調用loadClass方法加載類,代碼如下:
可以看出ClassLoader加載類時,先查看自身是否已經加載過該類,如果沒有加載過會首先讓父加載器去加載,如果父加載器無法加載該類時才會調用自身的findClass方法加載,該機制很大程度上避免了類的重復加載。
DexPathList
這里要重點說一下DexClassLoader的DexPathList。DexClassLoader重載了findClass方法,在加載類時會調用其內部的DexPathList去加載。DexPathList是在構造DexClassLoader時生成的,其內部包含了DexFile。如下圖所示:
DexPathList的loadClass會去遍歷DexFile直到找到需要加載的類。
騰訊的qq空間熱修復技術正是利用了DexClassLoader的加載機制,將需要替換的類添加到dexElements的前面,這樣系統會使用先找到的修復過的類。
單DexClassLoader與多DexClassLoader
通過給插件apk生成相應的DexClassLoader便可以訪問其中的類,這邊又有兩種處理方式,有單DexClassLoader和多DexClassLoader兩種結構。
對于多DexClassLoader結構來說,可以用下面的模型來標識。
對于每個插件都會生成一個DexClassLoader,當加載該插件中的類時需要通過對應DexClassLoader加載。這樣不同插件的類是隔離的,當不同插件引用了同一個類庫的不同版本時,不會出問題,RePlugin采用的就是此方案。
對于單DexClassLoader來說,其模型如下:
將插件的DexClassLoader中的pathList合并到主工程的DexClassLoader中。這樣做的好處時,可以在不同的插件以及主工程間直接互相調用類和方法,并且可以將不同插件的公共模塊抽出來放在一個common插件中直接供其他插件使用。Small采用的是這種方式。
插件和主工程的互相調用涉及到以下兩個問題:
插件調用主工程
在構造插件的ClassLoader時會傳入主工程的ClassLoader作為父加載器,所以插件是可以直接可以通過類名引用主工程的類。
主工程調用插件
若使用多ClassLoader機制,主工程引用插件中類需要先通過插件的ClassLoader加載該類再通過反射調用其方法。插件化框架一般會通過統一的入口去管理對各個插件中類的訪問,并且做一定的限制。
若使用單ClassLoader機制,主工程則可以直接通過類名去訪問插件中的類。該方式有個弊病,若兩個不同的插件工程引用了一個庫的不同版本,則程序可能會出錯,所以要通過一些規范去避免該情況發生。
關于雙親委托更詳細的資料,大家也可以訪問我博客之前的介紹:classloader雙親委托模式
資源加載
Android系統通過Resource對象加載資源,下面代碼展示了該對象的生成過程。
因此,只要將插件apk的路徑加入到AssetManager中,便能夠實現對插件資源的訪問。
具體實現時,由于AssetManager并不是一個public的類,需要通過反射去創建,并且部分Rom對創建的Resource類進行了修改,所以需要考慮不同Rom的兼容性。
資源路徑的處理
和代碼加載相似,插件和主工程的資源關系也有兩種處理方式:
合并式:addAssetPath時加入所有插件和主工程的路徑;
獨立式:各個插件只添加自己apk路徑
合并式由于AssetManager中加入了所有插件和主工程的路徑,因此生成的Resource可以同時訪問插件和主工程的資源。但是由于主工程和各個插件都是獨立編譯的,生成的資源id會存在相同的情況,在訪問時會產生資源沖突。
獨立式時,各個插件的資源是互相隔離的,不過如果想要實現資源的共享,必須拿到對應的Resource對象。
Context的處理
通常我們通過Context對象訪問資源,光創建出Resource對象還不夠,因此還需要一些額外的工作。 對資源訪問的不同實現方式也需要不同的額外工作。以VirtualAPK的處理方式為例。
第一步:創建Resource
第二步:hook主工程的Resource
對于合并式的資源訪問方式,需要替換主工程的Resource,下面是具體替換的代碼。
注意下上述代碼hook了幾個地方,包括以下幾個hook點:
替換了主工程context中LoadedApk的mResource對象。
將新的Resource添加到主工程ActivityThread的mResourceManager中,并且根據Android版本做了不同處理。
第三步:關聯resource和Activity
上述代碼是在Activity創建時被調用的(后面會介紹如何hook Activity的創建過程),在activity被構造出來后,需要替換其中的mResources為插件的Resource。由于獨立式時主工程的Resource不能訪問插件的資源,所以如果不做替換,會產生資源訪問錯誤。
做完以上工作后,則可以在插件的Activity中放心的使用setContentView,inflater等方法加載布局了。
解決資源沖突
合并式的資源處理方式,會引入資源沖突,原因在于不同插件中的資源id可能相同,所以解決方法就是使得不同的插件資源擁有不同的資源id。
資源id是由8位16進制數表示,表示為0xPPTTNNNN。PP段用來區分包空間,默認只區分了應用資源和系統資源,TT段為資源類型,NNNN段在同一個APK中從0000遞增。如下表所示:
所以思路是修改資源ID的PP段,對于不同的插件使用不同的PP段,從而區分不同插件的資源。具體實現方式有兩種:
修改aapt源碼,編譯期修改PP段。
修改resources.arsc文件,該文件列出了資源id到具體資源路徑的映射。
四大組件支持
Android開發中有一些特殊的類,是由系統創建的,并且由系統管理生命周期。如常用的四大組件,Activity,Service,BroadcastReceiver和ContentProvider。 僅僅構造出這些類的實例是沒用的,還需要管理組件的生命周期。其中以Activity最為復雜,不同框架采用的方法也不盡相同。下面以Activity為例詳細介紹插件化如何支持組件生命周期的管理。 大致分為兩種方式:
ProxyActivity代理
預埋StubActivity,hook系統啟動Activity的過程
ProxyActivity代理
ProxyActivity代理的方式最早是由dynamic-load-apk提出的,其思想很簡單,在主工程中放一個ProxyActivy,啟動插件中的Activity時會先啟動ProxyActivity,在ProxyActivity中創建插件Activity,并同步生命周期。下圖展示了啟動插件Activity的過程。
具體的過程如下:
首先需要通過統一的入口(如圖中的PluginManager)啟動插件Activity,其內部會將啟動的插件Activity信息保存下來,并將intent替換為啟動ProxyActivity的intent。
ProxyActivity根據插件的信息拿到該插件的ClassLoader和Resource,通過反射創建PluginActivity并調用其onCreate方法。
PluginActivty調用的setContentView被重寫了,會去調用ProxyActivty的setContentView。由于ProxyActivity重寫了getResource返回的是插件的Resource,所以setContentView能夠訪問到插件中的資源。同樣findViewById也是調用ProxyActivity的。
ProxyActivity中的其他生命周期回調函數中調用相應PluginActivity的生命周期。
理解ProxyActivity代理方式主要注意兩點:
ProxyActivity中需要重寫getResouces,getAssets,getClassLoader方法返回插件的相應對象。生命周期函數以及和用戶交互相關函數,如onResume,onStop,onBackPressedon,KeyUponWindow,FocusChanged等需要轉發給插件。
PluginActivity中所有調用context的相關的方法,如setContentView,getLayoutInflater,getSystemService等都需要調用ProxyActivity的相應方法。
缺點
插件中的Activity必須繼承PluginActivity,開發侵入性強。
如果想支持Activity的singleTask,singleInstance等launchMode時,需要自己管理Activity棧,實現起來很繁瑣。
插件中需要小心處理Context,容易出錯。
如果想把之前的模塊改造成插件需要很多額外的工作。
該方式雖然能夠很好的實現啟動插件Activity的目的,但是由于開發式侵入性很強,dynamic-load-apk之后的插件化方案很少繼續使用該方式,而是通過hook系統啟動Activity的過程,讓啟動插件中的Activity像啟動主工程的Activity一樣簡單。
hook方式
在介紹hook方式之前,先用一張圖簡要的介紹下系統是如何啟動一個Activity的。
上圖列出的是啟動一個Activity的主要過程,具體步驟如下:
Activity1調用startActivity,實際會調用Instrumentation類的execStartActivity方法,Instrumentation是系統用來監控Activity運行的一個類,Activity的整個生命周期都有它的影子。
通過跨進程的binder調用,進入到ActivityManagerService中,其內部會處理Activity棧。之后又通過跨進程調用進入到Activity2所在的進程中。
ApplicationThread是一個binder對象,其運行在binder線程池中,內部包含一個H類,該類繼承于類Handler。ApplicationThread將啟動Activity2的信息通過H對象發送給主線程。
主線程拿到Activity2的信息后,調用Instrumentation類的newActivity方法,其內通過ClassLoader創建Activity2實例。
下面介紹如何通過hook的方式啟動插件中的Activity,需要解決以下兩個問題:
插件中的Activity沒有在AndroidManifest中注冊,如何繞過檢測。
如何構造Activity實例,同步生命周期
解決方法有很多種,以VirtualAPK為例,核心思路如下:
先在Manifest中預埋StubActivity,啟動時hook上圖第1步,將Intent替換成StubActivity。
hook第10步,通過插件的ClassLoader反射創建插件Activity
之后Activity的所有生命周期回調都會通知給插件Activity
替換系統Instrumentation
VirtualAPK在初始化時會調用hookInstrumentationAndHandler,該方法hook了系統的Instrumentaiton類,由上文可知該類和Activity的啟動息息相關。
該段代碼將主線程中的Instrumentation對象替換成了自定義的VAInstrumentation類。在啟動和創建插件activity時,該類都會偷偷做一些手腳。
hook activity啟動過程
VAInstrumentation類重寫了execStartActivity方法,相關代碼如下:
execStartActivity中會先去處理隱式intent,如果該隱式intent匹配到了插件中的Activity,將其轉換成顯式。之后通過markIntentIfNeeded將待啟動的的插件Activity替換成了預先在AndroidManifest中占坑的StubActivity,并將插件Activity的信息保存到該intent中。其中有個dispatchStubActivity函數,會根據Activity的launchMode選擇具體啟動哪個StubActivity。VirtualAPK為了支持Activity的launchMode在主工程的AndroidManifest中對于每種啟動模式的Activity都預埋了多個坑位。
hook Activity的創建過程
上一步欺騙了系統,讓系統以為自己啟動的是一個正常的Activity。當來到圖 3.2的第10步時,再將插件的Activity換回來。此時調用的是VAInstrumentation類的newActivity方法。
由于AndroidManifest中預埋的StubActivity并沒有具體的實現類,所以此時會發生ClassNotFoundException。之后在處理異常時取出插件Activity的信息,通過插件的ClassLoader反射構造插件的Activity。
其他操作
插件Activity構造出來后,為了能夠保證其正常運行還要做些額外的工作。
這段代碼主要是將Activity中的Resource,Context等對象替換成了插件的相應對象,保證插件Activity在調用涉及到Context的方法時能夠正確運行。
經過上述步驟后,便實現了插件Activity的啟動,并且該插件Activity中并不需要什么額外的處理,和常規的Activity一樣。那問題來了,之后的onResume,onStop等生命周期怎么辦呢?答案是所有和Activity相關的生命周期函數,系統都會調用插件中的Activity。原因在于AMS在處理Activity時,通過一個token表示具體Activity對象,而這個token正是和啟動Activity時創建的對象對應的,而這個Activity被我們替換成了插件中的Activity,所以之后AMS的所有調用都會傳給插件中的Activity。
其他組件
四大組件中Activity的支持是最復雜的,其他組件的實現原理要簡單很多,簡要概括如下:
Service:Service和Activity的差別在于,Activity的生命周期是由用戶交互決定的,而Service的生命周期是我們通過代碼主動調用的,且Service實例和manifest中注冊的是一一對應的。實現Service插件化的思路是通過在manifest中預埋StubService,hook系統startService等調用替換啟動的Service,之后在StubService中創建插件Service,并手動管理其生命周期。
BroadCastReceiver:解析插件的manifest,將靜態注冊的廣播轉為動態注冊。
ContentProvider:類似于Service的方式,對插件ContentProvider的所有調用都會通過一個在manifest中占坑的ContentProvider分發。
小結
VirtualAPK通過替換了系統的Instrumentation,hook了Activity的啟動和創建,省去了手動管理插件Activity生命周期的繁瑣,讓插件Activity像正常的Activity一樣被系統管理,并且插件Activity在開發時和常規一樣,即能獨立運行又能作為插件被主工程調用。
其他插件框架在處理Activity時思想大都差不多,無非是這兩種方式之一或者兩者的結合。在hook時,不同的框架可能會選擇不同的hook點。如360的RePlugin框架選擇hook了系統的ClassLoader,即圖3.2中構造Activity2的ClassLoader,在判斷出待啟動的Activity是插件中的時,會調用插件的ClassLoader構造相應對象。另外RePlugin為了系統穩定性,選擇了盡量少的hook,因此它并沒有選擇hook系統的startActivity方法來替換intent,而是通過重寫Activity的startActivity,因此其插件Activity是需要繼承一個類似PluginActivity的基類的。不過RePlugin提供了一個Gradle插件將插件中的Activity的基類換成了PluginActivity,用戶在開發插件Activity時也是沒有感知的。
本文在開源項目:https://github.com/Android-Alvin/Android-LearningNotes 中已收錄,里面包含了Android插件化最全開源項目(騰訊插件化框架 Shadow、360插件化框架RePlugin、愛奇藝插件框架 Neptune、 滴滴VirtualApk、360插件開發之DroidPlugin...)等,資源持續更新中...