RePlugin插件化實踐

RePlugin的開源地址:https://github.com/Qihoo360/RePlugin
官方介紹:https://github.com/Qihoo360/RePlugin/blob/dev/README_CN.md
實現(xiàn)Demo:https://github.com/Jarrywell/RePluginDemo

背景

今年(2018年)的Google I/O大會上不僅發(fā)布了開發(fā)者熱捧的Jetpack組件,還發(fā)布了另一個大家不太注意卻是重量級的功能——Android App Bundle,它主要功能是讓App可以以功能模塊為粒度來發(fā)布迭代版本格仲,用戶在手機上首次使用時僅安裝一個基本的apk即可,其他功能模塊可按需去動態(tài)下載甸饱。以此方式來縮減初始安裝apk的大小和縮短下載安裝時間。只不過這個功能需要與Google Play配合使用仑濒,導(dǎo)致國內(nèi)開發(fā)者大都直接忽略了該功能叹话。

這里提到Android App Bundle主要是因為該功能跟去年在國內(nèi)流行的插件化開發(fā)很是接近,容易讓人聯(lián)想到這可能是官方提供的一個插件化方案(插件化要轉(zhuǎn)正了墩瞳?)驼壶。而且插件化在沉淀了一段時間后,大都相對比較成熟了(實現(xiàn)上還是存在差異)喉酌,甚至是一直為大家所詬病的穩(wěn)定性(需要Hook系統(tǒng)的類热凹,適配艱難)難題也已解決:一些框架已經(jīng)實現(xiàn)了只需要Hook一個點(僅僅Hook ClassLoader)的方案了(RePlugin),甚至還有宣稱完全不需要Hook的方案(Phantom)出現(xiàn)泪电。

剛好最近公司項目也在做apk大小的優(yōu)化般妙,需求點主要是由于接入了越來越多的第三方功能性的sdk導(dǎo)致項目臃腫不堪。因此想到使用插件化的方案來讓一些附加功能模塊實現(xiàn)動態(tài)加載相速,因此來補補插件化的課碟渺,總結(jié)一下實踐中碰到的問題。

插件化的應(yīng)用場景

迭代中使用插件化開發(fā)模式時和蚪,可使得項目具備如下幾個優(yōu)勢:

  • 項目可進行模塊化的拆分止状。使得宿主和插件之間更低的耦合度烹棉,以便實現(xiàn)模塊化開發(fā)(當(dāng)然組件化方案也是為了實現(xiàn)這種低耦合度的攒霹,只不過它關(guān)注的是編譯期的模塊化,而插件化關(guān)注的是運行時的模塊化)浆洗。
  • 提高開發(fā)效率催束。插件可單獨開發(fā)、編譯伏社、調(diào)試(特別是項目體積較大整體編譯需要花費一分鐘以上的時間時效果比較明顯)抠刺。
  • 實現(xiàn)熱修復(fù)功能塔淤。插件可單獨發(fā)布,使得線上BUG的解決可以達到”熱修復(fù)”的效果(有些需要重啟進程才可以)速妖。
  • 減小Apk的體積高蜂。用戶在安裝Apk時可以只下載安裝一個基本的apk(一些功能模塊在插件中),后續(xù)再按需下載插件罕容。

注:其實插件化的這幾種優(yōu)勢备恤,Android App Bundle也是實現(xiàn)了這幾個功能而已,可以說功能上很接近了锦秒。

目前插件化的基本實現(xiàn)原理

插件化的核心功能是App能在運行時動態(tài)的去加載和運行插件內(nèi)容露泊。但由于宿主和插件是完全分離的兩個Apk(分開編譯),那怎樣實現(xiàn)讓一個Apk(宿主)去加載另一個Apk(插件)的內(nèi)容呢旅择?怎樣讓插件的代碼(包括四大組件)運行起來呢惭笑?這就是插件化方案需要實現(xiàn)關(guān)鍵點。綜合來看生真,若要讓插件的特性和原生App盡可能保持一致的話沉噩,大致需要實現(xiàn)以下幾點才能達到目的:

  • 插件class的加載
    目前常見的幾個框架都使用了DexClassLoader來加載插件,因為DexClassLoader帶有一個optimizedDirectory目錄參數(shù)(這個路徑參數(shù)是用來保存解壓的dex文件)柱蟀,導(dǎo)致它天生可以加載外部的Jar屁擅、Apkdex。其實最開始使用DexClassLoader來加載其他class的方案仍然還是谷歌提供的思路(使用Multidex加載apk中的非主dex产弹,來解決方法數(shù)超過65535的問題)派歌,可以看出這里的思路就是借鑒了Multidex的經(jīng)驗來實現(xiàn)的。
    另外痰哨,class加載的結(jié)果一般存在兩種形式:一種是將插件的class合并到宿主中(宿主和插件共用一個ClassLoader)胶果,另一種是插件使用單獨的ClassLoader(宿主和插件不共用),這兩種形式各有各的優(yōu)勢和劣勢斤斧,后面再細說早抠。另外,宿主還要能加載到插件中的class(是實現(xiàn)插件的基本要求)撬讽,目前常見的有下面四種形式:
    1蕊连、就是上面提到的直接把插件中的類合并到宿主的ClassLoader中。(VirtualAPK的實現(xiàn))
    2游昼、Hook住宿主ApplicationmPackageInfo中負責(zé)類加載的PathClassLoader甘苍,將其替換為我們自定義的類加載器(RePluginClassLoader),其實它只是一個代理烘豌,最終的加載動作會路由到對應(yīng)插件的ClassLoader中去加載载庭。(RePlugin的方式)
    3、利用類加載器的雙親委派機制(在加載一個類時首先將其交給父加載器去加載,父加載器沒有找到類時才自己去加載)的特性:改變宿主App的ClassLoader委派鏈囚聚,將一個自定義的ClassLoader(MoClassLoader)嵌入到parent中靖榕,使得委派鏈由PathClassLoader->BootCalssLoader變?yōu)?PathClassLoader->MoClassLoader->BootCalssLoader(需要hook一下ClassLoaderparent成員變量),這樣通過宿主PathClassLoader去加載的類會最終交給MoClassLoader去加載了,MoClassLoader也是相當(dāng)與一個路由的作用顽铸。(MoPlugin的實現(xiàn))
    4茁计、不處理宿主與插件ClassLoader的關(guān)聯(lián),調(diào)用方手動指定ClassLoader去加載谓松。(Phantom的實現(xiàn))

  • 插件資源的加載
    即對插件Apk中 Resources對象的實例化簸淀,后續(xù)對插件資源的訪問都需要經(jīng)由這個Resources對象,這里的實現(xiàn)也有多種方式:有的是通過反射調(diào)用AssetManager.addAssetPath()將插件的目錄添加到插件資源對應(yīng)的AssetManager中毒返,然后以這個AssetManager手動創(chuàng)建一個新的Resources對象(VirtualAPK的實現(xiàn)方式租幕,需要Hook)、有的使用開放的API PackageManager.getPackageArchiveInfo()獲取PackageInfo拧簸,然后通過PackageManager.getResourcesForApplication(PackageInfo.applicationInfo)來讓PMS幫助創(chuàng)建Resources對象(RePlugin的實現(xiàn)方式劲绪,不需要Hook)。
    最終的Resources資源也存在兩種形式:一種是和宿主合并盆赤,另一種是插件獨立的Resources贾富,也各有優(yōu)缺點,這里可以參見RePlugin對資源合并和不合并帶來優(yōu)缺點的說明:詳細連接牺六。

  • 運行插件中的四大組件及生命周期
    這一步是插件化中的難點颤枪,而且要實現(xiàn)這一步上面兩步是必要的基礎(chǔ)(必須能加載到對應(yīng)的類和資源嘛),然而Android系統(tǒng)規(guī)定要運行的四大組件必須在Manifests文件中明確注冊才能夠運行起來(AMS會對注冊進行校驗淑际,啟動未注冊的組件會拋出異常)畏纲,因此如何讓動態(tài)加載的插件在Manifets中注冊成為了攔路虎。
    目前插件化框架基本上都使用了以占坑的方式預(yù)先在宿主的Manifests中埋入四大組件坑位春缕,在加載時將插件中的四大組件與坑位進行映射的方式來解決AMS對插件中四大組件的校驗問題(也有直接啟動坑位組件實現(xiàn)的--Phantom方案)盗胀。
    當(dāng)然最終四大組件的運行實現(xiàn)也各不相同,其中尤以Activity的實現(xiàn)最為復(fù)雜(它涉及到的屬性較多)锄贼,有的是Hook系統(tǒng)的IActivityManager或者Instrumentation來實現(xiàn)(VirtualAPK的實現(xiàn))票灰、有的是模擬系統(tǒng)AMS啟動Activity的方式來實現(xiàn)(先找坑位、再啟動對應(yīng)進程宅荤、最后在該進程中啟動插件Activity罩抗,RePlugin的實現(xiàn))贸呢、有的是直接啟動坑位然后在坑位的生命周期中處理插件的生命周期(Phantom的實現(xiàn))和屎。

  • 實現(xiàn)多進程(宿主和插件可能運行在不同的進程中妄辩,有跨進程交互的需求)
    由于android多進程的實現(xiàn)方式是通過在Manifests中注冊四大組件時顯式指定android:process=xx來實現(xiàn)的,而插件中的四大組件只能通過預(yù)埋在宿主中的坑位來映射加載琼了,這就給坑位的多進程預(yù)埋提出了更復(fù)雜的要求(插件中運行在哪個進程與坑位對應(yīng)起來逻锐,還要考慮啟動模式的組合)夫晌,因此大多數(shù)框架都不支持四大組件的多進程坑位(尤其是Activity的多進程坑位)雕薪。目前看到的只有RePlguin比較完美的實現(xiàn)了多進程(它是模擬了AMS啟動app的流程昧诱,在啟動組件前,先使用PluginProcessMain啟動映射的進程所袁,參見上一步說明)盏档。

  • 插件與宿主的交互,包括插件中普通類的調(diào)用燥爷、資源的使用
    雖然宿主和插件之間的形態(tài)是低耦合的蜈亩,但從產(chǎn)品的角度來看,模塊與模塊之間當(dāng)然應(yīng)該存在調(diào)用關(guān)系(不然怎么聯(lián)系起來呢前翎,這里指的是類之間的調(diào)用而不僅僅是四大組件的啟動)稚配,因此插件與宿主之間、插件與插件的仍然會有一些必要的耦合港华。
    另外道川,這里相互調(diào)用的便利程度就取決于前面步驟中插件的類和資源是否有與宿主合并了,因為要是合并了的話立宜,類和資源都在一個ClassLoaderResources中冒萄,這樣調(diào)用者便可以直接訪問了,只不過合并會導(dǎo)致一些類和資源的沖突問題橙数,因此有些框架并沒有選擇合并的方式尊流;如果不合并的話,在調(diào)用類或資源之前灯帮,就必須先去獲取插件對應(yīng)的ClassLoaderResources對象才能繼續(xù)調(diào)用崖技,這里便會增加使用的難度,特別是插件中使用了第三方的sdk時問題會更加嚴重(這里的實現(xiàn)一般會使用動態(tài)編譯去替換或者重寫ActivitygetResource()等函數(shù)實現(xiàn))钟哥。

  • 插件中so庫的調(diào)用
    安裝插件(一般是指將插件解壓到特定的目錄)時會將插件Apk進行解壓并釋放文件响疚。但如果涉及到so庫話,那該釋放哪種ABI類型的so庫呢瞪醋?這里涉及宿主進程是32位還是64位的判斷問題忿晕,因此插件化框架一般都是讀取宿主的ABI屬性來考慮插件的ABI屬性。導(dǎo)致這里會存在插件so庫可能與宿主不同ABI的so庫混用的可能(比如银受,宿主放的是64位的践盼,而插件放了32位的,則會出現(xiàn)混用的可能)宾巍,最終導(dǎo)致so的加載失敗咕幻。

