【長(zhǎng)篇】插件化架構(gòu)設(shè)計(jì)

一次讓你徹底掌握Android插件化架構(gòu)設(shè)計(jì)

插件化簡(jiǎn)介

宿主host 與 插件(免安裝:不需要安裝apk,下載即可)

-->插件加載
-->插件化中的組件支持(startActivity如何去啟動(dòng))
-->插件化中資源(布局文件圖片)的加載

插件化優(yōu)點(diǎn)

1減小apk的體積,按需求下載模塊
2動(dòng)態(tài)更新插件
3宿主和插件分開編譯,提升團(tuán)隊(duì)開發(fā)效率
4解決方法數(shù)查過65535問題

缺點(diǎn):
項(xiàng)目復(fù)雜度變高了,難度變高,版本兼容問題(插件化的兼容)

組件化和插件化區(qū)別

組件化:是將一個(gè)APP分成多個(gè)模塊,每個(gè)模塊都是一個(gè)組件(module),開發(fā)的過程中我們可以讓這些組件相互依賴或者單獨(dú)調(diào)試部分組件,但是最終發(fā)布的時(shí)候?qū)⑦@些組件合并成一個(gè)統(tǒng)一的APK。

插件化:是將整個(gè)APP拆分成很多模塊,每個(gè)模塊都是一個(gè)APK(組件化的每個(gè)模塊是一個(gè)Lib)最終打包的時(shí)候?qū)⑺拗鰽PK和插件APK分開打包,插件APK通過動(dòng)態(tài)下發(fā)到宿主APK。

插件化框架對(duì)比

1.png

選中DroidPlugin
因?yàn)?60大廠,四大組件全支持,插件不需在清單文件注冊(cè),最重要的是幾乎全部支持Android特性。

插件化架構(gòu)的設(shè)計(jì)思路

2.png

面試題 簡(jiǎn)述Java類加載的過程

3.png

加載階段,虛擬機(jī)主要完成三件事:
1通過一個(gè)類的權(quán)限定名來獲取定義此類的二進(jìn)制字節(jié)流(class文件—>字節(jié)流);
2將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)域的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)(字節(jié)流-->對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu));
3在java堆中生成一個(gè)代表這個(gè)類的class對(duì)象,作為方法區(qū)域數(shù)據(jù)的訪問入口。

驗(yàn)證:是否符合java字節(jié)碼編碼規(guī)范
初始化完就會(huì)得到class對(duì)象(一切反射的基石)

Android中類加載器的集成結(jié)構(gòu)

4.png

常用的類加載器

BootClassLoader:系統(tǒng)啟動(dòng)時(shí)用于加載系統(tǒng)常用類,ClassLoader內(nèi)部類;

PathClassLoader:加載系統(tǒng)類和應(yīng)用程序類,一般不建議開發(fā)者使用;

DexClassLoader:加載DEX文件及包含dex文件的apk或jar。也支持從SD卡進(jìn)行加載,這也意味著DexClassLoader可以在應(yīng)用未安裝的情況下加載dex相關(guān)文件。因此它是熱修復(fù)和插件化技術(shù)的基礎(chǔ)。

驗(yàn)證上述的Demo

5.png

使用一個(gè)dexclassloader:

7.png
6.png

記得給讀寫權(quán)限??!

最后參數(shù)父類為什么用pathClassLoader而不是BaseDexClassLoader,之后再說(在下一節(jié))

面試題:雙親委派機(jī)制與自定義String類

8.png
9.png

流程:
DexClassLoader加載前問PathClassLoader你加載過嗎,如果沒有,PathClassLoader問BootClassLoader你加載過嗎,如果沒有,BootClassLoader問是夠可以加載,能加載自己就加載了。不能再向下回傳。

雙親委派機(jī)制優(yōu)點(diǎn):
避免重復(fù)加載,若已經(jīng)加載直接從緩存中讀取。
更加安全,避免開發(fā)者修改系統(tǒng)類。

參數(shù)父類為什么用pathClassLoader而不是BaseDexClassLoader:根據(jù)ClassLoader繼承圖他們倆不是繼承關(guān)系,這個(gè)方法參數(shù)里parent不是父類的意思(父類是super),這里是優(yōu)先級(jí)、上一層的意思。

當(dāng)發(fā)生Activity跳轉(zhuǎn)時(shí)自動(dòng)加載插件Activity

10.png

如何手動(dòng) 變 自動(dòng)

即:把插件的classloader由DexClassloader變成 pathClassLoader,讓其認(rèn)為是自己人。就會(huì)自動(dòng)加載了。

