Android全面插件化RePlugin流程與源碼解析

RePlugin,360開源的全面插件化框架,按照官網說的,其目的是“盡可能多的讓模塊變成插件”,并在很穩定的前提下,盡可能像開發普通App那樣靈活。那么下面就讓我們一起深入♂了解它吧。 (ps :閱讀本文請多參考源碼圖片 ( ̄^ ̄)ゞ )

一、介紹

RePlugin對比其他插件化,它的強大和特色,在于它只Hook住了ClassLoader。One Hook這個堅持,最大程度保證了穩定性、兼容性和可維護性,詳見《全面插件化——RePlugin的使命》。當然,One Hook也極大的提高了實現復雜程度性,其中主要體現在:

  • 增加了Gradle插件腳本,實現開發中自動代碼修改與生成。
  • 分割了插件庫和宿主庫的代碼實現。
  • 代碼中存在很多不少@deprecatedTODO和臨時修改。
  • 初始化、加載、啟動等邏輯比較復雜。
圖一 Replugin項目結構

本篇將竭盡所能,為各位介紹其流程和內部實現,如果存在一些地方存在紕漏,還請指出。文章篇幅較長,需耐心閱讀,閱讀時可結合圖片源碼,同時歡迎收藏,或選擇感興趣點閱讀,下面主要涉及:

  • 二、ClassLoader基礎知識。
  • 三、Replugin項目原理和結構分析。
  • 四、Replugin的ClassLoader。
  • 五、Replugin的相關類介紹。
  • 六、Replugin的初始化。
  • 七、Replugin啟動Activity。
此處應有圖

二、ClassLoader基礎知識

既然Replugin選擇Hook住ClassLoader,那先簡單介紹下ClassLoader的基本知識吧,如熟悉者請略過。

ClassLoader又叫類加載器,是專門處理類加載,一個APP可以存在多個ClassLoader,它使用的是雙親代理模型,如下圖所示,創建一個ClassLoader,需要使用一個已有的ClassLoader對象,作為新建的實例的ParentLoader。

抽象基類ClassLoader

這樣的條件下,一個App中所有的ClassLoader都聯系了起來。當加載類時,如果當前ClassLoader未加載此類,就查詢ParentLoader是否加載過,一直往上查找,如果存在就返回,如果都沒有,就執行該Loader去執行加載工作。這樣避免了類重復加載的浪費。其中常見的Loader有:

  • BootClassLoader 是系統啟動時創建的,一般不需要用到。
  • PathClassLoader 是應用啟動時創建的,只能加載內部dex。
  • DexClassLoader 可以加載外部的dex。

RePlugin中存在兩個主要ClassLoaer:

  • 1、RePluginClassLoader 宿主App中的Loader,繼承PathClassLoader,也是唯一Hook住系統的Loader。

  • 2、PluginDexClassLoader 加載插件的Loader,繼承DexClassLoader。用來做一些“更高級”的特性。

三、Replugin項目原理和結構分析

1、基礎原理

簡單來說,其核心是hook住了 ClassLoader,在Activity啟動前:

  • 記錄下目標頁 ActivityA,替換成已自動注冊在 AndroidManifest 中的坑位 ActivityNS
  • ClassLoader 中攔截ActivityNS的創建,創建出ActivityA返回。
  • 返回的ActivityA占用著 ActivityNS 這個坑位,坑位由Gradle編譯時自動生成在AndroidManifest中。

在編譯時,replugin-replugin-library腳本,會替換代碼中的基礎類和方法。如下圖【官方原理圖】所示,替換的基類里會做一些初始化,所以這一塊稍微有點入侵性。此外,replugin-host-library生成AndroidManifest配置相關信息打包等,也由Gradle插件自動完成。

打包獨立APK,或者打包為插件,可單可插,這就是RePlugin。

官方原理圖

2、項目結構

RePlugin整個項目結構,目前分為四個module,其中又分為兩個gradle插件module,兩個library的java module,詳細如開頭【圖一 Replugin項目結構】,本文主要分析library相關,如果對gradle插件感興趣的,可以查看結尾其他推薦。

2.1、replugin-host-gradle :