由于項目選用的插件化框架是RePlugin(考察了現(xiàn)在仍在更新的幾個插件化方案與項目切合度做出的決定,主要原因是它僅Hook了一個點顶霞、并且支持多進程)肄程,因此下面的介紹均以RePlugin為示例锣吼,并附帶與其他框架的比較說明

簡單示例說明

在接入RePlugin時會發(fā)現(xiàn)它總共提供了4個庫分宿主和插件項目要分別接入蓝厌,下面先說明一下這幾個庫的功能玄叠,來大致了解它們在其中分別做了什么事情:

  • replugin-host-gradle: 宿主接入的gradle插件,主要任務(wù)是在編譯期間根據(jù)在build.gradle中配置的repluginHostConfig信息在Manifests中生成坑位組件信息拓提;動態(tài)生成RePluginHostConfig的配置類读恃;掃描項目assets中放置的內(nèi)置插件生成builtin.json
    注:該gradle插件沒有考慮到用戶會自定義Build Variant的情況或者在一個單獨的module中接入插件的情況代态,從接入過程來看這個情況還是比較普遍的寺惫,如果要適配這中情況只能將源碼下下來自己修改下。
  • replugin-host-lib:宿主接入的一個java sdk蹦疑,插件化核心功能都在這里實現(xiàn)西雀,負責(zé)框架的初始化、加載歉摧、啟動和管理插件等艇肴。
  • replugin-plugin-gradle:插件工程接入的gradle插件,主要功能是使用javassist在編譯期間動態(tài)去替換插件中的繼承基類判莉,如修改Activity的繼承豆挽、Provider的重定向等。
  • replugin-plugin-lib:插件工程接入的java sdk券盅,功能主要是通過反射調(diào)用宿主工程中replugin-host-lib的相關(guān)類(如RePlugin帮哈、RePluginInternal提供的接口,內(nèi)部實現(xiàn)都是反射)锰镀,以提供“雙向通信”的能力娘侍。

具體的接入細節(jié)步驟這里就不做過多介紹了,畢竟這不是一篇入門教程泳炉,且官方wiki已經(jīng)有非常詳細憾筏、明確的說明了,或者也可以參考我的Demo工程花鹅。這里主要是想記錄下實際接入過程中的使用的一些感想和閱讀源碼時的一些理解氧腰。下面的內(nèi)容都是假設(shè)宿主工程和插件工程都已經(jīng)配置好跑起來了的前提下介紹的。我們先來看一個簡單的啟動插件Activity的示例:

/**
 * 通過RePlugin提供的接口createIntent()創(chuàng)建一個指向插件中的activity的intent
 * 內(nèi)部實現(xiàn)就是創(chuàng)建了個ComponentName,只不過它的包名被插件名給替代了
 */
final String pluginName = "plugin1";
final String activityName = "com.test.android.plugin1.activity.InnerActivity";
Intent intent = RePlugin.createIntent(pluginName, activityName);

/**
 * 使用RePlugin提供的接口startActivity()來啟動插件activity
 * 若在指定的插件中沒有找到對應(yīng)的activity,則會回調(diào)
 * 到接口RePluginCallbacks.onPluginNotExistsForActivity()
 */
RePlugin.startActivity(MainActivity.this, intent);