11.png

為什么行得通:DexClassloader和PathClassLoader都是調(diào)用的一樣的父類方法,區(qū)別就是8.0之前可以指定生成后的odex目錄,8.0之后都是系統(tǒng)目錄了。

加載指定類的時(shí)序圖

12.png

BaseDexClassLoader有一個(gè)DexPathList屬性,調(diào)用findClass遍歷dexElements。

搞清時(shí)序圖是為了Activity跳轉(zhuǎn)時(shí)能自動(dòng)加載。我們知道了pathClassLoader的dex都放到哪里了。然后把dexClassLoader放進(jìn)去。

App里的dex是用一個(gè)Element[ ] dexElements數(shù)組來存放的。
*為什么是數(shù)組?為了分包,一個(gè)app可以有多個(gè)dex。

插件dex的處理

即上面說的流程
把dexClassLoader放進(jìn)pathClassLoader的Element[ ] dexElements數(shù)組

13.png

如果想改dexElements數(shù)組要用到Hook技術(shù)。

Hook與Hook技巧

14.png

Hook技巧:
1要掌握反射和代理模式;
2盡量Hook靜態(tài)變量或者單例對(duì)象;(因?yàn)殪o態(tài)變量不用實(shí)例化一個(gè)對(duì)象)
3盡量Hook public的對(duì)象和方法。(如果是private容易搞壞內(nèi)部結(jié)構(gòu))

插件化架構(gòu)的模塊關(guān)系

創(chuàng)建兩個(gè)module

15.png
16.png
17.png

如何加載插件、插入dexElements數(shù)組等復(fù)雜工作都會(huì)在PluginCore里去完成

編碼實(shí)現(xiàn)宿主與插件dex數(shù)組合并

插入dexElements數(shù)組在什么時(shí)候比較好呢?
應(yīng)用程序啟動(dòng)時(shí)候最好。即Application
配置權(quán)限

18.png

插件管理器 (單例模式)

19.png
public class PluginManager{
  private static PluginManager instance;
  private  Context context;
  
  private PluginManager(Context context){
    this.context = context;
  }
  public static PluginManager getInstance(Context context){
    if(instance == null){
      instance = new PluginManager(context);
    }
    return instance;
  }
  public void init(){
    try{
      loadApk();
    }catch(Exception e){e.printStackTrace();}   
  }
  //加載插件APK文件并且合并dexElements
  private void loadApk() throws Exception{
    //加載插件的apk
    String pluginApkPath = context.getExternalFilesDir(null).getAbsolutePath()+"/pluginapp-debug.apk";
    //即插件apk地址
    String cachePath = context.getDir("cache_plugin",Context.MODE_PRIVATE).getAbsolutePath();
    DexClassLoader dexClassLoader = new DexClassLoader(pluginApkPath ,cachePath ,null,context.getClassLoader());
    
    //反射操作
    Class<?> baseDexClassLoader = dexClassLoader.getClass().getSuperclass();
    Field pathListField = baseDexClassLoader.getDeclareField("pathList")
    pathListField.setAccessible(true);
    
    //1獲取plugin的dexElements
    Object pluginPathListObject = pathListField.get(dexClassLoader);
    Class<?> pathListClass = pluginPathListObject .getClass();
    Field dexElementsField = pathListClass.getDeclaredField("dexElements");
    dexElementsField.setAccessible(true);
    Object pluginDexElements = dexElementsField.get(pluginPathListObject);

    Log.d("z","pluginDexElements: "+pluginDexElements );

    //2獲取host的dexElements
    ClassLoader pathClassLoader =context.getClassLoader();
    Object hostPathListObject = pathClassLoader.get(pathClassLoader);
    Object hostDexElements = dexElementsField.get(hostPathListObject )
    Log.d("z","hostDexElements : "+hostDexElements );
    //3合并
    int pluginDexElementsLength = Array.getLength(pluginDexElements );
    int hostDexElementsLength = Array.getLength(hostDexElements);
    int newDexElementsLength = pluginDexElementsLength +hostDexElementsLength ;
    
    Object newDexElements = Array.newInstance(hostDexElements.getClass().getComponentType(),newDexElementsLength);

    for(int i=0;i<newDexElementsLength;i++){
      if(i<pluginDexElementsLength){//plugin
        Array.set(newDexElements,i,Array.get(pluginDexElements,i));
      }else{//host
        Array.set(newDexElements,i,Array.get(hostDexElements,i-pluginDexElementsLength));
      }
    }
    dexElementsField.set(hostPathListObject,newDexElements);
    Log.d("z","newDexElements: "+newDexElements);
    
  }
}

