RePlugin插件化實踐

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

背景

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

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

剛好最近公司項目也在做apk大小的優化,需求點主要是由于接入了越來越多的第三方功能性的sdk導致項目臃腫不堪。因此想到使用插件化的方案來讓一些附加功能模塊實現動態加載,因此來補補插件化的課,總結一下實踐中碰到的問題。

插件化的應用場景

迭代中使用插件化開發模式時,可使得項目具備如下幾個優勢:

  • 項目可進行模塊化的拆分。使得宿主和插件之間更低的耦合度,以便實現模塊化開發(當然組件化方案也是為了實現這種低耦合度的,只不過它關注的是編譯期的模塊化,而插件化關注的是運行時的模塊化)。
  • 提高開發效率。插件可單獨開發、編譯、調試(特別是項目體積較大整體編譯需要花費一分鐘以上的時間時效果比較明顯)。
  • 實現熱修復功能。插件可單獨發布,使得線上BUG的解決可以達到”熱修復”的效果(有些需要重啟進程才可以)。
  • 減小Apk的體積。用戶在安裝Apk時可以只下載安裝一個基本的apk(一些功能模塊在插件中),后續再按需下載插件。

注:其實插件化的這幾種優勢,Android App Bundle也是實現了這幾個功能而已,可以說功能上很接近了。

目前插件化的基本實現原理

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

  • 插件class的加載
    目前常見的幾個框架都使用了DexClassLoader來加載插件,因為DexClassLoader帶有一個optimizedDirectory目錄參數(這個路徑參數是用來保存解壓的dex文件),導致它天生可以加載外部的JarApkdex。其實最開始使用DexClassLoader來加載其他class的方案仍然還是谷歌提供的思路(使用Multidex加載apk中的非主dex,來解決方法數超過65535的問題),可以看出這里的思路就是借鑒了Multidex的經驗來實現的。
    另外,class加載的結果一般存在兩種形式:一種是將插件的class合并到宿主中(宿主和插件共用一個ClassLoader),另一種是插件使用單獨的ClassLoader(宿主和插件不共用),這兩種形式各有各的優勢和劣勢,后面再細說。另外,宿主還要能加載到插件中的class(是實現插件的基本要求),目前常見的有下面四種形式:
    1、就是上面提到的直接把插件中的類合并到宿主的ClassLoader中。(VirtualAPK的實現)
    2、Hook住宿主ApplicationmPackageInfo中負責類加載的PathClassLoader,將其替換為我們自定義的類加載器(RePluginClassLoader),其實它只是一個代理,最終的加載動作會路由到對應插件的ClassLoader中去加載。(RePlugin的方式)
    3、利用類加載器的雙親委派機制(在加載一個類時首先將其交給父加載器去加載,父加載器沒有找到類時才自己去加載)的特性:改變宿主App的ClassLoader委派鏈,將一個自定義的ClassLoader(MoClassLoader)嵌入到parent中,使得委派鏈由PathClassLoader->BootCalssLoader變為 PathClassLoader->MoClassLoader->BootCalssLoader(需要hook一下ClassLoaderparent成員變量),這樣通過宿主PathClassLoader去加載的類會最終交給MoClassLoader去加載了,MoClassLoader也是相當與一個路由的作用。(MoPlugin的實現)
    4、不處理宿主與插件ClassLoader的關聯,調用方手動指定ClassLoader去加載。(Phantom的實現)

  • 插件資源的加載
    即對插件Apk中 Resources對象的實例化,后續對插件資源的訪問都需要經由這個Resources對象,這里的實現也有多種方式:有的是通過反射調用AssetManager.addAssetPath()將插件的目錄添加到插件資源對應的AssetManager中,然后以這個AssetManager手動創建一個新的Resources對象(VirtualAPK的實現方式,需要Hook)、有的使用開放的API PackageManager.getPackageArchiveInfo()獲取PackageInfo,然后通過PackageManager.getResourcesForApplication(PackageInfo.applicationInfo)來讓PMS幫助創建Resources對象(RePlugin的實現方式,不需要Hook)。
    最終的Resources資源也存在兩種形式:一種是和宿主合并,另一種是插件獨立的Resources,也各有優缺點,這里可以參見RePlugin對資源合并和不合并帶來優缺點的說明:詳細連接

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

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

  • 插件與宿主的交互,包括插件中普通類的調用、資源的使用
    雖然宿主和插件之間的形態是低耦合的,但從產品的角度來看,模塊與模塊之間當然應該存在調用關系(不然怎么聯系起來呢,這里指的是類之間的調用而不僅僅是四大組件的啟動),因此插件與宿主之間、插件與插件的仍然會有一些必要的耦合。
    另外,這里相互調用的便利程度就取決于前面步驟中插件的類和資源是否有與宿主合并了,因為要是合并了的話,類和資源都在一個ClassLoaderResources中,這樣調用者便可以直接訪問了,只不過合并會導致一些類和資源的沖突問題,因此有些框架并沒有選擇合并的方式;如果不合并的話,在調用類或資源之前,就必須先去獲取插件對應的ClassLoaderResources對象才能繼續調用,這里便會增加使用的難度,特別是插件中使用了第三方的sdk時問題會更加嚴重(這里的實現一般會使用動態編譯去替換或者重寫ActivitygetResource()等函數實現)。

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

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

