插件化介绍和原理解析

什么是插件化

首先我们区分一下组件化和插件化的概念

  1. 组件化
    组件化开发就是将一个app分成多个模块,组件化强调功能拆分,单独编译,单独开发,根据需求动态配置组件。
  2. 插件化
    插件化是将一个apk根据业务功能拆分成不同的子apk,插件化更关注动态加载、热更新。
  3. 热修复
    热修复强调的是在不需要二次安装应用的前提下修复已知的bug。


    组件化和插件化.png

    热修复基本原理.png
堆比.png

插件化的优点

  1. 宿主和插件分开编译
  2. 并发开发
  3. 动态更新插件
  4. 按需下载模块
  5. 方法数或变量数爆棚
  6. 插件无需安装即可运行

插件化发展历程

image.png
  1. 静态代理
    dynamic-load-apk最早使用ProxyActivity这种静态代理技术,由ProxyActivity去控制插件中PluginActivity的生命周期
  2. 动态替换(HOOK)
    在实现原理上都是趋近于选择尽量少的hook,并通过在manifest中预埋一些组件实现对四大组件的动态插件化。像Replugin。
  3. 容器化框架
    VirtualApp能够完全模拟app的运行环境,能够实现app的免安装运行和双开技术。Atlas是阿里的结合组件化和热修复技术的一个app基础框架,号称是一个容器化框架。

插件化框架对比

插件化框架对比.png

插件化技术原理

实现插件化需要解决的问题

  1. 插件类的加载,解决宿主加载插件以及插件加载宿主的问题
  2. 资源文件的加载,解决宿主和插件的资源文件的加载问题,以及资源合并和资源冲突的问题
  3. 四大组件的支撑,支撑包括Activity,BroadReceiver. ContentProvider,Service四大组件在插件中的正常使用

类加载原理

classloader介绍

Classloader介绍.png

其中:

  1. BootClassLoader
    和java虚拟机中不同的是,BootClassLoader是ClassLoader内部类,由java代码实现而不是c++实现,是Android平台上所有ClassLoader的最终parent,这个内部类是包内可见。
  2. BaseDexClassLoader
    负责从指定的路径中加载类,加载类里面的各种校验、检查和初始化工作都由它来完成
  3. PathClassLoader
    继承自BaseDexClassLoader,只能加载已经安装到Android系统的APK里的类,主要逻辑由BaseDexClassLoader实现
  4. DexClassLoader
    继承自BaseDexClassLoader,可以加载用户自定义的其他路径里的类,主要逻辑都由BaseDexClassLoader实现。

双亲委派模型

含义:双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。

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
                  // 如果都没有找到,则通过自定义实现的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. 带有优先级的层次关系,通过这种层级关可以避免类的重复加载;
  2. 其次是考虑到安全因素,java核心API中定义类型不会被随意替换。

如何动态加载APK的类文件?

主要依赖上述DexClassLoader:
我们看下DexClassLoader的构造方法:
DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
dexPath:要加载的类所在的jar或者apk文件路径,类装载器将从该路径中寻找指定的目标类,该类必须是APK或jar的全路径
optimizedDirectory:odex优化之后的dex存放路径,真正的数据是从这个位置的dex文件加载的,由于ClassLoader只能加载内部存储路径中的dex文件,所以这个路径必须为内部路径
librarySearchPath:目标类中所使用的C/C++库存放的路径
classloader:本装载器的父装载器,一般使用当前执行类的装载器就可以了,在Android用context.getClassLoader()就可以了

加载样例如下:

private void loadClass() {
    // 获取推送到SDCard中的插件路劲
    String apkPath = Environment.getExternalStorageDirectory() + File.separator + "test.apk";
    // 优化后的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 {
        // 从优化后的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构造函数
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(null);
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }

其中真正去进行资源加载的为AssetManager 。
AssetManager的addAssetPath()方法添加系统资源和apk资源,并构造Resource提供给Context上下文进行使用,所以真正加载资源是通过AssetManger去加载。

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

思路

1. 反射调用AssetsManager的addAssetPath方法;
2. 将外部的apk路径添加进去,构建新的Resource对象
3. 通过classloader加载R.java获取drawable,对应的id
4. 通过上述构建的Resource获取drawable对象。

/**
* 反射添加资源路径,并创建新的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();
        //创建Resources
        Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(),
                superRes.getConfiguration());
        return mResources;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}


/**
* 1. 先获取资源的名称对应的id(通过反射R.java文件的变量)
* 2.  再根据我们构造的Resources 获取对应的资源对象。我
*/
public Drawable getApkDrawable(String drawableName){
    try {
        DexClassLoader dexClassLoader = new DexClassLoader(apkDir+File.separator+apkName,
        optimizedDirectoryFile.getPath(), null, context.getClassLoader());
 
        //通过使用apk自己的类加载器,反射出R类中相应的内部类进而获取我们需要的资源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时加入所有插件和主工程的路径;优点是插件和宿主可以相互访问,缺点是可能产生资源冲突。
独立式:各个插件只添加自己apk路径。不存在资源冲突,但是无法资源共享。

合并式资源冲突的解决方案:
修改aapt源码,定制aapt工具编译期间修改PP段
修改aapt的产物,即,编译后期重新整理插件Apk的资源,编排ID

插件Activity处理方案

代理模式

代理模式的定义:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。


代理模式.png

动态代理的具体实现参考如下:

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

如何动态加载插件中Acitivity

Activity插件化需要解决的问题:

  1. 怎么欺骗AMS去启动一个清单文件不存在的Activity ;
  2. 插件Activity的生命周期如何实现;
  3. 插件apk中用过的各种资源,如何动态的加载资源。
代理模式(DL框架)

ProxyActivity + 插件中没注册的Activity = 标准的Activity

代理模式插件化原理.png

主要流程如下:

  1. 宿主中通过启动ProxyAcitivity
  2. 代理activity通过AIDL通信和插件PluginActivity建立联系
  3. 当宿主中的代理ProxyAcitivity生命周期发生变化的时候,通过AIDL通知到PluginActivity。从而完成插件Activity生命周期的同步。
坑位占用模式

在AndroidManifest中注册,但并没有真实的实现 类,只作为其他Activity启动的坑位,通过HOOK AMS去加载插件中的Activity的class文件。

下面我们来介绍一下Replugin的Activity原理

Replugin原理.png
  1. Pmbase根据Intent找到对应的插件
  2. 分配坑位Activity,与插件中的Activity建立一对一的关系并保存在PluginContainer中
  3. 让系统启动坑位Activity,因为它是在Manifest中注册过的
  4. Android系统会尝试使用RepluginClassLoader加载坑位Activity的Class对象
  5. RepluginClassLoader 通过建立的对应关系找到插件Activity,并使用
    PluginDexClassLoader 加载插件Activity 的Class对象并返回
  6. Android系统就使用这个插件中的Activity的Class对象来运行生命周期函数
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容