相信大部分插件框架給的第一個示例都是這樣去啟動一個插件中的Activity來展示刨肃。不過通過這里的簡單示例我們能看出幾個要點:

  • 這里的startActivity()并沒有使用原生的Activity.startActivity()或者Context.startActivity()(VirtualAPK能直接調(diào)用)而是調(diào)用了RePlugin自己封裝的接口古拴。這里這樣實現(xiàn)的主要是因為RePlugin為了做到唯一Hook點而沒有像大部分框架的實現(xiàn)方式那樣去Hook系統(tǒng)的跳轉(zhuǎn)接口,所以只能退一步讓開發(fā)者去調(diào)用額外的接口去啟動插件了真友,雖然這里增加了學(xué)習(xí)成本黄痪,但大體的接口用法和原生是一致的(差別只是在創(chuàng)建ComponentName時使用插件名而不是平常的包名),而且還支持action的隱式啟動盔然。
  • 上述示例中展示的是在宿主中啟動插件中的Activity桅打,我們可以手動調(diào)用RePlugin封裝的startActivity()接口是嗜。但如果是在插件中啟動其他Activity(包括在其他插件中和其他進程中的Activity)呢?(RePlugin的宗旨是插件的開發(fā)要像原生開發(fā)一樣)或者是一個插件中接入了第三放sdk挺尾,然后sdk內(nèi)部有啟動Activity的需求鹅搪,我們沒法主動去調(diào)RePlugin的接口,該怎么適配這種情況呢潦嘶?RePlugin主要做了兩種情況的適配:
    第一種情況:如果插件是通過Activity.startActivity()啟動其他Activity的涩嚣,前面有提到過插件工程需要接入replugin-plugin-gradle插件崇众,他會在編譯期間去替換Activity的繼承關(guān)系為PluginActivity掂僵,它重寫了方法startActivity()來實現(xiàn)即便調(diào)用原生的方法也會給你轉(zhuǎn)向到RePlugin的方法:
PluginActivity

@Override
public void startActivity(Intent intent) {
    if (RePluginInternal.startActivity(this, intent)) {
        return;
    }
    super.startActivity(intent);
}

@Override
public void startActivityForResult(Intent intent, int requestCode) {
    if (RePluginInternal.startActivityForResult(this, intent, requestCode)) {
        return;
    }
    super.startActivityForResult(intent, requestCode);
}

RePluginInternalstartActivity() 方法通過反射最終還是調(diào)用到了RePlugin.startActivity()的實現(xiàn)方法。
第二種情況:如果是通過調(diào)用Context.startActivity()來啟動其他Activity的呢顷歌?這里的適配主要是在PluginContext中實現(xiàn)重寫startActivity()锰蓬,具體實現(xiàn)跟PluginActivity重寫方法大體是一樣的。為什么適配PluginContext的方法就能替換原生的方法呢眯漩?因為插件中的Context實例要么是通過Activity.getContext()獲取芹扭,要么是通過getApplicationContext()獲取的,只要這兩處地方拿到的ContextPluginContext就可以實現(xiàn)了赦抖,分別看下源碼舱卡,PluginActivity中替換Context的代碼是在attachBaseContext()中,這個方法會在ActivityonCreate()執(zhí)行之前就會調(diào)用:

PluginActivity
@Override
protected void attachBaseContext(Context newBase) {
    /**
     * 這里是通過反射到宿主中的Factory2.createActivityContext()去
     * 查詢獲取插件在加載是構(gòu)造的PluginContext對象
     */
    newBase = RePluginInternal.createActivityContext(this, newBase);
    super.attachBaseContext(newBase);
}

ApplicationContext的替換是在加載對應(yīng)插件時通過PluginApplicationClient的方法替換的:

PluginApplicationClient
private void callAppLocked() {
    //...
    /**
     * 創(chuàng)建插件對應(yīng)的Application實例
     */
    mApplicationClient = PluginApplicationClient.getOrCreate(
            mInfo.getName(), mLoader.mClassLoader, mLoader.mComponents, 
    mLoader.mPluginObj.mInfo);

    /**
     * 模擬AMS啟動app時队萤,會先調(diào)用Application的attachBaseContext()和onCreate()方法
     */
    if (mApplicationClient != null) {
        /**
         * 注意這里傳進去的的Context是在加載插件時創(chuàng)建的PluginContext
         */
        mApplicationClient.callAttachBaseContext(mLoader.mPkgContext);
        mApplicationClient.callOnCreate();
    }
}

其中mLoader.mPkgContext就是PluginContext轮锥。以上兩種情況的實現(xiàn)就做到了不需要Hook系統(tǒng)的方法就能實現(xiàn)啟動插件中Activity了。

  • 還有就是RePlugin的自定義方法startActivity()要做到坑位與插件Activity的映射啟動要尔,這里涉及到的東西比較多舍杜,比如:插件的加載、進程id的分配赵辕、坑位的分配等既绩,這里先不展開講了,后續(xù)再作說明还惠。

為什么RePlugin需要這個唯一Hook點

簡單說來饲握,它主要是處理四大組件(以Activity為例)的坑位替換問題:用已經(jīng)在Manifests中注冊了的坑位Activity去騙過AMS的校驗,然后在啟動流程后續(xù)階段具體實例化對應(yīng)Activity時去替換為坑位的組件(Instrumentation.newActivity()去實例化Activity)蚕键。我們看下Activity的啟動流程中校驗Manifests注冊和創(chuàng)建Activity的兩步:
第一步救欧、校驗Manifests的注冊:

Activity
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
    @Nullable Bundle options) {
    if (mParent == null) {
        //...
        /**
         * 通過Instrumentation.execStartActivity()啟動Activity
         */
        Instrumentation.ActivityResult ar =
            mInstrumentation.execStartActivity(
                this, mMainThread.getApplicationThread(), mToken, this,
                intent, requestCode, options);
        //...
    } else {
       //...
    }
}
Instrumentation
public ActivityResult execStartActivity(
    Context who, IBinder contextThread, IBinder token, String target,
    Intent intent, int requestCode, Bundle options) {
    //...
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);

        /**
         * 調(diào)用AMS去啟動對應(yīng)的Activity, 并返回啟動結(jié)果result
         */
        int result = ActivityManager.getService()
            .startActivity(whoThread, who.getBasePackageName(), intent,
                intent.resolveTypeIfNeeded(who.getContentResolver()),
                token, target, requestCode, 0, null, options);

        /**
         * 這里通過result檢測出錯的結(jié)果
         */
        checkStartActivityResult(result, intent);
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
    return null;
}

public static void checkStartActivityResult(int res, Object intent) {
    //..
    switch (res) {
        case ActivityManager.START_CLASS_NOT_FOUND:
            /**
             * 這里提示沒有在Manifest中注冊
             */
            if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
                throw new ActivityNotFoundException(
                    "Unable to find explicit activity class "
                        + ((Intent)intent).getComponent().toShortString()
                        + "; have you declared this activity in your AndroidManifest.xml?");
            throw new ActivityNotFoundException(
                "No Activity found to handle " + intent);
       //...
}

ActivityManager.getService().startActivity()的返回值result就是AMS的校驗結(jié)果,在下面函數(shù)checkStartActivityResult()中通過拋異常的方式反饋錯誤結(jié)果嚎幸。因此颜矿,只要保證走到這一步之前傳遞的Activity都是坑位Activity即可正常跑通。因此到該階段為止嫉晶,插件框架中傳遞的都是坑位信息骑疆。接下來看看startActivity()的后需階段田篇。

第二步、創(chuàng)建Activity

ActivityThread
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    //...
    ComponentName component = r.intent.getComponent();

    //...
    ContextImpl appContext = createBaseContextForActivity(r);
    Activity activity = null;
    try {
        /**
        * 在RePlugin中箍铭,這里獲取的ClassLoader已經(jīng)被替換成了RePluginClassLoader
        * newActivity()通過ClassLoader和ClassName構(gòu)建Activity的實例
        */
        java.lang.ClassLoader cl = appContext.getClassLoader();
        activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        r.intent.prepareToEnterProcess();
        if (r.state != null) {
            r.state.setClassLoader(cl);
        }
    } catch (Exception e) {
        if (!mInstrumentation.onException(activity, e)) {
            throw new RuntimeException(
                "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
        }
    }
    //...
    return activity;
}

其中mInstrumentation.newActivity()傳遞了一個ClassLoader,這個ClassLoader就是宿主App中的PathClassLoader泊柬,如果我們把它早早的替換成RePluginClassLoader,那下面的Activity加載最終就會走到我們自定義的RePluginClassLoader中去了:

Instrumentation
public Activity newActivity(ClassLoader cl, String className, Intent intent)
    throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    return (Activity)cl.loadClass(className).newInstance();
}