對應com.qihoo360.replugin:replugin-host-gradle:xxx依賴,主要負責在主程序的編譯期中生產各類文件:

  • 根據用戶的配置文件,生成HostBuildConfig類,方便插件框架讀取并自定義其屬性,如:進程數、各類型占位坑的數量、是否使用AppCompat庫、Host版本、pulgins-builtin.json文件名、內置插件文件名等。

  • 自動生成帶 RePlugin 插件坑位的 AndroidManifest.xml文件,文件中帶有如:

<activity 
    android:theme="@style/Theme.AppCompat" 
    android:name="com.qihoo360.replugin.sample.host.loader.a.ActivityN1STTS0"
    android:exported="false" 
    android:screenOrientation="portrait"
    android:configChanges="keyboard|keyboardHidden|orientation|screenSize" 
/>
2.2、replugin-host-library:

對應com.qihoo360.replugin:replugin-host-lib:xxx依賴,是一個Java工程,由主程序負責引入,是RePlugin的核心工程,負責初始化、加載、啟動、管理插件等。

2.3、replugin-plugin-gradle:

對應com.qihoo360.replugin:replugin-plugin-gradle:xxx ,是一個Gradle插件,由插件負責引入,主要負責在插件的編譯期中:配置插件打包相關信息;動態替換插件工程中的繼承基類,如下,修改Activity的繼承、Provider的重定向等。

    /* LoaderActivity 替換規則 */
    def private static loaderActivityRules = [
            'android.app.Activity'                    : 'com.qihoo360.replugin.loader.a.PluginActivity',
            'android.app.TabActivity'                 : 'com.qihoo360.replugin.loader.a.PluginTabActivity',
            'android.app.ListActivity'                : 'com.qihoo360.replugin.loader.a.PluginListActivity',
            'android.app.ActivityGroup'               : 'com.qihoo360.replugin.loader.a.PluginActivityGroup',
            'android.support.v4.app.FragmentActivity' : 'com.qihoo360.replugin.loader.a.PluginFragmentActivity',
            'android.support.v7.app.AppCompatActivity': 'com.qihoo360.replugin.loader.a.PluginAppCompatActivity',
            'android.preference.PreferenceActivity'   : 'com.qihoo360.replugin.loader.a.PluginPreferenceActivity',
            'android.app.ExpandableListActivity'      : 'com.qihoo360.replugin.loader.a.PluginExpandableListActivity'
    ]
2.4、replugin-plugin-library:

對應com.qihoo360.replugin:replugin-plugin-lib:xxx依賴,是一個Java工程,由插件端負責引入,主要提供通過“Java反射”來調用主程序中RePlugin Host Library的相關接口,并提供“雙向通信”的能力,以及各種基類Activity等
  
  其中的RePluginRePluginInternalPluginServiceClient都是反射宿主App :replugin-host-library 中的 RePluginRePluginInternalPluginServiceClient 類方法。

四、Replugin的ClassLoader。

這里主要介紹,宿主和插件使用的ClassLoader,以及它們的創建和Hook住時機。這是RePlugin唯一的Hook點,而其中插件ClassLoader和宿主ClassLoader是相互關系的,如下圖

將就的圖
1、宿主的ClassLoader

RePluginClassLoader,宿主的ClassLoader,繼承 PathClassLoader,構造方法使用原ClassLoader,和原ClassLoader的Parent生成。其中ParentLoader是因為雙親代理模型,創建ClassLoader所需,而原Loader用于保留在后期使用,如下圖

如下兩圖RePluginClassLoader 在創建時,淺拷貝原Loader的資源到 RePluginClassLoader 中,用于欺騙系統還處于原Loader,并且從原Loader中反射出常用方法,用于重載方法中使用。

拷貝資源
方式方法

宿主Loader中,主要是重載了 loadClass,其中從 PMF(RePlugin中公開接口類)中查找class,如果存在即返回插件class,如果不存在就從原Loader中加載。從而實現了對加載類的攔截。

這里的 PMF 在加載class時,其實用的是下面【2、插件的ClassLoader 】:PluginDexClassLoader,這個后面流程會講到。

2、插件的ClassLoader

PluginDexClassLoader,繼承DexClassLoader,構造時持有了宿主的ClassLoader,從宿主ClassLoader中反射獲取loadClass方法,當自己的loadClass方法找不到類時,從宿主Loader中加載。