簡單示例說明

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

  • replugin-host-gradle: 宿主接入的gradle插件,主要任務是在編譯期間根據在build.gradle中配置的repluginHostConfig信息在Manifests中生成坑位組件信息;動態生成RePluginHostConfig的配置類;掃描項目assets中放置的內置插件生成builtin.json
    注:該gradle插件沒有考慮到用戶會自定義Build Variant的情況或者在一個單獨的module中接入插件的情況,從接入過程來看這個情況還是比較普遍的,如果要適配這中情況只能將源碼下下來自己修改下。
  • replugin-host-lib:宿主接入的一個java sdk,插件化核心功能都在這里實現,負責框架的初始化、加載、啟動和管理插件等。
  • replugin-plugin-gradle:插件工程接入的gradle插件,主要功能是使用javassist在編譯期間動態去替換插件中的繼承基類,如修改Activity的繼承、Provider的重定向等。
  • replugin-plugin-lib:插件工程接入的java sdk,功能主要是通過反射調用宿主工程中replugin-host-lib的相關類(如RePluginRePluginInternal提供的接口,內部實現都是反射),以提供“雙向通信”的能力。

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

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

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

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

  • 這里的startActivity()并沒有使用原生的Activity.startActivity()或者Context.startActivity()(VirtualAPK能直接調用)而是調用了RePlugin自己封裝的接口。這里這樣實現的主要是因為RePlugin為了做到唯一Hook點而沒有像大部分框架的實現方式那樣去Hook系統的跳轉接口,所以只能退一步讓開發者去調用額外的接口去啟動插件了,雖然這里增加了學習成本,但大體的接口用法和原生是一致的(差別只是在創建ComponentName時使用插件名而不是平常的包名),而且還支持action的隱式啟動。
  • 上述示例中展示的是在宿主中啟動插件中的Activity,我們可以手動調用RePlugin封裝的startActivity()接口。但如果是在插件中啟動其他Activity(包括在其他插件中和其他進程中的Activity)呢?(RePlugin的宗旨是插件的開發要像原生開發一樣)或者是一個插件中接入了第三放sdk,然后sdk內部有啟動Activity的需求,我們沒法主動去調RePlugin的接口,該怎么適配這種情況呢?RePlugin主要做了兩種情況的適配:
    第一種情況:如果插件是通過Activity.startActivity()啟動其他Activity的,前面有提到過插件工程需要接入replugin-plugin-gradle插件,他會在編譯期間去替換Activity的繼承關系為PluginActivity,它重寫了方法startActivity()來實現即便調用原生的方法也會給你轉向到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() 方法通過反射最終還是調用到了RePlugin.startActivity()的實現方法。
第二種情況:如果是通過調用Context.startActivity()來啟動其他Activity的呢?這里的適配主要是在PluginContext中實現重寫startActivity(),具體實現跟PluginActivity重寫方法大體是一樣的。為什么適配PluginContext的方法就能替換原生的方法呢?因為插件中的Context實例要么是通過Activity.getContext()獲取,要么是通過getApplicationContext()獲取的,只要這兩處地方拿到的ContextPluginContext就可以實現了,分別看下源碼,PluginActivity中替換Context的代碼是在attachBaseContext()中,這個方法會在ActivityonCreate()執行之前就會調用:

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

ApplicationContext的替換是在加載對應插件時通過PluginApplicationClient的方法替換的:

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

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

其中mLoader.mPkgContext就是PluginContext。以上兩種情況的實現就做到了不需要Hook系統的方法就能實現啟動插件中Activity了。

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

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

簡單說來,它主要是處理四大組件(以Activity為例)的坑位替換問題:用已經在Manifests中注冊了的坑位Activity去騙過AMS的校驗,然后在啟動流程后續階段具體實例化對應Activity時去替換為坑位的組件(Instrumentation.newActivity()去實例化Activity)。我們看下Activity的啟動流程中校驗Manifests注冊和創建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);

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

        /**
         * 這里通過result檢測出錯的結果
         */
        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的校驗結果,在下面函數checkStartActivityResult()中通過拋異常的方式反饋錯誤結果。因此,只要保證走到這一步之前傳遞的Activity都是坑位Activity即可正常跑通。因此到該階段為止,插件框架中傳遞的都是坑位信息。接下來看看startActivity()的后需階段。