于是在這里我們便可以在RePluginClassLoader.loadClass()中通過某種映射關(guān)系替換掉坑位Activity實例化一個我們插件中的Activity诈火。具體實現(xiàn)見如下類:
attachBaseContext()中Hook宿主的PathClassLoader

//RePlugin
public static void attachBaseContext(Application app, RePluginConfig config) {
    //...
    PMF.init(app);
    //...
}

//PMF
public static final void init(Application application) {
    //...
    PatchClassLoaderUtils.patch(application);
}

//PatchClassLoaderUtils
public static boolean patch(Application application) {
    // 獲取Application的BaseContext (來自ContextWrapper)
    Context oBase = application.getBaseContext();

    Object oPackageInfo = ReflectUtils.readField(oBase, "mPackageInfo");

    // 獲取mPackageInfo.mClassLoader
    ClassLoader oClassLoader = (ClassLoader) ReflectUtils.readField(oPackageInfo, "mClassLoader");

    // 外界可自定義ClassLoader的實現(xiàn)兽赁,但一定要基于RePluginClassLoader類
    ClassLoader cl = RePlugin.getConfig().getCallbacks().createClassLoader(oClassLoader.getParent(), oClassLoader);

    // 將新的ClassLoader寫入mPackageInfo.mClassLoader
    ReflectUtils.writeField(oPackageInfo, "mClassLoader", cl);

    Thread.currentThread().setContextClassLoader(cl);
}

注:另一種方式就是利用ClassLoader的雙親委派模型將宿主的類加載器由PathClassLoader->BootCalssLoader 變?yōu)?PathClassLoader->MyClassLoader->BootCalssLoader。所有需要通過PathClassLoader加載的類都讓其父加載器MyClassLoader去加載也能達到目的冷守。(MoPlugin的實現(xiàn))

然后在后面去加載類時便能路由到RePluginClassLoader中處理刀崖,具體加載細節(jié)可以參看代碼中的注釋:

//RePluginClassLoader
//這里className是要替換的類
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    Class<?> c = null;
    /**
     * 這里最終調(diào)用PmBase.loadClass()去加載插件類
     */
    c = PMF.loadClass(className, resolve);
    if (c != null) {
        return c;
    }
    //...
    return super.loadClass(className, resolve);
}


//PmBase
final Class<?> loadClass(String className, boolean resolve) {

    /**
     * 通過坑位Activity找映射的插件Activity
     */
    if (mContainerActivities.contains(className)) {
        Class<?> c = mClient.resolveActivityClass(className);
        if (c != null) {
            return c;
        }
        //....
    }

    /**
     * 通過坑位Service找映射的插件Service
     */
    if (mContainerServices.contains(className)) {
        Class<?> c = loadServiceClass(className);
        if (c != null) {
            return c;
        }
        //...
    }

    /**
     * 通過坑位provider找映射的插件provider
     */
    if (mContainerProviders.contains(className)) {
        Class<?> c = loadProviderClass(className);
        if (c != null) {
            return c;
        }
        //...
    }

    /**
     * 通過動態(tài)注冊的類映射插件的類
     */
    DynamicClass dc = mDynamicClasses.get(className);
    if (dc != null) {
        final Context context = RePluginInternal.getAppContext();
        PluginDesc desc = PluginDesc.get(dc.plugin);
        //...
        Plugin p = loadAppPlugin(dc.plugin);
        if (p != null) {
            try {
                Class<?> cls = p.getClassLoader().loadClass(dc.className);
                //...
                return cls;
            } catch (Throwable e) {
            }
        }
        return dc.defClass;
    }
}

因此如果插件實現(xiàn)不需要進行坑位替換和映射的話,那么也可以不去做這個點的Hook操作拍摇,比如前面提到的那個Phantom框架就沒有Hook這里亮钦。

上面PmBase.loadClass()函數(shù)中還有一個比較重要的注意點——DynamicClass,它定義的是一個普通類(非四大組件)的映射關(guān)系充活,應(yīng)用場景是不能手動通過插件ClassLoader去加載類的場景(這里也是大部分框架沒有考慮到的地方)蜂莉,比如:插件中的自定義ViewFragment等需要在宿主的xml中使用混卵。使用方法如下:

/**
 * 定位到插件中要注冊類的位置(插件名+類名)創(chuàng)建一個ComponentName
 */
ComponentName target = RePlugin.createComponentName("plugin1", 
    "com.test.android.plugin1.fragment.Plugin1Demo1Fragment");
/**
 * 調(diào)用registerHookingClass()函數(shù)將需要替換的類與插件中的類做一個映射映穗,后面如果再來找目標類時
 * 則會去對應(yīng)插件中去找
 */
RePlugin.registerHookingClass("com.test.android.host.Plugin1Fragment", target, null);

/**
 * 這樣在xml中就能直接寫目標類了,比如這里的一個定義在xml中的fragment
 */
<fragment
    android:id="@+id/fragment"
    class="com.test.android.host.Plugin1Fragment"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1" />

以上代碼的說明其功能均是為了給插件的類與宿主做映射用的幕随。還有就是上面提到了組件坑位的映射和替換蚁滋,那具體RePlugin是怎么映射的呢?
其主要實現(xiàn)在PluginContainers類中實現(xiàn)合陵,由于這里實現(xiàn)最為復(fù)雜枢赔,使用文字描述簡化其過程:

  1. 請求分配坑位。
  2. 調(diào)度到常駐進程并通過組件的android:process=xx匹配到映射進程拥知,常駐進程此時再拉起一個Provider來啟動對應(yīng)新進程踏拜,并返回一個插件進程的Binder
  3. 插件進程啟動時從常駐進程加載登記表(坑和目標activity表)低剔。
  4. 插件進程從登記表中匹配坑位組件速梗。
  5. 請求者發(fā)起startActivity()請求,參數(shù)為坑位組件襟齿。

Service姻锁、Provider的處理

這兩個組件由于屬性較少(一般只涉及到多進程屬性android:process=xxx)且生命周期比較簡單,因此RePlugin對這兩個組件的實現(xiàn)采用了直接構(gòu)建對應(yīng)插件ServiceProvider)實例然后手動調(diào)用其生命周期函數(shù)猜欺。當(dāng)然為了適應(yīng)Android對app進程的管理(參考LMK策略)位隶,RePlugin也還是會在對應(yīng)的進程中運行一個坑位的Service,避免進程被系統(tǒng)誤殺开皿。接下來我們來看看startService()的啟動流程:


MainActivity
//demo啟動一個插件Service
Intent intent = RePlugin.createIntent("plugin1", "com.test.android.plugin1.service.Plugin1Service1");
PluginServiceClient.startService(MainActivity.this, intent);

上面的調(diào)用最終會通過binder執(zhí)行到Service的管理類PluginServiceServer中的startServiceLocked()涧黄,然后在其中會手動構(gòu)建Service對象并執(zhí)行其生命周期篮昧,最后啟動對應(yīng)進程的坑位Service防止系統(tǒng)誤殺:

//PluginServiceServer
ComponentName startServiceLocked(Intent intent, Messenger client) {
    intent = cloneIntentLocked(intent);
    ComponentName cn = intent.getComponent();

    /**
     * 這里構(gòu)造出插件Service實例
     */
    final ServiceRecord sr = retrieveServiceLocked(intent);

    /**
     * 這里最終調(diào)到installServiceLocked(),其中會手動調(diào)用Service的attachBaseContext(),onCreate()生命周期
     * 具體參見下面注釋說明
     */
    if (!installServiceIfNeededLocked(sr)) {
        return null;
    }

    /**
     * 從binder線程post到ui線程笋妥,去執(zhí)行Service的onStartCommand操作
     */
    Message message = mHandler.obtainMessage(WHAT_ON_START_COMMAND);
    Bundle data = new Bundle();
    data.putParcelable("intent", intent);
    message.setData(data);
    message.obj = sr;
    mHandler.sendMessage(message);

    return cn;
}