3、創建和Hook

創建:上面1、2中兩個Loader,是宿主在初始化時創建的,初始化時可以選擇配置RePluginCallbacks,callback中提供方法默認創建Loader,你也可以實現自定義的ClassLoader,但是需要繼承以上的Loader,如下圖

//初始化方式創建
RePlugin.getConfig().getCallbacks()
.createClassLoader(oClassLoader.getParent(), oClassLoader);
RePluginCallbacks

Hook:初始化時,PatchClassLoaderUtils會在Application的attachBaseContext()中,通過patch(application)Hook住宿主的ClassLoader,patch內部如下圖

hook ClassLoader

五、Replugin的相關類介紹

提前介紹一些功能類,后面就不做詳細介紹。

** 1、RePlugin** :RePlugin的對外入口類,提供install、uninstall、preload、startActivity、fetchPackageInfo、fetchComponentList,fetchClassLoader等等統一的方法入口,用戶操作的主要是它。
  
2、RePlugin.App:RePlugin中的內部類,針對Application的入口類,所有針對插件Application的調用應從此類開始和初始化,想象成插件的Application吧。

3、PmBase:RePlugin常用mPluginMgr變量表示,可以看作插件管理者。初始化插件、加載插件等一般都是從它開始。

4、PluginContainers:插件容器管理中心。

5、PmLocalImpl:各種本地接口實現,如startActivity,getActivityInfo,loadPluginActivity等。

6、PmInternalImpl:類似Activity的接口實現,內部實現了真正startActivity的邏輯、還有插件Activity生命周期的接口。

準備好了嗎,騷年

六、Replugin的初始化

那就是從 Application 初始化開始看起,枯燥的流程就要開始了,忍住兄弟,我們能贏。首先我們先看下面這流程圖,大致了解啟動流程:

將就的看吧
1、attachBaseContext

首先是從 Application 的 attachBaseContext 初始化開始。如下圖,這里主要是配置 RePluginConfigRePluginCallbacks ,然后根據 Config 去初始化插件。值得注意的是,RePluginConfig 中的 RePluginCallbacks 提供了默認方法創建 RePlugin 的 ClassLoader,還記得上面的介紹嗎?

看圖看圖
2、插件App.attachBaseContext

繼續上面的流程,進入RePlugin.App.attachBaseContext(this, c),如下圖,這里主要是初始化插件相關的進程、配置信息、插件的主框架和接口、根據默認路徑、加載默認插件等。插件的初始化從這里開始,其中主要為 PMF.init()PMF.callAttach()

繼續看圖看圖
3、主程序接口 PMF.init()/PMF.callAttach()

先進入到 PMF.init() ,如下圖,這里主要實例化了 PmBase 類,并初始化了它,創建了內部使用的 PmLocalImplPmInternalImp 接口 ,同時Hook住主程序的 ClassLoader,替換為 RePluginClassLoader,所以接下來的流程,主要是在 PmBase

PMF.init(),看圖吧

PmBase,按照項目中的變量名 mPluginMgr,可以理解為插件的管理者,它管理內部直接或間接的,管理著坑位分配、ClassLoader、插件、進程、啟動\停止頁面的接口等,如下圖。

PmBase創建,還是看圖

PmBase 的初始化,也就是插件的初始化,這里會啟動各類進程,初始化各種默認插件集合,為后續加載做準備。其中默認插件和配置文件的位置,一般默認是在 assert 的 plugins-builtin.json 和 "plugins" 文件夾下。

PmBase.init() 看圖看圖

接著PMF.callAttach() 其實就是 PmBase.callAttach()如下圖這里開始真正加載插件,初始化插件的 PluginDexClassLoader 、加載插件、初始化插件環境和接口。其中在執行 p.load() 的時候,會通過 Plugind.callAppLocked() 創建插件的 Application,并初始化。

PMF.callAttach() 看圖唄

以上是在主APP的初始化,深入 PmBase 中,Plugin.load()在加載時,會調用PluginDexClassLoader, 通過類名加載 Entry 類,然后反射出create方法,執行插件的初始化。其中 Entry 位于Plugin-lib庫中。這里初始化就去到了插件中了,插件中初始化時,會通過反射的到宿主host類的方法。

