Android插件化原理(一)—— 插件类加载、类加载原理、(双亲委托机制)

记录一下,这是2020年第一篇帖子,今年立了一个flag–经常写帖子。因为疫情的原因,只能每天在家养肚皮,躺床上为社会做贡献。实在是坐不住了,就开始写这篇文章吧。希望新的一年自己越来越厉害,也希望疫情早点过去


(一)什么是插件化

插件化技术最初源于免安装运行apk的想法,这个免安装的apk就可以理解为插件,而支持插件的app我们一般称之为宿主

(二)为什么要插件化

1.app功能模块越来越多,体积越来越大,维护变得困难;
2.模块之间的耦合度高,协同开发沟通成本越来越高;
3.方法数目可能超过65535,APP占用的内存越来越大;
4.增加功能只敢做加法。

(三)插件化和模块(模块)化的爱恨纠葛

  • 组件化开发主要是通过gradle配置,将一个app分成多个模块,每个模块都是一个组件,开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件同意合并成一个apk,这就是组件化开发。
  • 插件化和组件化略有不同,插件化开发是将整个apk拆分成多个模块,这些模块包括一个宿主和多个插件,每个模块都是一个apk,最终打包的时候宿主apk和插件apk分开打包。
  • 插件化其实是建立在模块化基础上的,插件化在实施之前需要先进性模块化。

(四)核心-插件化原理(方案之一)

各大平台方案对比
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
各大平台插件化方案对比
不再一一介绍原理,本次主题是基于 360的DroidPlugin

接下来是插件化的核心:

  • 如何加载插件的类?
  • 如何加载插件的资源?
  • 如何调用插件的类(四大组件)?

一. 如何加载插件的类

1. 类加载器

毫无疑问,类是通过类加载器classLoader加载的,我们先看几种类加载器.

1.1 PathClassLoader和DexClassLoader区别

首先我们来看DexClassLoader:

DexClassLoader 用于加载.jar .apk .dex中的类,也可以用来加载不是应用程序中的代码

api-25-7.1 -->
DexClassLoader-api-25

api-26-8.0 -->

DexClassLoader-api-26

api-28-9.0 -->

DexClassLoader-api-28

可以发现,DexClassLoader里什么也没有做,只是重写了BaseDexClassLoader的构造函数,注意api28里面的这段标红注释:该参数在api 26之后就废弃了,后面会讲到。

再看BaseDexClassLoader

api-25-7.1 -->
在这里插入图片描述

api-26-8.0 & api-28-9.0 -->
BaseDexClassLoader-api26

我们把焦点放在optimizedDirectory上:应该被写入的dex所在的目录。通过构造三个版本的函数可以看出:8.0之前使用了optimizedDirectory参数,8.0及8.0之后不再使用该参数

我们再移步看一下PathClassLoader:PathClassLoader是系统类和系统应用的类加载器:
PathClassLoader
PathClassLoader8.0前后并没有什么区别,同样重写了四个参数的构造函数,optimizedDirectory为null。

总结:

  • 8.0之前,DexClassLoader供系统(谷歌工程师)使用,PathClassLoader提供给开发者使用;
  • 8.0及之后并无区别。后来谷歌工程师越来越觉得写两个加载器简直是闲得蛋疼,干脆就不再区分了。

区别: 在8.0之后并无区别,

1.2 BootClassLoader 和PathClassLoader
/**
*注意这个类派生自AppCompatActivity,而非Activity
*/
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findClassLoader();
    }

    private void findClassLoader() {
        ClassLoader classLoader = getClassLoader();
        while (classLoader !=null){
            Log.d(TAG, "findClassLoader: classLoader : "+classLoader);
            classLoader = classLoader.getParent();
        }
        Log.d(TAG, "findClassLoader: classLoader : "+ Activity.class.getClassLoader());
    }
}

让我的小助理运行一下代码,小手那么一点,运行结果如下所示:

在这里插入图片描述

通过代码,我们可以得知:
MainActivity 的classLoader是 PathClassLoader;Activity 的classLoader是BootClassLoader; PathClassLoader的parent是BootClassLoader。

