【长篇】插件化架构设计

一次让你彻底掌握Android插件化架构设计

插件化简介

宿主host 与 插件(免安装:不需要安装apk,下载即可)

-->插件加载
-->插件化中的组件支持(startActivity如何去启动)
-->插件化中资源(布局文件图片)的加载

插件化优点

1减小apk的体积,按需求下载模块
2动态更新插件
3宿主和插件分开编译,提升团队开发效率
4解决方法数查过65535问题

缺点:
项目复杂度变高了,难度变高,版本兼容问题(插件化的兼容)

组件化和插件化区别

组件化:是将一个APP分成多个模块,每个模块都是一个组件(module),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件,但是最终发布的时候将这些组件合并成一个统一的APK。

插件化:是将整个APP拆分成很多模块,每个模块都是一个APK(组件化的每个模块是一个Lib)最终打包的时候将宿主APK和插件APK分开打包,插件APK通过动态下发到宿主APK。

插件化框架对比

1.png

选中DroidPlugin
因为360大厂,四大组件全支持,插件不需在清单文件注册,最重要的是几乎全部支持Android特性。

插件化架构的设计思路

2.png

面试题 简述Java类加载的过程

3.png

加载阶段,虚拟机主要完成三件事:
1通过一个类的权限定名来获取定义此类的二进制字节流(class文件—>字节流);
2将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构(字节流-->对应的数据结构);
3在java堆中生成一个代表这个类的class对象,作为方法区域数据的访问入口。

验证:是否符合java字节码编码规范
初始化完就会得到class对象(一切反射的基石)

Android中类加载器的集成结构

4.png

常用的类加载器

BootClassLoader:系统启动时用于加载系统常用类,ClassLoader内部类;

PathClassLoader:加载系统类和应用程序类,一般不建议开发者使用;

DexClassLoader:加载DEX文件及包含dex文件的apk或jar。也支持从SD卡进行加载,这也意味着DexClassLoader可以在应用未安装的情况下加载dex相关文件。因此它是热修复和插件化技术的基础。

验证上述的Demo

5.png

使用一个dexclassloader:

7.png
6.png

记得给读写权限!!

最后参数父类为什么用pathClassLoader而不是BaseDexClassLoader,之后再说(在下一节)

面试题:双亲委派机制与自定义String类

8.png
9.png

流程:
DexClassLoader加载前问PathClassLoader你加载过吗,如果没有,PathClassLoader问BootClassLoader你加载过吗,如果没有,BootClassLoader问是够可以加载,能加载自己就加载了。不能再向下回传。

双亲委派机制优点:
避免重复加载,若已经加载直接从缓存中读取。
更加安全,避免开发者修改系统类。

参数父类为什么用pathClassLoader而不是BaseDexClassLoader:根据ClassLoader继承图他们俩不是继承关系,这个方法参数里parent不是父类的意思(父类是super),这里是优先级、上一层的意思。

当发生Activity跳转时自动加载插件Activity

10.png

如何手动 变 自动

即:把插件的classloader由DexClassloader变成 pathClassLoader,让其认为是自己人。就会自动加载了。

11.png

为什么行得通:DexClassloader和PathClassLoader都是调用的一样的父类方法,区别就是8.0之前可以指定生成后的odex目录,8.0之后都是系统目录了。

加载指定类的时序图

12.png

BaseDexClassLoader有一个DexPathList属性,调用findClass遍历dexElements。

搞清时序图是为了Activity跳转时能自动加载。我们知道了pathClassLoader的dex都放到哪里了。然后把dexClassLoader放进去。

App里的dex是用一个Element[ ] dexElements数组来存放的。
*为什么是数组?为了分包,一个app可以有多个dex。

插件dex的处理

即上面说的流程
把dexClassLoader放进pathClassLoader的Element[ ] dexElements数组

13.png

如果想改dexElements数组要用到Hook技术。

Hook与Hook技巧

14.png

Hook技巧:
1要掌握反射和代理模式;
2尽量Hook静态变量或者单例对象;(因为静态变量不用实例化一个对象)
3尽量Hook public的对象和方法。(如果是private容易搞坏内部结构)

插件化架构的模块关系

创建两个module

15.png
16.png
17.png

如何加载插件、插入dexElements数组等复杂工作都会在PluginCore里去完成

编码实现宿主与插件dex数组合并

插入dexElements数组在什么时候比较好呢?
应用程序启动时候最好。即Application
配置权限

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

结果:

非常轻松地获得插件的class

25.png

接下来是如何启动我们的组件。Activity如何跳转的

Activity启动中的跨进程访问

我们试图这样来做跳转:

26.png

报错了:

27.png

提示没有在宿主里注册(虽然在插件自己的里面注册了)

28.png

Activity1 --》Activity2 要走两次跨进程访问

AMS会检查我们的activity是否在清单文件里完成注册。所以报错了