4、Application的onCreate

這里主要是切換handler到主線程,注冊各種廣播接收監聽,如增加插件、卸載插件、更新插件,可以看出這里設計很多內部進程通信的。

七、Replugin啟動Activity

這里僅描述了Activity啟動的其中一個流程,也是簡化版的,實際代碼邏輯復雜多了,但是萬變不離其宗,這里幫你梳理流程,描述一些關鍵的點,讓你快速理解Activity的啟動流程。

再將就下吧,看圖
1、startActivity

從上面的流程圖我們知道,啟動插件Activity可以從RePlugin.startActivity開始,startActivity經歷了 FactoryPmLocalImpl ,其實大部分啟動的邏輯其實主要在 PmInternalImpl 中。

具體流程如下圖,這里簡化了實際代碼,關鍵在于 loadPluginActivity。這里獲取了插件對應的坑位,然后保存了目標Activity的信息,通過系統啟動坑位。

因為已經Hook住了ClassLoader,在 loadClass 時再加載出目標Activity,這樣坑位中承載的,便是繞過系統打開的目標Activity。下面我們進入 loadPluginActivity

說了看圖
2、loadPluginActivity

loadPluginActivity 其實是 PmBase 中的 PmLocalImpl 內部方法。如下圖,這里主要是根據獲取到 ActivityInfo,然后根據坑位去為目標Activity分配坑位。

其中 getActivityInfo 是通過插件名稱,獲得插件對象 PluginPlugin可能是初始化中已加載的,如果未加載就加載返回,然后根據 Plugin 中緩存的坑位信息,返回 ActivityInfo

下面進入 allocActivityContainer 看坑位的分配,只有分配到坑位,插件的Activity才可以啟動,這是一個IPC過程。

看圖沒?
2、allocActivityContainer

allocActivityContainer 在類 PluginProcessPer 中,還記得我們在 PmBase.init() 時初始化過它么? 分配坑位也是RePlugin的核心之一。

allocActivityContainer 中, 主要邏輯是bindActivity ,如下圖,bindActivity 去找到目標Activity匹配的容器,然后加載目標Activity判斷是否存在,并建立映射,返回容器。然后分配的邏輯,在 PluginContainers.alloc 中。

看我大圖
3、PluginContainers.alloc

alloc / alloc2 方法分配坑位,最后都是到了 allocLocked 方法中,其實RePlugin中,如下圖,便是坑位分配的邏輯:

  • 如果存在未啟動的坑位,就使用它。
  • 如果沒有就找最老的:已經被釋放的、或者時間最老的。
  • 如果還不行,那么擠掉最老的一個。
看圖說話
4、PulginActivity

上面的流程總結,是替換目標Activity,加載插件,分配坑位,啟動目標坑位,攔截ClassLoader的loadClass去加載返回目標Activity。

這個時候啟動的Activity還不完整,從模塊框架中我們知道,在編譯時,RePlugin會把繼承的Activity替換為如 PluginActivity(當前還有AppComPluginActivity等)。這時候加載啟動的目標Activity,其實是繼承了 PluginActivity

如下圖PluginActivity 重載Activity中的一些方法,實現了Activity的補全和自定義操作,如坑位管理,啟動宿主Activity等。

至此,一個插件Activity就啟動起來了,頭暈目眩了沒?為了實現 One Hook 這個信念,RePlugin 實現了復雜的流程,從代碼中可以看出,這些年作者們從中走的的各種坑、各種妥協與堅持、復雜的技術積累、已經經歷了多年的嚴酷考驗。

不知道有多少人能完整看到這,碼字不易,如有疏漏還是多多包涵,由于篇(tou)幅(lan)原因,關于Service等的就不多做敘述了,不知道本文對你是否能有些幫助,歡迎留言討論。

最后說“一”句

為什么要去了解一個庫實現原理呢?學習框架的架構思想?這是一個原因。但是歸根結底,是幫助你在使用庫的過程中,能靠自己解決各種問題。程序員的日常一般都忙于各種工作,各種技術群中的大佬們,大部分時候,沒辦法一一解答你的各種咨詢,所以使用它、了解它、多嘗試靠自己去探索突破吧。

其他推薦

注意到了嗎?最后的總是我!
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容