Android 全面外掛化 RePlugin 流程與原始碼解析

戀貓de小郭發表於2017-07-23

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

一、介紹

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

  • 增加了Gradle外掛指令碼,實現開發中自動程式碼修改與生成。
  • 分割了外掛庫和宿主庫的程式碼實現。
  • 程式碼中存在很多不少@deprecatedTODO和臨時修改。
  • 初始化、載入、啟動等邏輯比較複雜。

圖一 Replugin專案結構
圖一 Replugin專案結構

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

  • 二、ClassLoader基礎知識。
  • 三、Replugin專案原理和結構分析。
  • 四、Replugin的ClassLoader。
  • 五、Replugin的相關類介紹。
  • 六、Replugin的初始化。
  • 七、Replugin啟動Activity。

此處應有圖
此處應有圖

二、ClassLoader基礎知識

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

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

抽象基類ClassLoader
抽象基類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
RePluginCallbacks

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

hook ClassLoader
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(),看圖吧
PMF.init(),看圖吧

  PmBase,按照專案中的變數名 mPluginMgr,可以理解為外掛的管理者,它管理內部直接或間接的,管理著坑位分配、ClassLoader、外掛、程式、啟動\停止頁面的介面等,如下圖。

PmBase建立,還是看圖
PmBase建立,還是看圖

  PmBase 的初始化,也就是外掛的初始化,這裡會啟動各類程式,初始化各種預設外掛集合,為後續載入做準備。其中預設外掛和配置檔案的位置,一般預設是在 assert 的 plugins-builtin.json 和 "plugins" 資料夾下。

PmBase.init() 看圖看圖
PmBase.init() 看圖看圖

  接著PMF.callAttach() 其實就是 PmBase.callAttach()如下圖這裡開始真正載入外掛,初始化外掛的 PluginDexClassLoader 、載入外掛、初始化外掛環境和介面。其中在執行 p.load() 的時候,會通過 Plugind.callAppLocked() 建立外掛的 Application,並初始化。

PMF.callAttach() 看圖唄
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等的就不多做敘述了,不知道本文對你是否能有些幫助,歡迎留言討論。

最後說“一”句

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

其他推薦

注意到了嗎?最後的總是我!
注意到了嗎?最後的總是我!

相關文章