把插件apk文件放到sdcard/Android/包名/files下

23.png

其他:

24.png
21.png
22.png

結(jié)果:

非常輕松地獲得插件的class

25.png

接下來是如何啟動(dòng)我們的組件。Activity如何跳轉(zhuǎn)的

Activity啟動(dòng)中的跨進(jìn)程訪問

我們?cè)噲D這樣來做跳轉(zhuǎn):

26.png

報(bào)錯(cuò)了:

27.png

提示沒有在宿主里注冊(cè)(雖然在插件自己的里面注冊(cè)了)

28.png

Activity1 --》Activity2 要走兩次跨進(jìn)程訪問

AMS會(huì)檢查我們的activity是否在清單文件里完成注冊(cè)。所以報(bào)錯(cuò)了

Hook在插件化架構(gòu)中的運(yùn)用

29.png

我們寫一個(gè)RegisteredActivity,里面什么都不需要,只要在清單文件里注冊(cè)。

分析activity啟動(dòng)流程

30.png
31.png

IActivityManager 就是AMS對(duì)象

我們hook這個(gè)IActivityManager 或者直接拿到mInstance這個(gè)屬性 即可拿到AMS對(duì)象

Hook AMS

32.png
33.png
public class HookUtils{
  //hook 我們的AMS對(duì)象,對(duì)其startActivity攔截
  //把里面intent處理
  public static void hookAMS(Context context) throws Exception{
    //1.獲取AMS對(duì)象
    //1.1獲取靜態(tài)屬性ActivityManager.IActivityManager Singleton的靜態(tài)屬性值
    //它是Singleton類型
    Field iActivityManagerSingletonField = ActivityManager.class.getDeclaredField("IActivityManagerSingleton");
    iActivityManagerSingletonField.setAccessible(true);
    Object iActivityManagerSingletonObject = iActivityManagerSingletonField.get(null);
    
    //1.2獲取Singleton的mInstance屬性值
    Class<?> singletonClazz = Class.forName("android.util.Singleton");

    Field mInstanceField = singletonClazz.getDeclaredField("mInstance");
    mInstanceField.setAccessible(true);
    Object AMSSubject = mInstanceField.get(iActivityManagerSingletonObject);


    //2對(duì)AMS對(duì)象進(jìn)行代理
    Class<?> IActivityManagerInterface=Class.forName("android.app.IActivityManager");
    AMSInvocationHandler handler = new AMSInvocationHandler(context,AMSSubject);
    Object AMSProxy = Proxy.newProxyInstance(
      Thread.currentThread().getContextClassLoader()
      ,new Class[]{IActivityManagerInterface}
      ,handler 

    );

    mInstanceField.set(iActivityManagerSingletonObject,AMSProxy);
    
    //3InvocationHandler對(duì)AMS對(duì)象的方法進(jìn)行攔截

  }

}
public class AMSInvocationHandler implements InvocationHandler{
  private Context context;
  private Object subject;
  
  public AMSInvocationHandler (Context context,Object subject){
    this.context = context;
    this.subject = subject;
   }
  
  @Override
  public Object invoke(Object proxy,Method method,Object[] args) throws Throwable{
    
    if("startActivity".equals(method.getName())){
      Log.d("z","AMSInvocationHandler startActivity invoke");
      //要把PluginActivity替換成RegisteredActivity
      //找到intent參數(shù)
      for(int i = 0;i<args.length;i++){
        Object arg = args[i];
        if(arg instanceof Intent){
          Intent intentNew = new Intent();
          intentNew.setClass(context,RegisteredActivity.class);    
          //原來的保存
          intentNew.putExtra("actionIntent",(Intent)arg);
          args[i] = intentNew;
          Log.d("z","AMSInvocationHandler new Intent");
          break;
        }
      }
      
    }
  
    return method.invoke(subject,args);
  }

}

在上面pluginManager 代碼段里加上

public void init(){
    try{
      loadApk();
      HookUtils.hookAMS(context);
      HookUtils.hookHandler();
    }catch(Exception e){e.printStackTrace();}   
  }

Hook Handler思路

現(xiàn)在等AMS驗(yàn)證完后,我們什么時(shí)候把想要啟動(dòng)的拿出來?

34.png

dispatchMessage會(huì)判斷callback是否為空
如果不為空,會(huì)執(zhí)行callback的handlerMessage,然后在執(zhí)行子類的handlerMessage.
所以我們給callback一個(gè)值,先執(zhí)行callback里handlerMessage,修改我們需要的內(nèi)容。