第二步、創建Activity

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

    //...
    ContextImpl appContext = createBaseContextForActivity(r);
    Activity activity = null;
    try {
        /**
        * 在RePlugin中,這里獲取的ClassLoader已經被替換成了RePluginClassLoader
        * newActivity()通過ClassLoader和ClassName構建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()中通過某種映射關系替換掉坑位Activity實例化一個我們插件中的Activity。具體實現見如下類:
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的實現,但一定要基于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 變為 PathClassLoader->MyClassLoader->BootCalssLoader。所有需要通過PathClassLoader加載的類都讓其父加載器MyClassLoader去加載也能達到目的。(MoPlugin的實現)

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

//RePluginClassLoader
//這里className是要替換的類
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    Class<?> c = null;
    /**
     * 這里最終調用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;
        }
        //...
    }

    /**
     * 通過動態注冊的類映射插件的類
     */
    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;
    }
}

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

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

/**
 * 定位到插件中要注冊類的位置(插件名+類名)創建一個ComponentName
 */
ComponentName target = RePlugin.createComponentName("plugin1", 
    "com.test.android.plugin1.fragment.Plugin1Demo1Fragment");
/**
 * 調用registerHookingClass()函數將需要替換的類與插件中的類做一個映射,后面如果再來找目標類時
 * 則會去對應插件中去找
 */
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是怎么映射的呢?
其主要實現在PluginContainers類中實現,由于這里實現最為復雜,使用文字描述簡化其過程:

  1. 請求分配坑位。
  2. 調度到常駐進程并通過組件的android:process=xx匹配到映射進程,常駐進程此時再拉起一個Provider來啟動對應新進程,并返回一個插件進程的Binder
  3. 插件進程啟動時從常駐進程加載登記表(坑和目標activity表)。
  4. 插件進程從登記表中匹配坑位組件。
  5. 請求者發起startActivity()請求,參數為坑位組件。

Service、Provider的處理

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


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

