1. 程式人生 > >外掛化系列開發之九--Android 全面外掛化 RePlugin 流程與原始碼解析

外掛化系列開發之九--Android 全面外掛化 RePlugin 流程與原始碼解析

 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 中的 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);
RePluginCallbacksRePluginCallbacks

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

hook ClassLoaderhook 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 初始化開始。如下圖,這裡主要是配置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 。

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經歷了 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等的就不多做敘述了,不知道本文對你是否能有些幫助,歡迎留言討論。

最後說“一”句

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

自此外掛化系列已完結,後續有變化持續更新。