ActivityThread有一個(gè)H 內(nèi)部類 繼承自handler,里面記錄了hangler的信息(activityInfo)。

Handler發(fā)消息,looper輪詢到 后都會(huì)dispatchMessage,里面判斷mCallback是否為空。(一般都為空)

35.png

所以我們修改callback,在里面做事。

編碼實(shí)現(xiàn)Hook Handler

在上面HookUtils里加入一個(gè)方法

//hook 獲取到 Handler的特定消息(LAUNCH_ACTIVITY)中的intent,進(jìn)行處理。
//將intent對(duì)象里的RegisteredActivity替換成PluginActivity

public static void hookHandler() throws Exception{
  //1.獲取到handler對(duì)象(mH屬性值)
  //1.1獲取到ActivityThread對(duì)象
  Class<?>  activityThreadClazz= Class.forName("android.app.ActivityThread");
  //拿到activityThread對(duì)象,他有一個(gè)靜態(tài)的屬性(sCurrentActivityThread)
  Field sCurrentActivityThreadField = activityThreadClazz.getDeclaredField("sCurrentActivityThread");
  sCurrentActivityThreadField.setAccessible(true);
  Object activityThreadObject = sCurrentActivityThreadField.get(null);

  //1.2獲取ActivityThread對(duì)象的mH屬性值
  Field mHField = activityThreadClazz.getDeclaredField("mH");
  mHField.setAccessible(true);
  Object handler = mHField.get(activityThreadObject);

  //2.給我們的Handler的mCallBack屬性進(jìn)行賦值
  Field mCallbackField = Handler.class.getDeclaredField("mCallback");
  mCallbackField .setAccessible(true);


  //3.在callback里面將intent對(duì)象里的RegisteredActivity替換成PluginActivity
    //創(chuàng)建了MyCallback 
    mCallbackField.set(handler,new MyCallback());
}
public class MyCallback implements Handler.Callback{
     private static final int LAUNCH_ACTIVITY = 100;

  @Override
   public boolean handleMessage(Message msg){
      switch(msg.what){
        case LAUNCH_ACTIVITY:
          Log.d("z","MyCallback  handleMessage LAUNCH_ACTIVITY");
          try{
            Field intentField = msg.obj.getClass().getDeclaredField("intent");
            intentField.setAcessible(true);
            Intent intent = intentField.get(msg.obj);
            //取出我們放入的actionIntent
            Parcelable actionIntent = intent.getParcelableExtra("actionIntent");
            if(actionIntent!= null){
              //替換
              Log.d("z","MyCallback  intent replaced");
              intentField.set(msg.obj,actionIntent);
            }
            
          }catch(Exception e){
            e.printStackTrace();
          }
          break;
      }
      return false;//這里true直接結(jié)束了,return false則執(zhí)行子類的hangleMessage。!
    }
}

AMS版本適配

37.png

