插件化介紹和原理解析

什么是插件化

首先我們區(qū)分一下組件化和插件化的概念

  1. 組件化
    組件化開發(fā)就是將一個app分成多個模塊,組件化強調(diào)功能拆分,單獨編譯,單獨開發(fā),根據(jù)需求動態(tài)配置組件。
  2. 插件化
    插件化是將一個apk根據(jù)業(yè)務(wù)功能拆分成不同的子apk,插件化更關(guān)注動態(tài)加載、熱更新。
  3. 熱修復(fù)
    熱修復(fù)強調(diào)的是在不需要二次安裝應(yīng)用的前提下修復(fù)已知的bug。


    組件化和插件化.png

    熱修復(fù)基本原理.png
堆比.png

插件化的優(yōu)點

  1. 宿主和插件分開編譯
  2. 并發(fā)開發(fā)
  3. 動態(tài)更新插件
  4. 按需下載模塊
  5. 方法數(shù)或變量數(shù)爆棚
  6. 插件無需安裝即可運行

插件化發(fā)展歷程

image.png
  1. 靜態(tài)代理
    dynamic-load-apk最早使用ProxyActivity這種靜態(tài)代理技術(shù),由ProxyActivity去控制插件中PluginActivity的生命周期
  2. 動態(tài)替換(HOOK)
    在實現(xiàn)原理上都是趨近于選擇盡量少的hook,并通過在manifest中預(yù)埋一些組件實現(xiàn)對四大組件的動態(tài)插件化。像Replugin。
  3. 容器化框架
    VirtualApp能夠完全模擬app的運行環(huán)境,能夠?qū)崿F(xiàn)app的免安裝運行和雙開技術(shù)。Atlas是阿里的結(jié)合組件化和熱修復(fù)技術(shù)的一個app基礎(chǔ)框架,號稱是一個容器化框架。

插件化框架對比

插件化框架對比.png

插件化技術(shù)原理

實現(xiàn)插件化需要解決的問題

  1. 插件類的加載,解決宿主加載插件以及插件加載宿主的問題
  2. 資源文件的加載,解決宿主和插件的資源文件的加載問題,以及資源合并和資源沖突的問題
  3. 四大組件的支撐,支撐包括Activity,BroadReceiver. ContentProvider,Service四大組件在插件中的正常使用

類加載原理

classloader介紹

Classloader介紹.png

其中:

  1. BootClassLoader
    和java虛擬機中不同的是,BootClassLoader是ClassLoader內(nèi)部類,由java代碼實現(xiàn)而不是c++實現(xiàn),是Android平臺上所有ClassLoader的最終parent,這個內(nèi)部類是包內(nèi)可見。
  2. BaseDexClassLoader
    負責(zé)從指定的路徑中加載類,加載類里面的各種校驗、檢查和初始化工作都由它來完成
  3. PathClassLoader
    繼承自BaseDexClassLoader,只能加載已經(jīng)安裝到Android系統(tǒng)的APK里的類,主要邏輯由BaseDexClassLoader實現(xiàn)
  4. DexClassLoader
    繼承自BaseDexClassLoader,可以加載用戶自定義的其他路徑里的類,主要邏輯都由BaseDexClassLoader實現(xiàn)。

雙親委派模型

含義:雙親委派的意思是如果一個類加載器需要加載類,那么首先它會把這個類請求委派給父類加載器去完成,每一層都是如此。一直遞歸到頂層,當(dāng)父加載器無法完成這個請求時,子類才會嘗試去加載。

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先從緩存查找該class對象,找到就不用重新加載
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) {
                      //如果找不到,則委托給父類加載器去加載
                      c = parent.loadClass(name, false);
                  } else {
                  //如果沒有父類,則委托給啟動加載器去加載
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
              }
              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // 如果都沒有找到,則通過自定義實現(xiàn)的findClass去查找并加載
                  c = findClass(name);
                  // this is the defining class loader; record the stats
                  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
              }
          }
          if (resolve) {//是否需要在加載時進行解析
              resolveClass(c);
          }
          return c;
      }
  }

為什么使用雙親委派模型?

  1. 帶有優(yōu)先級的層次關(guān)系,通過這種層級關(guān)可以避免類的重復(fù)加載;
  2. 其次是考慮到安全因素,java核心API中定義類型不會被隨意替換。

如何動態(tài)加載APK的類文件?

主要依賴上述DexClassLoader:
我們看下DexClassLoader的構(gòu)造方法:
DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
dexPath:要加載的類所在的jar或者apk文件路徑,類裝載器將從該路徑中尋找指定的目標(biāo)類,該類必須是APK或jar的全路徑
optimizedDirectory:odex優(yōu)化之后的dex存放路徑,真正的數(shù)據(jù)是從這個位置的dex文件加載的,由于ClassLoader只能加載內(nèi)部存儲路徑中的dex文件,所以這個路徑必須為內(nèi)部路徑
librarySearchPath:目標(biāo)類中所使用的C/C++庫存放的路徑
classloader:本裝載器的父裝載器,一般使用當(dāng)前執(zhí)行類的裝載器就可以了,在Android用context.getClassLoader()就可以了

加載樣例如下:

