Android-插件化開發

插件開發介紹

動態加載背景

Alt text
Alt text

當工程愈來愈大的時候,痛點來了:
一、 文件太多,編譯太慢,apk包太大
二、 方法數超過65536,須要分包,啓動時進行,啓動界面根據分包狀況會有不一樣耗時
三、 線上功能出現bug,不能及時修復,須要從新打包修復
四、 功能模塊開發須要依賴總體項目進度,沒法單獨進行開發調試android

有沒有相關的技術方案解決上面的這些痛點了?git

動態加載的形式
github

Alt text
Alt text

  1. 應用在運行的時候經過加載一些本地不存在的可執行文件實現一些特定的功能;
  2. 這些可執行文件是能夠替換的;

插件化實現過程

Alt text
Alt text

基本實現流

Android項目中,插件化按照可執行文件的不一樣大體能夠分爲兩種:bash

  • 動態加載so庫;
  • 動態加載dex/jar/apk文件(如今動態加載廣泛說的是這種);

實現的基本步驟爲:app

  • 把可執行文件(.so/dex/jar/apk)拷貝到應用APP內部存儲;
  • 加載可執行文件;
  • 調用具體的方法執行業務邏輯;框架

    DexClassLoader dexClassLoader = new DexClassLoader(optimizedDexOutputPath.getAbsolutePath(), dexOutputDir.getAbsolutePath(), null, getClassLoader());
    
              Class libProviderClazz = null;
    
              try {
    
                  libProviderClazz = dexClassLoader.loadClass("me.kaede.dexclassloader.MyLoader");
    
                  // 遍歷類裏全部方法
    
                  Method[] methods = libProviderClazz.getDeclaredMethods();
    
                  for (int i = 0; i < methods.length; i++) {
    
                      Log.e(TAG, methods[i].toString());
    
                  }
    
                  Method start = libProviderClazz.getDeclaredMethod("func");// 獲取方法
    
                  start.setAccessible(true);// 把方法設爲public,讓外部能夠調用
    
                  String string = (String) start.invoke(libProviderClazz.newInstance());// 調用方法並獲取返回值
    
                  Toast.makeText(this, string, Toast.LENGTH_LONG).show();
    
              } catch (Exception exception) {
    
                  // Handle exception gracefully here.
    
                  exception.printStackTrace();
    
              }複製代碼

有幾個疑問了:ide

一、 既然是apk,那麼資源文件怎麼加載進來
二、 四大組件怎麼啓動,並無註冊在宿主apk裏
………工具

具體實現

上面兩個問題也是插件化具體實現的關鍵問題。對於上述兩個問題,咱們最直觀的解決辦法:佈局

一、 在宿主程序中添加劇新拷貝一份插件apk中的資源文件,或者採用純代碼佈局。
二、 將插件中的mnifest文件也拷貝到宿主應用中
三、 替換activity爲fragmentgradle

…....

Alt text
Alt text

可是適用範圍:界面更新比較少,基本是單一界面,不會有太大改動的。
上述的解決辦法可以解決插件化的基本痛點,可是咱們仍然痛。好比插件替換成fragment後,靈活性下降了,須要不斷的更新插件apk中的資源文件與manifest到宿主程序中。。。

先看Activity:採用代理的方式解決manifest中插件組件未註冊不能使用的問題。

Alt text
Alt text

具體的實現流程如上圖所示:
一、 在宿主中註冊一個代理ProxyActivity
二、 插件中的PluginActivity實現一個相似於Acitivty生命週期的協議接口
三、 經過中轉類PluginManager來執行跳轉,這裏跳轉到代理類ProxyActivity

宿主程序要啓動插件組件,經過調用PluginManager的startActivity方法啓動,在這個方法裏將插件類替換爲代理類,同時代理類會持有插件類的引用。
插件與代理類的綁定發生在PluginManager啓動的時候發生,經過反射的機制在代理類執行生命週期的時候構造出插件類的引用,爲了讓插件類也具備本身的生命週期,代理類與插件類都實現了一個生命週期的接口,保證插件在不註冊的狀況下具備基本的生命週期

接下來看一下資源文件的問題:
先貼一段源碼 Context


public Resources getResources() {

    if (mResources != null) {

        return mResources;

    }

    if (mOverrideConfiguration == null) {

        mResources = super.getResources();

        return mResources;

    } else {

        Context resc = createConfigurationContext(mOverrideConfiguration);

        mResources = resc.getResources();

        return mResources;

    }

}複製代碼

跟蹤到ResourceManager關鍵代碼