修改之前代碼

  public static void hookAMS(Context context) throws Exception{
    //1.獲取AMS對(duì)象
    //1.1獲取靜態(tài)屬性ActivityManager.IActivityManager Singleton的靜態(tài)屬性值
    //它是Singleton類型
    Field iActivityManagerSingletonField =null;
    if(Build.VERSION.SDK_INT>=BUILD.VERSION_O){
      iActivityManagerSingletonField = ActivityManager.class.getDeclaredField("IActivityManagerSingleton");
    }else{
    //低版本拿ActivityManagerNative的gDefault
      Class<?> ActivityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
      iActivityManagerSingletonField = ActivityManagerNativeClazz.getDeclaredField("gDefault");
    }
    iActivityManagerSingletonField.setAccessible(true);
    Object iActivityManagerSingletonObject = iActivityManagerSingletonField.get(null);

Handler版本適配

38.png
39.png

修改MyCallback

public class MyCallback implements Handler.Callback{
     private static final int LAUNCH_ACTIVITY = 100;

    private static final int EXECUTE_TRANSACTION= 159;

  @Override
   public boolean handleMessage(Message msg){
      switch(msg.what){
        case LAUNCH_ACTIVITY:
          Log.d("z","MyCallback  handleMessage LAUNCH_ACTIVITY");
          try{
            Field intentField = msg.obj.getClass().getDeclaredField("intent");
            intentField.setAcessible(true);
            Intent intent = intentField.get(msg.obj);
            //取出我們放入的actionIntent
            Parcelable actionIntent = intent.getParcelableExtra("actionIntent");
            if(actionIntent!= null){
              //替換
              Log.d("z","MyCallback  intent replaced");
              intentField.set(msg.obj,actionIntent);
            }
            
          }catch(Exception e){
            e.printStackTrace();
          }
          break;
        //API 28
        case EXECUTE_TRANSACTION:
          try{
            //Intent 
            //1獲取mActivityCallbacks集合
            Object clientTransactionObject = msg.obj;
            Class<?>clientTransactionClazz = clientTransactionObject.getClass();
            Field mActivityCallbacksField = clientTransactionClazz.getDeclaredField("mActivityCallbacks");
            mActivityCallbacksField.setAccessible(true); 
            List mActivityCallbacks = (List)mActivityCallbacksField.get(clientTransactionObject);
            //2遍歷集合里的元素得到LaunchActivityItem
            for(Object item:mActivityCallbacks ){
              if("android.app.servertransaction.LaunchActivityItem".equals(item.getClass().getName())){
                Field mIntentField = item.getClass().getDeclaredField("mIntent");
                mIntentField.setAccessible(true);

                Intent intent = (Intent)mIntentField.get(item);
                Parcelable actionIntent =intent.getParcelableExtra("actionIntent");
                if(actionIntent !=null){
                  Log.d("z","MyCallback handleMessage intent replaced");
            //3替換LaunchActivityItem的Intent     
                  mIntentField.set(item,actionIntent);
                }
              }
            }     
          }catch(Exception e){
            e.printStackTrace();
          }
          
          break;
      }
      return false;//這里true直接結(jié)束了,return false則執(zhí)行子類的hangleMessage。!
    }
}

面試題 簡(jiǎn)述Activity啟動(dòng)流程

我們從Context的starstActivity說起,其實(shí)現(xiàn)時(shí)ContextImpl的startActivity,然后內(nèi)部通過Instrumentation來嘗試啟動(dòng)Activity,它會(huì)調(diào)用AMS的startActivity方法,這是一個(gè)跨進(jìn)程過程,當(dāng)AMS效驗(yàn)完成Activity的合法性后,會(huì)通過Application回調(diào)到我們的進(jìn)程,也是一次跨進(jìn)程過程,而ApplicationThread就是一個(gè)Binder,毀掉邏輯是在binder線程池中完成的,所以需要通過Handler H將其切換到UI線程,第一個(gè)消息是LAUNCH_ACTIVITY,它對(duì)應(yīng)handleLaunchActivity,在這個(gè)方法里玩成了Activity的創(chuàng)建和啟動(dòng)。

面試題raw目錄和assets目錄有什么區(qū)別

raw:Android 會(huì)自動(dòng)的為目錄中的所有資源文件生成一個(gè)ID,這意味著很容易就可以訪問到這個(gè)資源,甚至在xml中都是可以訪問的,使用ID訪問的速度是最快的

assets:不會(huì)生成ID,只能通過AssetManager訪問,xml中不能訪問,訪問速度會(huì)慢一些,不過操作更加方便。

插件化中的資源加載

Resources資源加載過程分析

我們是否可以new 一個(gè)Resource來加載資源呢?

40.png

Activity構(gòu)建上下文時(shí)也會(huì)構(gòu)建Resources。

ActivityThread-->
創(chuàng)建上下文

41.png
42.png

ResourceManager如何創(chuàng)建的?

實(shí)際上是ResourcesImpl 創(chuàng)建AssetManager,其中指定要去加載資源的路徑

所以我們new Resources()對(duì)象時(shí)指定AssetManager,并且AssetManager指定我們插件資源的路徑,那么這個(gè)Resources對(duì)象就可以加載我們的資源了。

編碼實(shí)現(xiàn)插件資源加載

在PluginManager中寫入方法:

//獲取插件的Resources對(duì)象
public Resources loadResources() throws Exception{
  String pluginApkPath = context.getExternalFilesDir(null).getAbsolutePath()+"/pluginapp-debug.apk";
  AssetManager assetManager = AssetManager.class.newInstance();
  Method addAssetPathMethod = AssetManager.class.getMethod("addAssetPath",String.class);
  addAssetPathMethod.invoke(assetManager,pluginApkPath);
  
  return new Resources(assetManager,context.getResources().getDisplayMetrics(),context.getResources().getConfiguration());
}

那在什么時(shí)候使用?

43.png

Plugin 和宿主在同一個(gè)Application之下

所以在宿主的Application里調(diào)用

修改Application,重寫父類的getResources()

45.png

在插件Activity里 重寫getResource,從Application里拿

44.png

運(yùn)行時(shí),插件會(huì)進(jìn)入到宿主的Application里找到資源

如果好多插件,Resource需要分組。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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