private boolean installServiceLocked(ServiceRecord sr) {
    // 通過ServiceInfo創(chuàng)建Service對象
    Context plgc = Factory.queryPluginContext(sr.plugin);

    ClassLoader cl = plgc.getClassLoader();


    // 構(gòu)建Service對象
    Service s;
    try {
        s = (Service) cl.loadClass(sr.serviceInfo.name).newInstance();
    } catch (Throwable e) {

    }

    // 只復(fù)寫Context懊昨,別的都不做
    try {
        /**
         * 手動調(diào)用Service的attachBaseContext()
         */
        attachBaseContextLocked(s, plgc);
    } catch (Throwable e) {
    }

    /**
     * 手動調(diào)用Service的onCreate()
     */
    s.onCreate();
    sr.service = s;

    // 開啟“坑位”服務(wù),防止進程被殺
    ComponentName pitCN = getPitComponentName();
    sr.pitComponentName = pitCN;
    startPitService(pitCN);
    return true;
}

Provider的處理也很簡單春宣,僅僅是通過替換操作的Uri參數(shù)酵颁,讓其命中對應(yīng)坑位進程的Provider,然后在對應(yīng)坑位進程的函數(shù)從Uri解析出對應(yīng)插件的Provider并手動執(zhí)行最終的操作:

MainActivity
測試Provider的demo
final String authorities = "com.android.test.host.demo.plugin1.TEST_PROVIDER";
Uri uri = Uri.parse("content://" + authorities + "/" + "test");

ContentValues cv = new ContentValues();
cv.put("name", "plugin1 demo");
cv.put("address", "beijing");

/**
 * 宿主操作插件中的provider時context必須要傳插件中的context
 */
Context pluginContext = RePlugin.fetchContext("plugin1");
final Uri result = PluginProviderClient.insert(pluginContext, uri, cv);
DLog.d(TAG, "provider insert result: " + result);

此時會調(diào)用到

//PluginProviderClient
public static Uri insert(Context c, Uri uri, ContentValues values) {
    Uri turi = toCalledUri(c, uri); //轉(zhuǎn)換為目標的uri
    /**
     * 這里使用轉(zhuǎn)換后的uri將會跳轉(zhuǎn)到對應(yīng)進程坑位的Provider
     */
    return c.getContentResolver().insert(turi, values);
}

//轉(zhuǎn)換邏輯
public static Uri toCalledUri(Context context, String plugin, Uri uri, int process) {
    /**
     * 根據(jù)process映射到對應(yīng)進程的的坑位Provider
     */
    String au;
    if (process == IPluginManager.PROCESS_PERSIST) {
        au = PluginPitProviderPersist.AUTHORITY;
    } else if (PluginProcessHost.isCustomPluginProcess(process)) {
        au = PluginProcessHost.PROCESS_AUTHORITY_MAP.get(process);
    } else {
        au = PluginPitProviderUI.AUTHORITY;
    }

    /**
     * 轉(zhuǎn)換為replugin格式的uri
     */
    // from => content://                                                  com.qihoo360.contacts.abc/people?id=9
    // to   => content://com.qihoo360.mobilesafe.Plugin.NP.UIP/plugin_name/com.qihoo360.contacts.abc/people?id=9
    String newUri = String.format("content://%s/%s/%s", au, plugin, uri.toString().replace("content://", ""));
    return Uri.parse(newUri);
}

最終會執(zhí)行對應(yīng)坑位Providerinsert()函數(shù):

//PluginPitProviderBase
public Uri insert(Uri uri, ContentValues values) {
    PluginProviderHelper.PluginUri pu = mHelper.toPluginUri(uri);
    if (pu == null) {
        return null;
    }
    /**
     * 通過PluginUri手動構(gòu)建運行時的ContentProvider
     */
    ContentProvider cp = mHelper.getProvider(pu);
    if (cp == null) {
        return null;
    }
    /**
     * 手動調(diào)用其insert函數(shù)
     */
    return cp.insert(pu.transferredUri, values);
}

廣播的處理

廣播的處理則更為簡單月帝,就是將插件中Manifests中注冊的靜態(tài)廣播變成在加載插件時手動注冊的動態(tài)廣播即可躏惋,下面的調(diào)用在加載插件時觸發(fā):

//Loader
final boolean loadDex(ClassLoader parent, int load) {

    /**
     * 這里加載插件出插件的四大組件信息
     */
    mComponents = new ComponentList(mPackageInfo, mPath, mPluginObj.mInfo);

    // 動態(tài)注冊插件中聲明的 receiver
    regReceivers();
}

private void regReceivers() throws android.os.RemoteException {
    if (mPluginHost != null) {
        mPluginHost.regReceiver(plugin, ManifestParser.INS.getReceiverFilterMap(plugin));
    }
}


//常駐進程的PmHostSvc
public void regReceiver(String plugin, Map rcvFilMap) throws RemoteException {
    HashMap<String, List<IntentFilter>> receiverFilterMap = (HashMap<String, List<IntentFilter>>) rcvFilMap;

    // 遍歷此插件中所有靜態(tài)聲明的 Receiver
    for (HashMap.Entry<String, List<IntentFilter>> entry : receiverFilterMap.entrySet()) {
        for (IntentFilter filter : filters) {
            int actionCount = filter.countActions();
            while (actionCount >= 1) {
                saveAction(filter.getAction(actionCount - 1), plugin, receiver);
                actionCount--;
            }

            // 注冊 Receiver
            mContext.registerReceiver(mReceiverProxy, filter);
        }
    }
}

注:上面的注冊動作是在插件加載時進行的。因此嫁赏,這就意味著必須要是使用過插件中的類或資源后(會觸發(fā)插件的加載)才能響應(yīng)插件中的靜態(tài)廣播其掂。RePlugin這么設(shè)計也還是符合按需加載的機制油挥,官方也給出了具體原因:鏈接地址

多進程的支持

通篇看一遍RePlugin源碼潦蝇,可以發(fā)現(xiàn)它花了特別大的篇幅來實現(xiàn)四大組件的多進程(基本上涉及到插件的內(nèi)容都與進程掛勾了),且還可以看到多處跨進程的binder通信(多多少少可以看到類似android中AMS管理四大組件的影子)深寥,前面提到四大組件的啟動攘乒、坑位等問題時特意沒過多的涉及多進程(東西太多),所以在這里統(tǒng)一梳理一下惋鹅。

為什么大部分插件化開源框架都有意避開了多進程的實現(xiàn)则酝?因為實現(xiàn)太過復(fù)雜,對坑位的預(yù)埋提出了更高的要求(預(yù)埋坑位的進程名需要與插件中未知的進程名進行映射)闰集,更重要的是還涉及到宿主中有多進程沽讹、插件中有多進程以及雙方進程間要通信等情況,導(dǎo)致要統(tǒng)一管理信息變的復(fù)雜了武鲁。于是很多框架為了更輕量級(RePlugin的源碼會比VirtualAPK的源碼多了好幾倍)都沒去實現(xiàn)爽雄。但RePlugin在wiki中有提到要讓app處處都能插件化的愿景,所以它就沒法逃避這個問題沐鼠。

那難點在哪里挚瘟?
1、坑位配置更加復(fù)雜饲梭。特別是activity本身就涉及到啟動模式乘盖、taskAffnity、主題等屬性的組合憔涉,現(xiàn)在多加入一個android:process=xxx的組合订框,坑位的數(shù)量成指數(shù)級增長了。
2兜叨、進程名稱是插件中Manifests中的組件屬性中定義的穿扳,需要與對應(yīng)坑位的進程進行映射(注意:這里要在Activity啟動之前完成)藤违。
3、由于坑位和插件內(nèi)容分布在多個進程中纵揍,對坑位和插件的管理涉及到了跨進程顿乒,這大大增加了復(fù)雜度。