其实他们两个的作用

  • BootClassLoader – 加载FramWork的class文件;
  • PathClassLoader – 加载应用内的class文件,包含implementation到的第三方库以及.jar、.aar等。

2. 类的生命周期

在这里插入图片描述

这里我们重点了解一下类加载阶段主要做的事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流;
  • 将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构。
  • 在java堆中生成一个代表这个类的Class对象,作为方法区域数据的访问入口。

其它阶段不是我们本章的重点,这里不再赘述,可参考Java类的生命周期


3 加载一个类

注:演示代码是基于Android API 9.0的
注:演示代码是基于Android API 9.0的
注:演示代码是基于Android API 9.0的

重要的话说三遍

3.1 「双亲委托」

首先通过代码演示如何加载一个类来请出一个传说中的大“人物”——双亲委托机制

       String dexPath = "";
       DexClassLoader dexClassLoader = new DexClassLoader(dexPath, this.getCacheDir().getAbsolutePath(),
                    null, getClassLoader());
       Class<?> clazz = dexClassLoader.loadClass("com.margin.plugin.ProxyActivity");

这样,一个类(插件中的类)就被加载成功了。那么,类究竟是如何加载的呢,请小助理位于ClassLoader中的loadClass把源码拿过来:

  • loadClass-1 :
    在这里插入图片描述

首先通过findLoadedClass检查是否已经加载过了;如果没有加载过,检查是否存在parent(注意这个parent父母即双亲,并不是指父类,而是上一级),如果双亲存在则调用parent的loadClass()方法,依次递归;

清楚此流程了,那么一个几乎所有小孩子都问过的问题来了——我是从哪里来的?不不不,是parent从哪里来的😳😳😳???

 DexClassLoader dexClassLoader = new DexClassLoader(dexPath,     this.getCacheDir().getAbsolutePath(),
                    null, getClassLoader());

我们在new DexClassLoader的时候传递的第四个参数就是parent,通过1.2和1.3得知,这个getClassLoader得到的是PathClassLoader,所以这里的classLoader层级关系为:

类加载器parent层级关系

这样,一层一层向上查找的过程,最终到了BootClassLoader的loadClass方法中,我们小助理再来上一下代码:

  • loadClass-2 : bcl-loadClass

注:BootClassLoader 是ClassLoader的一个内部类。

在这个方法中,仍然是先通过findLoadedClass判断是否已经加载过,如果没有,就不再找parent,而是直接调用findClass方法并返回class

在这里插入图片描述

双亲将加载到的class依次向下返回,第一层级的parent检查是否为null,如果为null则调用自己的findClass方法,将结果再次向下返回直至最初的DexClassLoader中的loadClass方法中(代码 loadClass-1),此时,如果双亲找到的class仍然为null,那么,就调用自己的findClass方法查找class

双亲委托机制的流程图

双亲委托机制

总结『双亲委托机制』

  • 1.首先通过findLoadedClass检查是否已经加载过了;
  • 2.如果没有加载过,检查是否存在parent(注意这个parent父母即双亲,并不是指父类,而是上一级),如果双亲存在则调用parent的loadClass()方法,依次递归调用,当到达顶部的时候不再检查双亲而是调用findClass方法并低层返回结果;
  • 3.如果最终都没有查找到或加载成功则调用自身的findClass并返回结果。

为什么使用双亲委托机制?

  • 1.避免重复加载,当双亲加载器已经加载了该类的时候,就没有必要子ClassLoader再次加载;
  • 2.安全性考虑,防止核心API库被随意篡改。

至此,双亲委托机制介绍完毕。

3.2 加载类
3.2.1 查找已加载的类findLoadedClass

cl-findLoadedClass2
vm-findLoadedClass

最终到达Native层,然后先这样再这样在那样,就完成了,哈哈哈哈哈哈

3.2.2 查找类findClass

查看BootClassLoader的findClass方法:
bcl-findClass-2
此处只是重写了ClassLoader的findClass方法,调用了Class的native方法。

我们在看DexClassLoader和PathClassLoader,他们的findClass由其父类BaseDexClassLoader重写:

  • 代码块1
    bdcl-findClass
    这里没有什么特别的,核心是通过pathList.findClass()查找class,pathList是DexPathList的实例,我们接着看这个方法:

  • 代码块2
    DexPathList-findClass