上面的調用最終會通過binder執行到Service的管理類PluginServiceServer中的startServiceLocked(),然后在其中會手動構建Service對象并執行其生命周期,最后啟動對應進程的坑位Service防止系統誤殺:

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

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

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

    /**
     * 從binder線程post到ui線程,去執行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創建Service對象
    Context plgc = Factory.queryPluginContext(sr.plugin);

    ClassLoader cl = plgc.getClassLoader();


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

    }

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

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

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

Provider的處理也很簡單,僅僅是通過替換操作的Uri參數,讓其命中對應坑位進程的Provider,然后在對應坑位進程的函數從Uri解析出對應插件的Provider并手動執行最終的操作:

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);

此時會調用到

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

//轉換邏輯
public static Uri toCalledUri(Context context, String plugin, Uri uri, int process) {
    /**
     * 根據process映射到對應進程的的坑位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;
    }

    /**
     * 轉換為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);
}

最終會執行對應坑位Providerinsert()函數:

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

廣播的處理

廣播的處理則更為簡單,就是將插件中Manifests中注冊的靜態廣播變成在加載插件時手動注冊的動態廣播即可,下面的調用在加載插件時觸發:

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

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

    // 動態注冊插件中聲明的 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;

    // 遍歷此插件中所有靜態聲明的 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);
        }
    }
}

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

多進程的支持

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

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

那難點在哪里?
1、坑位配置更加復雜。特別是activity本身就涉及到啟動模式、taskAffnity、主題等屬性的組合,現在多加入一個android:process=xxx的組合,坑位的數量成指數級增長了。
2、進程名稱是插件中Manifests中的組件屬性中定義的,需要與對應坑位的進程進行映射(注意:這里要在Activity啟動之前完成)。
3、由于坑位和插件內容分布在多個進程中,對坑位和插件的管理涉及到了跨進程,這大大增加了復雜度。

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

RePlugin實現:在app啟動時(主進程)會拉起一個常駐進程(類似與系統的ActivityManagerService對應的進程),后續涉及插件的相關機制都去通過binder調用常駐進程,插件信息信息保存在這個常駐進程中,并統一管理和分配(這樣才能保持一致性)。然后當需要啟動一個插件組件(如Activity)時,先使用進程屬性(android:process=xxx)提前匹配將要運行的進程名,然后常駐進程以該進程名為參數啟動一個對應進程的Provider(沒有實際作用,僅僅是為了拉起一個新進程并返回一個Binder對象),這樣便可以在新進程中執行Application的生命周期(attachBaseContext()onCreate())了,而這里是我們在新進程中初始化插件的入口,于是我們便可以在啟動插件組件之前先啟動新進程了(這是因為常駐進程對多進程進行管理,需要提前建立兩個進程的通信通道并同步一些插件信息,一切準備就緒后再啟動對應坑位組件)。

先來看看demo中動態生成坑位信息,它在gradle配置中指定了3個進程,對activityproviderservice生成的坑位信息如下(一部分),p0~p2就是需要去映射的進程名:

//activity的多進程坑位,可以看到同一屬性的activity有3個進程(p0、p1、p2)對應的坑位
<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坑位,用于拉活對應進程
<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: 每個進程都會實例化這個類,但是內部實現會區分進程走到不同的分支。
PmHostSvc: 僅運行在常駐進程中(Service端),統一管理一切,并通過binder向其他進程(Client端)提供訪問接口。
PluginProcessPer:每個進程都有實例(Service端),并將一個binder注冊到常駐進程PmHostSvc(Client端),它相當于是插件進程與常駐進程的通信通道。
PluginProcessMain: 沒有具體的實例,內部都是靜態方法,只是提供其他進程與常駐進程進行交互的接口,最重要的接口就是connectToHostSvc(),將兩個進程連接起來并同步一些信息。
PluginProcessHost: 沒有具體的實例,內部使用了靜態變量保存了一些進程參數的初始值。
ProcessPitProviderPersistProcessPitProviderUIProcessPitProviderP0ProcessPitProviderP1ProcessPitProviderP2:這幾個provider就是在啟動坑位前先拉起,讓對應進程先起來的作用。

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

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

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

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

上面方法最終調用到PmBaseinit()方法,這里會區分是否是常駐進程啟動(服務端)還是非常駐進程(客戶端)啟動,分別對客戶端和服務端進行初始化,其中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()創建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);
}
  • 啟動插件組件時帶起插件坑位進程
    這里的插件進程是在啟動一個插件組件(聲明了進程名)時觸發的,我們以上面那一節啟動插件中的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);
}

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

//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);

    //調用系統接口啟動坑位Activity
    context.startActivity(intent);

    return true;
}

下面看他的具體實現,具體步驟參見注釋:

//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);

        // 根據 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);
}

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

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

最終內部則是調用PmBase.startPluginProcessLocked()接口去啟動進程,其步驟跟啟動常駐進程原理是一致的,還是通過啟動對應進程的Provider來最終觸發新進程Application.attachBaseContext()的執行,便有進入了框架的初始化流程:

//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' />

就這樣啟動了一個新進程了!!一直跟過來其實并沒有發現什么新技術,還是套用了四大組件的啟動+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);
}

因此,讀取插件中的資源可以使用該接口實現,參數定義參見上述的定義,下面是讀取drawable示例:

/**
 * 獲取插件的Resources對象(觸發插件的加載)
 */
Resources resources = RePlugin.fetchResources("plugin2");
/**
 * 通過resource的getIdentifier()接口獲取對應資源的id(參數參考上面的定義)
 */
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去加載布局,測試結果布局中的資源仍不能加載
     */
    View result = getLayoutInflater().inflate(parser, parent);

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

so庫的支持

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

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

    // 若主程序用的是64位進程,則所屬的SO必須只拷貝64位的,否則會出異常。32位也是如此
    // 問:如果用戶用的是64位處理器,宿主沒有放任何SO,那么插件會如何?
    // 答:宿主在被安裝時,系統會標記此為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;
}

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

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

其他

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

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

總結

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

參考文獻

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.lxweimin.com/p/74a70dd6adc9
https://blog.csdn.net/yulong0809/article/details/78428247

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 青山綠水瓊花艷, 臨澤新顏萬里程。 制種率先民致富, 葡萄產業板橋宏。 丹霞雄起游人旺, 凹凸儲量世聞名。 大美棗...
    撫彝牛人閱讀 908評論 2 1
  • 當你的生命還有最后四個月,你會做什么? 從小到大,都是在別人的選擇中度過,也一直活在別人的期望之中,并不斷被當做“...
    端木山閱讀 299評論 0 0
  • 08116 鄒公子 在《幸福的種子》中松居直先生一直在反復強調一個觀點——那就是繪本“無用論”。 任何一個妄圖用繪...
    自制力才是超能力閱讀 487評論 8 5
  • github 倉庫 我們可以再網絡上創建一個倉庫 我們可以創建一個空的倉庫QQ20150721-5@2x.png ...
    Yanni_L閱讀 361評論 1 1