一開始看覺得好像沒那么復(fù)雜泽谨,因為android:process屬性已經(jīng)配在了坑位上了璧榄,那我們直接啟動對應(yīng)坑位組件不就運行在對應(yīng)的進程了嗎(原生機制)?但回頭一想吧雹,其實不然骨杂,插件組件啟動前必須要先映射坑位,那到底映射哪一個坑位呢(那么多進程)雄卷,且統(tǒng)一管理這些坑位映射關(guān)系還涉及到進程的管理(因為坑位分配涉及到進程分配)這無形中就增加了附加難度(相當(dāng)與AMS對四大組件的管理)搓蚪。

RePlugin實現(xiàn):在app啟動時(主進程)會拉起一個常駐進程(類似與系統(tǒng)的ActivityManagerService對應(yīng)的進程),后續(xù)涉及插件的相關(guān)機制都去通過binder調(diào)用常駐進程丁鹉,插件信息信息保存在這個常駐進程中妒潭,并統(tǒng)一管理和分配(這樣才能保持一致性)。然后當(dāng)需要啟動一個插件組件(如Activity)時揣钦,先使用進程屬性(android:process=xxx)提前匹配將要運行的進程名雳灾,然后常駐進程以該進程名為參數(shù)啟動一個對應(yīng)進程的Provider(沒有實際作用,僅僅是為了拉起一個新進程并返回一個Binder對象)冯凹,這樣便可以在新進程中執(zhí)行Application的生命周期(attachBaseContext()谎亩、onCreate())了,而這里是我們在新進程中初始化插件的入口宇姚,于是我們便可以在啟動插件組件之前先啟動新進程了(這是因為常駐進程對多進程進行管理匈庭,需要提前建立兩個進程的通信通道并同步一些插件信息,一切準備就緒后再啟動對應(yīng)坑位組件)浑劳。

先來看看demo中動態(tài)生成坑位信息阱持,它在gradle配置中指定了3個進程,對activity呀洲、provider紊选、service生成的坑位信息如下(一部分),p0~p2就是需要去映射的進程名:

//activity的多進程坑位道逗,可以看到同一屬性的activity有3個進程(p0兵罢、p1、p2)對應(yīng)的坑位
<activity android:name='com.android.test.host.demo.loader.a.ActivityN1NRNTS0' android:configChanges='keyboard|keyboardHidden|orientation|screenSize' android:exported='false' android:screenOrientation='portrait' android:theme='@style/Theme.AppCompat' />
<activity android:name='com.android.test.host.demo.loader.a.ActivityP0NRNTS0' android:configChanges='keyboard|keyboardHidden|orientation|screenSize' android:exported='false' android:screenOrientation='portrait' android:theme='@style/Theme.AppCompat' android:process=':p0' />
<activity android:name='com.android.test.host.demo.loader.a.ActivityP1NRNTS0' android:configChanges='keyboard|keyboardHidden|orientation|screenSize' android:exported='false' android:screenOrientation='portrait' android:theme='@style/Theme.AppCompat' android:process=':p1' />
<activity android:name='com.android.test.host.demo.loader.a.ActivityP2NRNTS0' android:configChanges='keyboard|keyboardHidden|orientation|screenSize' android:exported='false' android:screenOrientation='portrait' android:theme='@style/Theme.AppCompat' android:process=':p2' />

//provider坑位滓窍,用于拉活對應(yīng)進程
<provider android:name='com.qihoo360.replugin.component.process.ProcessPitProviderP0' android:authorities='com.android.test.host.demo.loader.p.mainN100' android:process=':p0' android:exported='false' />
<provider android:name='com.qihoo360.replugin.component.process.ProcessPitProviderP1' android:authorities='com.android.test.host.demo.loader.p.mainN99' android:process=':p1' android:exported='false' />
<provider android:name='com.qihoo360.replugin.component.process.ProcessPitProviderP2' android:authorities='com.android.test.host.demo.loader.p.mainN98' android:process=':p2' android:exported='false' />

//常駐進程provider
<provider android:name='com.qihoo360.replugin.component.process.ProcessPitProviderPersist' android:authorities='com.android.test.host.demo.loader.p.main' android:exported='false' android:process=':replugin' />

//service坑位
<service android:name='com.qihoo360.replugin.component.service.server.PluginPitServiceP0' android:process=':p0' android:exported='false' />
<service android:name='com.qihoo360.replugin.component.service.server.PluginPitServiceP1' android:process=':p1' android:exported='false' />
<service android:name='com.qihoo360.replugin.component.service.server.PluginPitServiceP2' android:process=':p2' android:exported='false' />

代碼中涉及到進程管理的類:
PmBase: 每個進程都會實例化這個類卖词,但是內(nèi)部實現(xiàn)會區(qū)分進程走到不同的分支。
PmHostSvc: 僅運行在常駐進程中(Service端),統(tǒng)一管理一切此蜈,并通過binder向其他進程(Client端)提供訪問接口即横。
PluginProcessPer:每個進程都有實例(Service端),并將一個binder注冊到常駐進程PmHostSvc(Client端)裆赵,它相當(dāng)于是插件進程與常駐進程的通信通道东囚。
PluginProcessMain: 沒有具體的實例,內(nèi)部都是靜態(tài)方法战授,只是提供其他進程與常駐進程進行交互的接口页藻,最重要的接口就是connectToHostSvc(),將兩個進程連接起來并同步一些信息植兰。
PluginProcessHost: 沒有具體的實例份帐,內(nèi)部使用了靜態(tài)變量保存了一些進程參數(shù)的初始值。
ProcessPitProviderPersist楣导、ProcessPitProviderUI废境、ProcessPitProviderP0ProcessPitProviderP1筒繁、ProcessPitProviderP2:這幾個provider就是在啟動坑位前先拉起噩凹,讓對應(yīng)進程先起來的作用。

我們來看看RePlugin中進程啟動時的流程膝晾,主要分兩種情況:

  • 主進程的啟動和常駐進程的啟動
    主進程啟動是用戶進入app時啟動的第一個進程屬于主動啟動栓始,常駐進程的啟動則是由主進程(也包括其他非常駐進程)啟動后帶起來的,是被動啟動血当,下面看下這兩個進程啟動時的流程:

在Application.attachBaseContext()中調(diào)用,主要是初始化PmBase:

//RePlugin
public static void attachBaseContext(Application app, RePluginConfig config) {
    PMF.init(app);
}

上面方法最終調(diào)用到PmBaseinit()方法禀忆,這里會區(qū)分是否是常駐進程啟動(服務(wù)端)還是非常駐進程(客戶端)啟動臊旭,分別對客戶端和服務(wù)端進行初始化,其中initForClient()會去拿常駐進程中PmHostSvc(如果常駐進程沒有啟動則帶起)的aidl接口箩退,以該接口建立連接离熏。這里一定是客戶端先啟動(app的主進程是客戶端),因此常駐進程是后啟動的:

//PmBase
void init() {
    if (IPC.isPersistentProcess()) {
        // 初始化“Server”所做工作戴涝,主要實例化PmHostSvc
        initForServer();
    } else {
        // 連接到Server
        initForClient(); 
    }
}
private final void initForClient() {
    // 1. 先嘗試連接
    PluginProcessMain.connectToHostSvc();
}

這里通過手拉起一個運行在常駐進程中的Provider滋戳,這樣常駐進程就起來了,然后就進入了常駐進程的attachBaseContext()->PmBase.ini()->initForServer()創(chuàng)建PmHostSvc最終返回給client:
Provider是在Manifests中的坑位啥刻,注意運行在常駐進程(android:process=':replugin'冀续,Demo中指定了常駐進程的名字為replugin):

//常駐進程的provider,注意android:process屬性
<provider android:name='com.qihoo360.replugin.component.process.ProcessPitProviderPersist' 
android:authorities='com.android.test.host.demo.loader.p.main' android:exported='false' 
android:process=':replugin' />
//PluginProcessMain
static final void connectToHostSvc() {
    IBinder binder = PluginProviderStub.proxyFetchHostBinder(context);
    sPluginHostRemote = IPluginHost.Stub.asInterface(binder);
}