Resources getTopLevelResources(String resDir, String[] splitResDirs,
            String[] overlayDirs, String[] libDirs, int displayId,
            Configuration overrideConfiguration, CompatibilityInfo compatInfo) {
        Resources r;
        AssetManager assets = new AssetManager();
        if (libDirs != null) {
            for (String libDir : libDirs) {
                if (libDir.endsWith(".apk")) {
                    if (assets.addAssetPath(libDir) == 0) {
                        Log.w(TAG, "Asset path '" + libDir +
                                "' does not exist or contains no resources.");
                    }
                }
            }
        }
        DisplayMetrics dm = getDisplayMetricsLocked(displayId);
        Configuration config ……;
        r = new Resources(assets, dm, config, compatInfo);
        return r;
    }複製代碼

經過這些代碼從一個APK文件加載res資源並建立Resources實例,通過這些邏輯後就可使用R文件訪問資源了。具體過程是,獲取一個AssetManager實例,使用其「addAssetPath」方法加載APK(裏的資源),再使用DisplayMetrics、Configuration、CompatibilityInfo實例一塊兒建立咱們想要的Resources實例。
經過上面的源碼,能夠經過反射的機制來實現一樣的效果,關鍵代碼貼圖:

private AssetManager createAssetManager(String dexPath) {

    try {

        AssetManager assetManager = AssetManager.class.newInstance();

        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);

        addAssetPath.invoke(assetManager, dexPath);

        return assetManager;

    } catch (Exception e) {

        e.printStackTrace();

        return null;

    }



}private Resources createResources(AssetManager assetManager) {

    Resources superRes = mContext.getResources();

    Resources resources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());

    return resources;

}複製代碼

這樣就建立了插件獨立的resource了,資源文件也可以獨立的加載了。

上面兩個最重要的問題都有了比較好的解決辦法,可是你覺得到這裏就結束了嗎。。。。然而我只能告訴你

Alt text
Alt text

這裏面還有不少問題要處理:
實際運行的Activity實例其實都是ProxyActivity,並非真正想要啓動的Activity;
每每不是全部的apk均可做爲插件被加載,插件項目須要依賴特定的框架,還有須要遵循必定的"開發規範";
由於採用了反射,程序的不穩定性增長了。
。。。。

插件實現的一些不思議實現

有一些奇妙的東西

對於上述採用代理的實現方式已經基本可以知足部門內部的插件化的大部分需求,但是對於上面的限制,還有更好的解決辦法。
解決對策就是,在須要啓動插件的某一個Activity(好比PlugActivity)的時候,動態建立一個TargetActivity,新建立的TargetActivity會繼承PlugActivity的全部共有行爲,而這個TargetActivity的包名與類名恰好與咱們事先註冊的TargetActivity一致,咱們就能以標準的方式啓動這個Activity。運行時動態建立並編譯一個Activity類,這種想法不是天方夜譚,動態建立類的工具備dexmaker和asmdex,兩者均能實現動態字節碼操做,最大的區別是前者是建立dex文件,然後者是建立class文件。

Alt text
Alt text

這是他的一個加載流程,在啓動插件acitivty 的時候,先替換成宿主程序中已經註冊過的targetactivity,這個targetactivity是不存在的一開始,這樣咱們會首先進入到frameworkclassloader,發現targetactivity後,調用插件的classloder進行targetacitivity的建立,並對新生成的dex建立新的dexclassloder,用該classloder加載targetactivity,這樣就完成了一個真正的替換。對於通常的類跳轉,咱們直接經過pluginclassloader加載就行了。
這裏還有一個問題,就是插件內部的activity的跳轉,也沒有註冊,這種狀況下,咱們是在生成的targetactivity裏面在調用startactivityforresult的時候,這裏也作一層替換,就是調用pluginmanager的startactivity就行了。

RePlugin

這是最近360開源的一款插件化開發框架。你們對360其實很早以前就知道他的另一款插件了----- DroidPlugin,這個在業界也是挺有名的一款開源插件化開發框架。只是這個工具hook的點太多,致使不穩定性過高

最近開源的RePlugin,據說只hook了一個點,到底有多牛逼,也順應潮流看了一下這個框架。首先看一下他的使用方式:

一、 添加 RePlugin Host Gradle 依賴,在項目根目錄的 build.gradle(注意:不是 app/build.gradle) 中添加 replugin-host-gradle 依賴:

buildscript {
    dependencies {
        classpath 'com.qihoo360.replugin:replugin-host-gradle:2.1.5'
        ...
    }
}複製代碼

二、 添加 RePlugin Host Library 依賴。