Hook在插件化架构中的运用

29.png

我们写一个RegisteredActivity,里面什么都不需要,只要在清单文件里注册。

分析activity启动流程

30.png
31.png

IActivityManager 就是AMS对象

我们hook这个IActivityManager 或者直接拿到mInstance这个属性 即可拿到AMS对象

Hook AMS

32.png
33.png
public class HookUtils{
  //hook 我们的AMS对象,对其startActivity拦截
  //把里面intent处理
  public static void hookAMS(Context context) throws Exception{
    //1.获取AMS对象
    //1.1获取静态属性ActivityManager.IActivityManager Singleton的静态属性值
    //它是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对AMS对象进行代理
    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对AMS对象的方法进行拦截

  }

}
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参数
      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思路

现在等AMS验证完后,我们什么时候把想要启动的拿出来?

34.png

dispatchMessage会判断callback是否为空
如果不为空,会执行callback的handlerMessage,然后在执行子类的handlerMessage.
所以我们给callback一个值,先执行callback里handlerMessage,修改我们需要的内容。

ActivityThread有一个H 内部类 继承自handler,里面记录了hangler的信息(activityInfo)。

Handler发消息,looper轮询到 后都会dispatchMessage,里面判断mCallback是否为空。(一般都为空)

35.png

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

编码实现Hook Handler

在上面HookUtils里加入一个方法

//hook 获取到 Handler的特定消息(LAUNCH_ACTIVITY)中的intent,进行处理。
//将intent对象里的RegisteredActivity替换成PluginActivity

public static void hookHandler() throws Exception{
  //1.获取到handler对象(mH属性值)
  //1.1获取到ActivityThread对象
  Class<?>  activityThreadClazz= Class.forName("android.app.ActivityThread");
  //拿到activityThread对象,他有一个静态的属性(sCurrentActivityThread)
  Field sCurrentActivityThreadField = activityThreadClazz.getDeclaredField("sCurrentActivityThread");
  sCurrentActivityThreadField.setAccessible(true);
  Object activityThreadObject = sCurrentActivityThreadField.get(null);

  //1.2获取ActivityThread对象的mH属性值
  Field mHField = activityThreadClazz.getDeclaredField("mH");
  mHField.setAccessible(true);
  Object handler = mHField.get(activityThreadObject);

  //2.给我们的Handler的mCallBack属性进行赋值
  Field mCallbackField = Handler.class.getDeclaredField("mCallback");
  mCallbackField .setAccessible(true);


  //3.在callback里面将intent对象里的RegisteredActivity替换成PluginActivity
    //创建了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直接结束了,return false则执行子类的hangleMessage。!
    }
}

AMS版本适配

37.png

修改之前代码

  public static void hookAMS(Context context) throws Exception{
    //1.获取AMS对象
    //1.1获取静态属性ActivityManager.IActivityManager Singleton的静态属性值
    //它是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直接结束了,return false则执行子类的hangleMessage。!
    }
}

面试题 简述Activity启动流程

我们从Context的starstActivity说起,其实现时ContextImpl的startActivity,然后内部通过Instrumentation来尝试启动Activity,它会调用AMS的startActivity方法,这是一个跨进程过程,当AMS效验完成Activity的合法性后,会通过Application回调到我们的进程,也是一次跨进程过程,而ApplicationThread就是一个Binder,毁掉逻辑是在binder线程池中完成的,所以需要通过Handler H将其切换到UI线程,第一个消息是LAUNCH_ACTIVITY,它对应handleLaunchActivity,在这个方法里玩成了Activity的创建和启动。

面试题raw目录和assets目录有什么区别

raw:Android 会自动的为目录中的所有资源文件生成一个ID,这意味着很容易就可以访问到这个资源,甚至在xml中都是可以访问的,使用ID访问的速度是最快的

assets:不会生成ID,只能通过AssetManager访问,xml中不能访问,访问速度会慢一些,不过操作更加方便。

插件化中的资源加载

Resources资源加载过程分析

我们是否可以new 一个Resource来加载资源呢?

40.png

Activity构建上下文时也会构建Resources。

ActivityThread-->
创建上下文

41.png
42.png

ResourceManager如何创建的?

实际上是ResourcesImpl 创建AssetManager,其中指定要去加载资源的路径

所以我们new Resources()对象时指定AssetManager,并且AssetManager指定我们插件资源的路径,那么这个Resources对象就可以加载我们的资源了。

编码实现插件资源加载

在PluginManager中写入方法:

//获取插件的Resources对象
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());
}

那在什么时候使用?

43.png

Plugin 和宿主在同一个Application之下

所以在宿主的Application里调用

修改Application,重写父类的getResources()

45.png

在插件Activity里 重写getResource,从Application里拿

44.png

运行时,插件会进入到宿主的Application里找到资源

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容