深刻理解Android插件化技術

深刻理解Android插件化技術

插件化技術能夠說是Android高級工程師所必須具有的技能之一,從2012年插件化概念的提出(Android版本),到2016年插件化的百花爭豔,能夠說,插件化技術引領着Android技術的進步。緩存

做者:code_xzhapp

原文:框架

插件化提要

能夠說,插件化技術涉及得很是普遍,其中最核心的就是Android的類加載機制和反射機制,相關原理請你們自行百度。ide

插件化發展歷史

插件化技術最初源於免安裝運行apk的想法,這個免安裝的apk能夠理解爲插件。支持插件化的app能夠在運行時加載和運行插件,這樣即可以將app中一些不經常使用的功能模塊作成插件,一方面減少了安裝包的大小,另外一方面能夠實現app功能的動態擴展。想要實現插件化,主要是解決下面三個問題:函數

  • 插件中代碼的加載和與主工程的互相調用
  • 插件中資源的加載和與主工程的互相訪問
  • 四大組件生命週期的管理

下面是比較出名的幾個開源的插件化框架,按照出現的時間排序。研究它們的實現原理,能夠大體看出插件化技術的發展,根據實現原理能夠將這幾個框架劃分紅了三代。組件化

第一代:dynamic-load-apk最先使用ProxyActivity這種靜態代理技術,由ProxyActivity去控制插件中PluginActivity的生命週期。該種方式缺點明顯,插件中的activity必須繼承PluginActivity,開發時要當心處理context。而DroidPlugin經過Hook系統服務的方式啓動插件中的Activity,使得開發插件的過程和開發普通的app沒有什麼區別,可是因爲hook過多系統服務,異常複雜且不夠穩定。佈局

第二代:爲了同時達到插件開發的低侵入性(像開發普通app同樣開發插件)和框架的穩定性,在實現原理上都是趨近於選擇儘可能少的hook,並經過在manifest中預埋一些組件實現對四大組件的插件化。另外各個框架根據其設計思想都作了不一樣程度的擴展,其中Small更是作成了一個跨平臺,組件化的開發框架。spa

第三代:VirtualApp比較厲害,可以徹底模擬app的運行環境,可以實現app的免安裝運行和雙開技術。Atlas是阿里今年開源出來的一個結合組件化和熱修復技術的一個app基礎框架,其普遍的應用與阿里系的各個app,其號稱是一個容器化框架。.net

插件化原理

類加載

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的過程。

具體的過程以下:

  1. 首先須要經過統一的入口(如圖中的PluginManager)啓動插件Activity,其內部會將啓動的插件Activity信息保存下來,並將intent替換爲啓動ProxyActivity的intent。
  2. ProxyActivity根據插件的信息拿到該插件的ClassLoader和Resource,經過反射建立PluginActivity並調用其onCreate方法。
  3. PluginActivty調用的setContentView被重寫了,會去調用ProxyActivty的setContentView。因爲ProxyActivity重寫了getResource返回的是插件的Resource,因此setContentView可以訪問到插件中的資源。一樣findViewById也是調用ProxyActivity的。
  4. 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的主要過程,具體步驟以下:

  1. Activity1調用startActivity,實際會調用Instrumentation類的execStartActivity方法,Instrumentation是系統用來監控Activity運行的一個類,Activity的整個生命週期都有它的影子。
  2. 經過跨進程的binder調用,進入到ActivityManagerService中,其內部會處理Activity棧。以後又經過跨進程調用進入到Activity2所在的進程中。
  3. ApplicationThread是一個binder對象,其運行在binder線程池中,內部包含一個H類,該類繼承於類Handler。ApplicationThread將啓動Activity2的信息經過H對象發送給主線程。
  4. 主線程拿到Activity2的信息後,調用Instrumentation類的newActivity方法,其內經過ClassLoader建立Activity2實例。

下面介紹如何經過hook的方式啓動插件中的Activity,須要解決如下兩個問題:

  • 插件中的Activity沒有在AndroidManifest中註冊,如何繞過檢測。
  • 如何構造Activity實例,同步生命週期

解決方法有不少種,以VirtualAPK爲例,核心思路以下:

  1. 先在Manifest中預埋StubActivity,啓動時hook上圖第1步,將Intent替換成StubActivity。
  2. hook第10步,經過插件的ClassLoader反射建立插件Activity
  3. 以後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時也是沒有感知的。

相關文章
相關標籤/搜索