//PluginProviderStub
private static final IBinder proxyFetchHostBinder(Context context, String selection) {
    Uri uri = ProcessPitProviderPersist.URI; //com.android.test.host.demo.loader.p.main
    //PROJECTION_MAIN = = {"main"};
    cursor = context.getContentResolver().query(uri, PROJECTION_MAIN, selection, null, null);
}
//ProcessPitProviderPersist
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
    sInvoked = true;
    return PluginProviderStub.stubMain(uri, projection, selection, selectionArgs, sortOrder);
}

//PluginProviderStub
public static final Cursor stubMain(Uri uri, String[] projection, String selection, String[] selectionArgs, 
    String sortOrder) {
    
    if (SELECTION_MAIN_BINDER.equals(selection)) {
        return BinderCursor.queryBinder(PMF.sPluginMgr.getHostBinder());
    }
}

final IBinder getHostBinder() {
    return mHostSvc; //PmHostSvc
}

//BinderCursor
public static final Cursor queryBinder(IBinder binder) {
    return new BinderCursor(PluginInfo.QUERY_COLUMNS, binder);
}
  • 啟動插件組件時帶起插件坑位進程
    這里的插件進程是在啟動一個插件組件(聲明了進程名)時觸發(fā)的瀑梗,我們以上面那一節(jié)啟動插件中的Activity的示例為例來看下插件進程啟動的流程:
    RePlugin提供的啟動入口:
//RePlugin
public static boolean startActivity(Context context, Intent intent) {
    ComponentName cn = intent.getComponent();
    String plugin = cn.getPackageName();
    String cls = cn.getClassName();
    return Factory.startActivityWithNoInjectCN(context, intent, plugin, cls, IPluginManager.PROCESS_AUTO);
}

最終調(diào)用到PluginLibraryInternalProxy.startActivity()接口,這一步中的loadPluginActivity()是關(guān)鍵,會去觸發(fā)加載插件顺献、進程啟動、坑位映射等核心操作:

//PluginLibraryInternalProxy
public boolean startActivity(Context context, Intent intent, String plugin, String activity, int process, 
    boolean download) {

    /**
     * 這一步去加載插件、啟動進程、映射坑位(核心)
     */
    ComponentName cn = mPluginMgr.mLocal.loadPluginActivity(intent, plugin, activity, process);

    // 將Intent指向到“坑位”扬虚。這樣:
    // from:插件原Intent
    // to:坑位Intent
    intent.setComponent(cn);

    //調(diào)用系統(tǒng)接口啟動坑位Activity
    context.startActivity(intent);

    return true;
}

下面看他的具體實現(xiàn),具體步驟參見注釋:

//PluginCommImpl
public ComponentName loadPluginActivity(Intent intent, String plugin, String activity, int process) {
    ActivityInfo ai = null;
    String container = null;
    PluginBinderInfo info = new PluginBinderInfo(PluginBinderInfo.ACTIVITY_REQUEST);

    try {
        // 獲取 ActivityInfo
        ai = getActivityInfo(plugin, activity, intent);

        // 根據(jù) activity 的 processName球恤,選擇進程 ID 標識
        if (ai.processName != null) {
            process = PluginClientHelper.getProcessInt(ai.processName);
        }

        // 容器選擇(啟動目標進程)
        IPluginClient client = MP.startPluginProcess(plugin, process, info);

        // 遠程分配坑位
        container = client.allocActivityContainer(plugin, process, ai.name, intent);

    } catch (Throwable e) {
    }

    return new ComponentName(IPC.getPackageName(), container);
}

然后是調(diào)用MP.startPluginProcess()啟動進程辜昵,最終調(diào)用aidl調(diào)用到常駐進程的PmHostSvc的接口:

public static final IPluginClient startPluginProcess(String plugin, int process, PluginBinderInfo info) {
    return PluginProcessMain.getPluginHost().startPluginProcess(plugin, process, info);
}

最終內(nèi)部則是調(diào)用PmBase.startPluginProcessLocked()接口去啟動進程,其步驟跟啟動常駐進程原理是一致的咽斧,還是通過啟動對應(yīng)進程的Provider來最終觸發(fā)新進程Application.attachBaseContext()的執(zhí)行路鹰,便有進入了框架的初始化流程:

//PmBase
final IPluginClient startPluginProcessLocked(String plugin, int process, PluginBinderInfo info) {
    // 啟動
    boolean rc = PluginProviderStub.proxyStartPluginProcess(mContext, index);

    return client;
}

//PluginProviderStub
static final boolean proxyStartPluginProcess(Context context, int index) {
    //
    ContentValues values = new ContentValues();
    values.put(KEY_METHOD, METHOD_START_PROCESS);
    values.put(KEY_COOKIE, PMF.sPluginMgr.mLocalCookie);
    Uri uri = context.getContentResolver().insert(ProcessPitProviderBase.buildUri(index), values);

    return true;
}

查看Manifests中的坑位信息如下,注意android:process=':p0'表示的進程名:

<provider android:name='com.qihoo360.replugin.component.process.ProcessPitProviderP0'
android:authorities='com.android.test.host.demo.loader.p.mainN100' android:process=':p0' 
android:exported='false' />

就這樣啟動了一個新進程了J粘晋柱!一直跟過來其實并沒有發(fā)現(xiàn)什么新技術(shù),還是套用了四大組件的啟動+binder就完成的诵叁,但是很巧妙雁竞。

資源讀取

RePlugin中Resources資源在宿主和插件中是獨立開來的。因此拧额,宿主讀取插件的資源和插件讀取宿主的資源都需要先獲取對方的Resources對象碑诉,然后再從該Resources對象中去獲取。RePlugin提供接口:

//RePlugin
public static Resources fetchResources(String pluginName) {
    return Factory.queryPluginResouces(pluginName);
}
//通過插件的Context.getResources()也可以
public static Context fetchContext(String pluginName) {
    return Factory.queryPluginContext(pluginName);
}

由于資源id是在插件中的侥锦,因此不能直接通過R.id等直接來引用进栽,Resources提供了一個按資源名稱和類型來讀取資源的接口getIdentifier(),其定義如下:

/**
 * @param name The name of the desired resource.
 * @param defType Optional default resource type to find, if "type/" is
 *                not included in the name.  Can be null to require an
 *                explicit type.
 * @param defPackage Optional default package to find, if "package:" is
 *                   not included in the name.  Can be null to require an
 *                   explicit package.
 *
 * @return int The associated resource identifier.  Returns 0 if no such
 *         resource was found.  (0 is not a valid resource ID.)
 */
public int getIdentifier(String name, String defType, String defPackage) {
    return mResourcesImpl.getIdentifier(name, defType, defPackage);
}

因此恭垦,讀取插件中的資源可以使用該接口實現(xiàn)快毛,參數(shù)定義參見上述的定義,下面是讀取drawable示例:

/**
 * 獲取插件的Resources對象(觸發(fā)插件的加載)
 */
Resources resources = RePlugin.fetchResources("plugin2");
/**
 * 通過resource的getIdentifier()接口獲取對應(yīng)資源的id(參數(shù)參考上面的定義)
 */
final int id = resources.getIdentifier("test_plugin2_img", "drawable",
        "com.test.android.plugin2");
if (id != 0) {
    /**
     * 通過id去讀取真正的資源文件
     */
    final Drawable drawable = resources.getDrawable(id);
    if (drawable != null) {
        mPluginImageView.setImageDrawable(drawable);
    }
}

讀取layout的示例:

Resources resources = RePlugin.fetchResources("plugin2");
id = resources.getIdentifier("layout_test_plugin", "layout",
        "com.test.android.plugin2");
if (id != 0) {
    ViewGroup parent = findViewById(R.id.id_layout_plugin);
    XmlResourceParser parser = resources.getLayout(id);

    /**
     * 通過XmlResourceParser去加載布局番挺,測試結(jié)果布局中的資源仍不能加載
     */
    View result = getLayoutInflater().inflate(parser, parent);

    /**
     * 這種方式也不能加載唠帝,會去宿主中找
     */
    //View result = getLayoutInflater().inflate(id, parent);
}

so庫的支持