这个方法迭代dexElements,通过Element查找目标class,Element是DexPathList的一个内部类,咱们再看看其方法:

  • 代码块3
    Element-findClass

如果dexFile不为空,则调用dexFile.loadClassBinaryName返回class。dexFile是Element的一个变量,由构造器传入。那么我们就得看看Element从何而来,从上面可知,Element由dexElements而来,那么我们就得看dexElements是怎么来的。

DexPathList的过个构造函数中都有dexElements的创建:
DexPathList-constructor-dexElements

关键是这一行:

    this.dexElements = makeDexElements(splitDexPath(dexPath),optimizedDirectory,suppressedExceptions, definingContext, isTrusted);

可见dexElements由该方法创建的,我们接着查看该方法,在看一下,不妨先看一下第一个参数的方法splitDexPath方法:
DexPathList-splitDexPath

splitDexPath()方法主要作用:通过分隔符把给定的path分成一个个File,并将结果以List的形式返回,它去除不可读,不可用的、常规的文件等等。 知道了splitDexPath()的作用,我们再返回查看makeDexElements()方法:

       /**
     * Makes an array of dex/resource path elements, one per element of
     * the given array.
     */
    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader) {
        return makeDexElements(files, optimizedDirectory, suppressedExceptions, loader, false);
    }


    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;
      /*
       * Open all files and load the (direct or contained) dex files up front.
       */
      for (File file : files) {
          if (file.isDirectory()) {
              // We support directories for looking up resources. Looking up resources in
              // directories is useful for running libcore tests.
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();

              DexFile dex = null;
              if (name.endsWith(DEX_SUFFIX)) {
                  // Raw dex file (not inside a zip/jar).
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      if (dex != null) {
                          elements[elementsPos++] = new Element(dex, null);
                      }
                  } catch (IOException suppressed) {
                      System.logE("Unable to load dex file: " + file, suppressed);
                      suppressedExceptions.add(suppressed);
                  }
              } else {
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  } catch (IOException suppressed) {
                      /*
                       * IOException might get thrown "legitimately" by the DexFile constructor if
                       * the zip file turns out to be resource-only (that is, no classes.dex file
                       * in it).
                       * Let dex == null and hang on to the exception to add to the tea-leaves for
                       * when findClass returns null.
                       */
                      suppressedExceptions.add(suppressed);
                  }

                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      elements[elementsPos++] = new Element(dex, file);
                  }
              }
              if (dex != null && isTrusted) {
                dex.setTrusted();
              }
          } else {
              System.logW("ClassLoader referenced unknown path: " + file);
          }
      }
      if (elementsPos != elements.length) {
          elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }

这个方法里面就是迭代splitDexPath所返回的List,通过file查找到dexFile,然后创建一个Element的实例并将dexFile放入,也就是说,一个dex对应一个Element,再将Element放入elements数组中,最后返回elements,这个elements就对应了一个apk中的所有文件。

class.dex

如果classLoader是PathClassLoader,elements就是应用内的所有类,如果classLoader是DexClassLoader,则需要看创建实例时传递的第一个参数path路径下的。

4. 加载插件中的类

1.创建一个插件:

  • step1:在项目中新建一个module:plugin,随便新建一个类:
public class Test {
    private static final String TAG = "Test";

    public static void sayHi() {
        Log.d(TAG, "sayHi: Hello , this is Plug-in");
    }
}
  • step2:将Test.class打包成dex文件:
    终端定位到:module下javac中的build/intermediates/javac/debug/classes
    然后执行命令: dx --dex --output=test.dex com/margin/plugin/Test.class
    执行完之后可以看到classes目录下多了一个test.dex文件。
    -step3:将dex文件放到sd卡中:

class.dex
-step4:在宿主app中编写代码并运行,别忘了在清单文件中添加文件读写权限,代码中不要忘了写运行时权限检查:

 private void luanchClass() {
        final String dexPath = "/sdcard/test.dex";
        PathClassLoader dexClassLoader = new PathClassLoader(dexPath, getClassLoader());
        try {
            //类的路径
            Class<?> testClazz = dexClassLoader.loadClass("com.margin.plugin.Test");
            //加载到类之后,反射调用其方法
            Method sayHiMethod = testClazz.getMethod("sayHi");
            sayHiMethod.invoke(null);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

    }

加载到类之后,反射调用方法,(别忘了添加文件访问权限),结果如下:
在这里插入图片描述

看到日志的那一瞬间,泪目了,终于类加载成功了,辛辛苦苦养了多年的猪,终于会拱白菜了😀😀😀。
可是,这就完了嘛?当然不是,如果是这么简单,我们就不会在前面长篇大论讲类加载远离了,而且这种方式每次调用都要使用类加载器加载及其不方便,怎么办呢,请看下文。

5.将插件与宿主的类合并

上面第4步,是使用时加载的方式,这种方式很不方便,所以我们换一种方式,加载合并插件,顾名思义,就是在app启动(当然时机是自定的)时将插件全部加载并合并到宿主中。

这种方式的核心思想就是,将插件的类合并到宿主中,具体步骤如下:

  • 新建一个类加载假期,用于加载插件;
  • 反射获取插件的类加载器的的pathList中的dexElements;
  • 获取当前宿主app的类加载器,然后同上一步反射获取其dexElements;
  • 将宿主和插件的dexElements数组合并;
  • 将合并后的dexElements反射设置到宿主的类加载器(DexPathList)中。
    代码如下:
  public static void loadClass(Context context) {
        //Element[] dexElements 是DexPathList的一个变量;
        //DexPathList pathList 是BaseDexClassesLoader的一个变量;
        try {

            //反射获取BaseDexClassLoader
            Class<?> baseDexClassLoaderClazz = Class.forName("dalvik.system.BaseDexClassLoader");

            //1.获取公共的 DexPathList pathList Field
            Field pathListField = baseDexClassLoaderClazz.getDeclaredField("pathList");
            pathListField.setAccessible(true);

            //2.获取公共的 Elementp[] elements Field
            Class<?> dexPathListClazz = Class.forName("dalvik.system.DexPathList");
            Field dexElementField = dexPathListClazz.getDeclaredField("dexElements");
            dexElementField.setAccessible(true);

            //3.创建新插件的类加载器,反射获取插件 类加载器的elements
            DexClassLoader dexClassLoader = new DexClassLoader(apkPath, context.getCacheDir().getAbsolutePath(),
                    null, context.getClassLoader());
            Object pluginPathList = pathListField.get(dexClassLoader);
            Object[] pluginElements = (Object[]) dexElementField.get(pluginPathList);

            //4.获取宿主baseDexClassLoader的elements
            PathClassLoader hostClassLoader = (PathClassLoader) context.getClassLoader();
            Object hostPathList = pathListField.get(hostClassLoader);
            Object[] hostDexElements = (Object[]) dexElementField.get(hostPathList);

            //5.将插件的elements合并到宿主elements中,然后反射重新设置宿主dexElements的值
            Object[] compoundElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(),
                    hostDexElements.length + pluginElements.length);
            System.arraycopy(hostDexElements, 0, compoundElements, 0, hostDexElements.length);
            System.arraycopy(pluginElements, 0, compoundElements, hostDexElements.length, pluginElements.length);

            dexElementField.set(hostPathList, compoundElements);


        } catch (Exception e) {
            e.printStackTrace();
            Log.e(TAG, "loadClass: ", e);
        }
    }

在Applicaion中加载,别忘了把这个Applicaiton写到清单文件里

public class HostApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        LoadUtil.loadClass(this);
    }
}

然后就可以调用目标类了:

   /**
     * 将插件的类合并到宿主内后的加载方式
     */
    private void launchCompoundClass() {
        try {
            Class<?> testClazz = Class.forName("com.margin.plugin.Test");
            Method sayHiMethod = testClazz.getMethod("sayHi");
            sayHiMethod.invoke(null);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

然后运行就可以看到结果了,我就不再贴结果了。

至此,插件化第一步,类加载就完成了。

所有代码见 github: Magic-plug-in

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值