外掛化系列開發之九--Android 全面外掛化 RePlugin 流程與原始碼解析
RePlugin,360開源的全面外掛化框架,按照官網說的,其目的是“儘可能多的讓模組變成外掛”,並在很穩定的前提下,儘可能像開發普通App那樣靈活。那麼下面就讓我們一起深入♂瞭解它吧。 (ps :閱讀本文請多參考原始碼圖片 ( ̄^ ̄)ゞ )
一、介紹
RePlugin對比其他外掛化,它的強大和特色,在於它只Hook住了ClassLoader。One Hook這個堅持,最大程度保證了穩定性、相容性和可維護性,詳見《全面外掛化——RePlugin的使命》。當然,One Hook也極大的提高了實現複雜程度性,其中主要體現在:
- 增加了Gradle外掛指令碼,實現開發中自動程式碼修改與生成。
- 分割了外掛庫和宿主庫的程式碼實現。
- 程式碼中存在很多不少
@deprecated
、TODO
和臨時修改。 - 初始化、載入、啟動等邏輯比較複雜。
本篇將竭盡所能,為各位介紹其流程和內部實現,如果存在一些地方存在紕漏,還請指出。文章篇幅較長,需耐心閱讀,閱讀時可結合圖片原始碼,同時歡迎收藏,或選擇感興趣點閱讀,下面主要涉及:
- 二、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等
其中的RePlugin
、RePluginInternal
、PluginServiceClient
都是反射宿主App
:replugin-host-library
中的 RePlugin
、 RePluginInternal
、PluginServiceClient
類方法。
四、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內部如下圖。
五、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
初始化開始。如下圖,這裡主要是配置RePluginConfig
和 RePluginCallbacks
,然後根據
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
類,並初始化了它,建立了內部使用的 PmLocalImpl
和 PmInternalImp
介面
,同時Hook住主程式的 ClassLoader,替換為 RePluginClassLoader
,所以接下來的流程,主要是在 PmBase
。
PmBase
,按照專案中的變數名 mPluginMgr
,可以理解為外掛的管理者,它管理內部直接或間接的,管理著坑位分配、ClassLoader、外掛、程序、啟動\停止頁面的介面等,如下圖。
PmBase
的初始化,也就是外掛的初始化,這裡會啟動各類程序,初始化各種預設外掛集合,為後續載入做準備。其中預設外掛和配置檔案的位置,一般預設是在
assert 的 plugins-builtin.json
和 "plugins"
資料夾下。
接著PMF.callAttach()
其實就是 PmBase.callAttach()
,如下圖這裡開始真正載入外掛,初始化外掛的 PluginDexClassLoader
、載入外掛、初始化外掛環境和介面。其中在執行p.load()
的時候,會通過 Plugind.callAppLocked()
建立外掛的
Application,並初始化。
以上是在主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經歷了 Factory
、 PmLocalImpl
,其實大部分啟動的邏輯其實主要在PmInternalImpl
中。
具體流程如下圖,這裡簡化了實際程式碼,關鍵在於 loadPluginActivity
。這裡獲取了外掛對應的坑位,然後儲存了目標Activity的資訊,通過系統啟動坑位。
因為已經Hook住了ClassLoader,在 loadClass
時再加載出目標Activity,這樣坑位中承載的,便是繞過系統開啟的目標Activity。下面我們進入 loadPluginActivity
。
2、loadPluginActivity
loadPluginActivity
其實是 PmBase
中的 PmLocalImpl
內部方法。如下圖,這裡主要是根據獲取到 ActivityInfo
,然後根據坑位去為目標Activity分配坑位。
其中 getActivityInfo
是通過外掛名稱,獲得外掛物件 Plugin
, Plugin
可能是初始化中已載入的,如果未載入就載入返回,然後根據 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等的就不多做敘述了,不知道本文對你是否能有些幫助,歡迎留言討論。
最後說“一”句
為什麼要去了解一個庫實現原理呢?學習框架的架構思想?這是一個原因。但是歸根結底,是幫助你在使用庫的過程中,能靠自己解決各種問題。程式設計師的日常一般都忙於各種工作,各種技術群中的大佬們,大部分時候,沒辦法一一解答你的各種諮詢,所以使用它、瞭解它、多嘗試靠自己去探索突破吧。
自此外掛化系列已完結,後續有變化持續更新。