private void loadClass() {
    // 獲取推送到SDCard中的插件路勁
    String apkPath = Environment.getExternalStorageDirectory() + File.separator + "test.apk";
    // 優(yōu)化后的dex存放路徑
    String dexOutput = getCacheDir() + File.separator + "DEX";
    File file = new File(dexOutput);
    if (!file.exists()) file.mkdirs();
    DexClassLoader dexClassLoader = new DexClassLoader(apkPath, dexOutput, null, getClassLoader());
    try {
        // 從優(yōu)化后的dex文件中加載APK_HELLO_CLASS_PATH類
        clazz = dexClassLoader.loadClass("com.iflytek.test.HelloWorld");
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

這樣我們就可以加載一個指定路徑下的apk文件的class文件

加載插件資源文件

加載插件資源文件原理

//獲取資源文件的方式
Drawable drawable = context.getResource().getDrawable(R.drawable.error);

Resource構(gòu)造函數(shù)
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(null);
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }

其中真正去進行資源加載的為AssetManager 。
AssetManager的addAssetPath()方法添加系統(tǒng)資源和apk資源,并構(gòu)造Resource提供給Context上下文進行使用,所以真正加載資源是通過AssetManger去加載。

 public final int addAssetPath(String path) {
        return  addAssetPathInternal(path, false);
    }

思路

1. 反射調(diào)用AssetsManager的addAssetPath方法;
2. 將外部的apk路徑添加進去,構(gòu)建新的Resource對象
3. 通過classloader加載R.java獲取drawable,對應(yīng)的id
4. 通過上述構(gòu)建的Resource獲取drawable對象。

/**
* 反射添加資源路徑,并創(chuàng)建新的Resources 對象
*/
private Resources getPluginResources() {
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        //反射獲取AssetManager的addAssetPath方法
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        //將插件包地址添加進行
        addAssetPath.invoke(assetManager, apkDir+ File.separator+apkName);
        Resources superRes = context.getResources();
        //創(chuàng)建Resources
        Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(),
                superRes.getConfiguration());
        return mResources;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}


/**
* 1. 先獲取資源的名稱對應(yīng)的id(通過反射R.java文件的變量)
* 2.  再根據(jù)我們構(gòu)造的Resources 獲取對應(yīng)的資源對象。我
*/
public Drawable getApkDrawable(String drawableName){
    try {
        DexClassLoader dexClassLoader = new DexClassLoader(apkDir+File.separator+apkName,
        optimizedDirectoryFile.getPath(), null, context.getClassLoader());
 
        //通過使用apk自己的類加載器,反射出R類中相應(yīng)的內(nèi)部類進而獲取我們需要的資源id
        Class<?> clazz = dexClassLoader.loadClass(apkPackageName + ".R$drawable");
        Field field = clazz.getDeclaredField(drawableName);
        int resId = field.getInt(R.id.class);//得到圖片id
        Resources mResources = getPluginResources();
        assert mResources != null;
        return mResources.getDrawable(resId);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return null;
}

資源文件處理方式

合并式:addAssetPath時加入所有插件和主工程的路徑;優(yōu)點是插件和宿主可以相互訪問,缺點是可能產(chǎn)生資源沖突。
獨立式:各個插件只添加自己apk路徑。不存在資源沖突,但是無法資源共享。

合并式資源沖突的解決方案:
修改aapt源碼,定制aapt工具編譯期間修改PP段
修改aapt的產(chǎn)物,即,編譯后期重新整理插件Apk的資源,編排ID

插件Activity處理方案

代理模式

代理模式的定義:代理模式給某一個對象提供一個代理對象,并由代理對象控制對原對象的引用。通俗的來講代理模式就是我們生活中常見的中介。


代理模式.png

動態(tài)代理的具體實現(xiàn)參考如下:

public class DynamicProxyHandler implements InvocationHandler {
    private Object object;
    public DynamicProxyHandler(Object object) {
        this.object = object;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        breforeInvoke();
        Object result =  method.invoke(object, args);
        afterInvoke();
    }
 }

如何動態(tài)加載插件中Acitivity

Activity插件化需要解決的問題:

  1. 怎么欺騙AMS去啟動一個清單文件不存在的Activity ;
  2. 插件Activity的生命周期如何實現(xiàn);
  3. 插件apk中用過的各種資源,如何動態(tài)的加載資源。
代理模式(DL框架)

ProxyActivity + 插件中沒注冊的Activity = 標(biāo)準的Activity

代理模式插件化原理.png

主要流程如下:

  1. 宿主中通過啟動ProxyAcitivity
  2. 代理activity通過AIDL通信和插件PluginActivity建立聯(lián)系
  3. 當(dāng)宿主中的代理ProxyAcitivity生命周期發(fā)生變化的時候,通過AIDL通知到PluginActivity。從而完成插件Activity生命周期的同步。
坑位占用模式

在AndroidManifest中注冊,但并沒有真實的實現(xiàn) 類,只作為其他Activity啟動的坑位,通過HOOK AMS去加載插件中的Activity的class文件。

下面我們來介紹一下Replugin的Activity原理

Replugin原理.png
  1. Pmbase根據(jù)Intent找到對應(yīng)的插件
  2. 分配坑位Activity,與插件中的Activity建立一對一的關(guān)系并保存在PluginContainer中
  3. 讓系統(tǒng)啟動坑位Activity,因為它是在Manifest中注冊過的
  4. Android系統(tǒng)會嘗試使用RepluginClassLoader加載坑位Activity的Class對象
  5. RepluginClassLoader 通過建立的對應(yīng)關(guān)系找到插件Activity,并使用
    PluginDexClassLoader 加載插件Activity 的Class對象并返回
  6. Android系統(tǒng)就使用這個插件中的Activity的Class對象來運行生命周期函數(shù)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容