android {
   // ATTENTION!!! Must CONFIG this to accord with Gradle's standard, and avoid some error defaultConfig { applicationId "com.qihoo360.replugin.sample.host" } } // ATTENTION!!! Must be PLACED AFTER "android{}" to read the applicationId apply plugin: 'replugin-host-gradle' // If use AppCompat, open the useAppCompat repluginHostConfig { useAppCompat = true } dependencies { compile 'com.qihoo360.replugin:replugin-host-lib:2.1.5' }複製代碼

三、 配置application。讓工程的 Application 直接繼承自 RePluginApplication。

這個是接入方式,接下來,看一下調用插件的方式:

一、 宿主調用插件的組件:

RePlugin.startActivity(MainActivity.this, RePlugin.createIntent("demo1", 
    "com.qihoo360.replugin.sample.demo1.MainActivity"));複製代碼

二、 插件調用宿主主鍵:須要使用包名來調用

// 方法1(最「單品」)
Intent intent = new Intent();
intent.setComponent(new ComponentName("demo2", 
    "com.qihoo360.replugin.sample.demo2.databinding.DataBindingActivity"));
context.startActivity(intent);

// 方法2(快速建立Intent)
Intent intent = RePlugin.createIntent("demo2", 
    "com.qihoo360.replugin.sample.demo2.databinding.DataBindingActivity");
context.startActivity(intent);
// 方法3(一行搞定)
RePlugin.startActivity(v.getContext(), new Intent(), "demo2", 
    "com.qihoo360.replugin.sample.demo2.databinding.DataBindingActivity");複製代碼

首先宿主的application要繼承RePluginApplication,啓動插件中的某個activity; RePlugin.startActivity(MainActivity.this, RePlugin.createIntent("demo1", "com.qihoo360.replugin.sample.demo1.MainActivity"));

Alt text
Alt text
Alt text
Alt text

RePlugin首先會經過gradle插件在宿主程序中生成一個新的manifest文件,上面是新生成文件裏面的部分截圖,這裏展現的就是插件須要的坑位。是的你猜到了,這個文件首先會把全部可能狀況的組件的坑位都生成到這裏,而後讓插件找到合適的坑位去匹配。這裏的坑位命名規則符合如下規則:

/**

 * 目前的策略是,針對每一種 launchMode 分配兩種坑位(透明主題(TS)和不透明主題(NTS))

 * <p>

 * 例:透明主題

 *        <N1NRTS0, ActivityState>

 * NR + TS  - > <N1NRTS1, ActivityState>

 *        <N1NRTS2, ActivityState>

 * <p>

 * 例:不透明主題

 *        <N1NRNTS0, ActivityState>

 * NR + NTS - > <N1NRNTS1, ActivityState>

 *        <N1NRNTS2, ActivityState>

 * <p>

 * 其中:N1 表示當前爲 UI 進程,NR 表示 launchMode 爲 Standard,NTS 表示坑的 theme 爲 Not Translucent。

 */複製代碼

每一個activity坑位的命名方式都是按照進程名+啓動模式+透明方式的方式命名。這裏組合起來有230個左右的坑位,足夠一個apk中各類activity的填坑了。
對於service與provider的坑位的設置也就稍微簡單點了。
再看一下在插件中聲明原始activity,有一種快捷的進程匹配方式,宿主就能很快的匹配上坑位:

Alt text
Alt text

這樣聲明的規則以下:

from:原來聲明的進程名是什麼。例若有個Activity,其進程名聲明爲「com.qihoo360.launcher:wff」
to:要映射到的進程名,必須以「$」開頭,表示「特殊進程」
$ui:映射到UI進程
$p0:映射到進程坑位0進程
$p1:映射到進程坑位1進程
以此類推。複製代碼

接下來咱們看一看他的部分實現原理。其實Replugin的實現方式咱們能夠分解成三個部分:

一、 建立坑位。足夠多的坑位,可以覆蓋足夠多的組件跟場景。(主要是經過自定義gradle插件來實現)
二、 加載插件時,匹配坑位進行替換
三、 Classloader進行點hook,將坑位從新替換成目標組件。
看一下hook實現。先看一下app啓動進程與AMS涉及到的相關

Alt text
Alt text
Alt text
Alt text

App進程與ASM進程進行通訊的流程就是進程間通訊的原理,而後經過各自的binder代理進行通訊。AMS進程完成Activity生命周
期的管理以及任務棧的管理,由主線程的handler來處理收到的消息,也就是咱們這裏的ActivityThread裏面的handler來處理。
若是咱們沒有坑匹配與替換的過程,咱們能夠如今ASM這一層hook一次,把targetActivity替換成咱們已經註冊好的PluginActivity,接下來咱們的app進程接管了Activity的管理,這裏咱們再替換爲targetactivity就能夠了。既然這裏已經填坑完畢,咱們只須要在app進程將PluginActivity替換回去就行了。

相關文章鏈接:
dynamic-load-apk: github.com/singwhatiwa…

RePlugin: github.com/Qihoo360/Re… ,

www.jianshu.com/p/ca3bda080…

相關文章
相關標籤/搜索