查看源碼發(fā)現(xiàn)RePlugin對so庫的支持其實并沒有做額外的處理,僅僅是在安裝插件(加壓插件包)時讀取一下宿主的ABI值玄柏,然后再根據(jù)宿主的ABI去釋放插件對應(yīng)的libs目錄文件襟衰。具體邏輯都在PluginNativeLibsHelper文件中了,關(guān)鍵函數(shù)如下所示(可以參看其中的注釋):

//PluginNativeLibsHelper
// 根據(jù)Abi來獲取需要釋放的SO在壓縮包中的位置
private static String findSoPathForAbis(Set<String> soPaths, String soName) {

    // 若主程序用的是64位進程粪摘,則所屬的SO必須只拷貝64位的瀑晒,否則會出異常。32位也是如此
    // 問:如果用戶用的是64位處理器徘意,宿主沒有放任何SO苔悦,那么插件會如何?
    // 答:宿主在被安裝時映砖,系統(tǒng)會標記此為64位App间坐,則之后的SO加載則只認64位的
    // 問:如何讓插件支持32位?
    // 答:宿主需被標記為32位才可以≈袼危可在宿主App中放入任意32位的SO(如放到libs/armeabi目錄下)即可劳澄。

    // 獲取指令集列表
    boolean is64 = VMRuntimeCompat.is64Bit();
    String[] abis;
    if (is64) {
        abis = BuildCompat.SUPPORTED_64_BIT_ABIS;
    } else {
        abis = BuildCompat.SUPPORTED_32_BIT_ABIS;
    }

    // 開始尋找合適指定指令集的SO路徑
    String soPath = findSoPathWithAbiList(soPaths, soName, abis);
    
    return soPath;
}

另外,雖然插件最終能解析對應(yīng)的libs目錄蜈七,但也存在宿主和插件中so文件ABI屬性不一致的情況秒拔,這里官方也給出了詳細介紹:插件so庫ABI說明

而宿主的ABI屬性的判斷條件則比較復(fù)雜了飒硅,但這里不是插件框架的范疇砂缩,放一篇介紹的比較流暢的文章鏈接:Android的so文件加載機制詳解

其他

  • Phantom
    這個方案號稱是唯一零Hook的占坑方案,翻看了一遍源碼它確實做到了零Hook點(就是相比RePlugin要Hook住app的PathClassLoader三娩,它不需要Hook)庵芭,RePlugin要Hook住PathClassLoader只是為了在加載插件中的四大組件時去替換為坑位信息(這里還只能在ClassLoader中去做,否則就只能去Hook AMS了)雀监,Phantom方案就是看透了這點:干脆就直接啟動坑位組件双吆,并將插件中的具體組件信息通過Intent傳遞到坑位中去構(gòu)建一個運行時的插件組件(如:Plugin1Activity),然后在坑位組件的生命周期中手動去調(diào)用插件組件的生命周期会前,坑位組件相當(dāng)于是一個代理(其實在RePlugin中Service組件的實現(xiàn)就是這種方式)好乐,其余的實現(xiàn)大體跟RePlugin差不多,相當(dāng)于是RePlugin的簡化版瓦宜,也仍沒有進程的概念蔚万,所有組件只能是主進程。

  • MoPlugin
    這是公司內(nèi)部自研的插件化方案临庇,雖然是插件化的模式反璃,但感覺它更傾向于是簡單的熱更新實現(xiàn),它主要功能是將一些對外不變的接口類(包名不能變苔巨、函數(shù)接口不能去刪減版扩,可以增加)進行插件化的改造,使得那些需要升級更新的類(使用一個注解來標明)和資源能從插件中動態(tài)的加載侄泽。可以看出它傾向于對某些不常變化的東西進行更新(其實它初衷就是用來實現(xiàn)SDK內(nèi)部插件的框架)蜻韭。
    實現(xiàn)方式:類的加載前面也有提到過悼尾,是利用雙親委派的機制在原有的加載鏈路中插入一個MoClassLoader來實現(xiàn);加載后的資源最終合并到宿主的Resources中(這里要合并是因為這些插件細節(jié)屬于SDK內(nèi)部邏輯了肖方,不便于暴露給調(diào)用方)闺魏;不需要埋坑位(有固定不變的前提)。

總結(jié)

梳理下來發(fā)現(xiàn)俯画,其實插件化并沒有引入什么高深的新技術(shù)析桥,而且實現(xiàn)下來無非就是那么幾個點:類的加載、資源的加載,坑位處理等泡仗,只是不同的框架有不同的實現(xiàn)而已埋虹。
比較復(fù)雜的地方是需要你對AMS的工作流程要比較清楚,特別對Hook的關(guān)鍵點娩怎,要能抓住其來龍去脈搔课;另外就是需要對apk的安裝流程有整體的認識(插件的安裝類似apk的安裝,即將文件進行釋放和解析)截亦,后續(xù)應(yīng)多了解這兩個流程的實現(xiàn)爬泥。

參考文獻

https://github.com/Qihoo360/RePlugin
https://github.com/Qihoo360/RePlugin/wiki/%E9%AB%98%E7%BA%A7%E8%AF%9D%E9%A2%98
http://www.reibang.com/p/74a70dd6adc9
https://blog.csdn.net/yulong0809/article/details/78428247

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市崩瓤,隨后出現(xiàn)的幾起案子袍啡,更是在濱河造成了極大的恐慌,老刑警劉巖却桶,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件境输,死亡現(xiàn)場離奇詭異,居然都是意外死亡肾扰,警方通過查閱死者的電腦和手機畴嘶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來集晚,“玉大人窗悯,你說我怎么就攤上這事⊥蛋危” “怎么了蒋院?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長莲绰。 經(jīng)常有香客問我欺旧,道長,這世上最難降的妖魔是什么蛤签? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任辞友,我火速辦了婚禮,結(jié)果婚禮上震肮,老公的妹妹穿的比我還像新娘称龙。我一直安慰自己,他們只是感情好戳晌,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布鲫尊。 她就那樣靜靜地躺著,像睡著了一般沦偎。 火紅的嫁衣襯著肌膚如雪疫向。 梳的紋絲不亂的頭發(fā)上咳蔚,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天,我揣著相機與錄音搔驼,去河邊找鬼谈火。 笑死,一個胖子當(dāng)著我的面吹牛匙奴,可吹牛的內(nèi)容都是我干的堆巧。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼泼菌,長吁一口氣:“原來是場噩夢啊……” “哼谍肤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起哗伯,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤荒揣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后焊刹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體系任,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年虐块,在試婚紗的時候發(fā)現(xiàn)自己被綠了俩滥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡贺奠,死狀恐怖霜旧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情儡率,我是刑警寧澤挂据,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站儿普,受9級特大地震影響崎逃,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜眉孩,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一个绍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧浪汪,春花似錦障贸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽涩维。三九已至殃姓,卻和暖如春袁波,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蜗侈。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工篷牌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人踏幻。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓枷颊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親该面。 傳聞我的和親對象是個殘疾皇子夭苗,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 青山綠水瓊花艷, 臨澤新顏萬里程隔缀。 制種率先民致富题造, 葡萄產(chǎn)業(yè)板橋宏。 丹霞雄起游人旺猾瘸, 凹凸儲量世聞名界赔。 大美棗...
    撫彝牛人閱讀 883評論 2 1
  • 當(dāng)你的生命還有最后四個月淮悼,你會做什么? 從小到大揽思,都是在別人的選擇中度過袜腥,也一直活在別人的期望之中,并不斷被當(dāng)做“...
    端木山閱讀 251評論 0 0
  • 08116 鄒公子 在《幸福的種子》中松居直先生一直在反復(fù)強調(diào)一個觀點——那就是繪本“無用論”。 任何一個妄圖用繪...
    自制力才是超能力閱讀 470評論 8 5
  • github 倉庫 我們可以再網(wǎng)絡(luò)上創(chuàng)建一個倉庫 我們可以創(chuàng)建一個空的倉庫QQ20150721-5@2x.png ...
    Yanni_L閱讀 341評論 1 1