当前位置: 首页 > article >正文

Android-插件化详解

目录

一,定义

二,原理

三,双亲委托机制

四,源码分析

五,插件化原理分析

六,代码实现


一,定义

什么是插件化?插件化就是将整个app拆分成多个模块,这些模块包括一个宿主和多个插件,每一个模块都是一个apk,最终打包的时候宿主apk和插件apk分开打包。

那么这样做的好处是什么呢?

1.减小app的体积,按需下载,用不到某些模块就不需要下载相应的插件apk

2.减小模块之间的耦合度,降低协同开发的成本

3.解决了应用之前的相互调用问题

目前比较流行的插件化框架及其特性如下:

二,原理

首先我们来看一下一个apk文件里面都包含了什么:

可以看到,主要是包含了dex文件和res资源文件等。

我们再看看dex文件里面有什么:

 dex文件里面包含了我们所有的类生成的class文件。

所以说,如果我们要加载插件apk中的某个类的某个方法,只需要加载dex文件,通过反射拿到class对象,就可以调用该方法了。

关于反射可以参考文章Java反射机制详解:Class、构造器、成员变量与方法

那么如何加载dex文件呢? 这就涉及到了android的类加载机制。

三,双亲委托机制

android的类加载器有三种,BootClassLoader,PathClassLoader,DexClassLoader.

其中PathClassLoader和DexClassLoader都是继承自BaseDexClassLoader,下面我们来看看他们的源码:

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

可以看到,他们的区别就是第二个参数 optimizedDirectory,这个参数的意 思是生成的 odex(优化的dex)存放的路径。在8.0(API 26)及之后,二者就完全一样了。

这里需要注意的是,BootClassLoader主要是用来加载SDK的类的,比如android SDK下面的系统的类。而PathClassLoader主要是用来加载应用的类,比如我们手写的类或者应用的第三方库的类(Glide等)。

 我们来看看他们三个类之间的关系:

 下面我们看一下ClassLoader的loadClass方法:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized(this.getClassLoadingLock(name)) {
        Class<?> c = this.findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (this.parent != null) {
                    c = this.parent.loadClass(name, false);
                } else {
                    c = this.findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException var10) {
            }
            if (c == null) {
                long t1 = System.nanoTime();
                c = this.findClass(name);
                PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            this.resolveClass(c);
        }
        return c;
    }
}

首先检测这个类是否已经被加载了,如果已经加载 了,直接获取并返回。如果没有被加载,parent 不 为 null,则调用parent的loadClass进行加载,依次递 归,如果找到了或者加载了就返回,如果即没找到 也加载不了,才自己去加载。

上面代码可以总结为如下的流程:

 需要注意的一点是这里的parent并不是父类的意思,而是类似于链表的结构,parent类似于链表的下一个元素,而BootClassLoader类似于链表的末端,所以它没有parent。

这样做的好处是什么呢?

1, 避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。

2,安全性考虑,防止核心API库被随意篡改。

四,源码分析

这里主要通过源码的角度来分析下插件化的原理,所以只分析findClass部分源码

先看一下BaseDexClassLoader的findClass方法:

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
  List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
  Class c = pathList.findClass(name, suppressedExceptions);
  if (c == null) {
     ClassNotFoundException cnfe = new ClassNotFoundException(
         "Didn't find class \"" + name + "\" on path: " + pathList);
       for (Throwable t : suppressedExceptions) {
           cnfe.addSuppressed(t);
       }
            throw cnfe;
      }
      return c;
}

 然后就走到了pathList的findClass方法,看一下这里的pathList是什么:

public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
  // TODO We should support giving this a library search path maybe.
  super(parent);
  this.pathList = new DexPathList(this, dexFiles);
}

原来是DexPathList,那接下来就看看DexPathList的findClass方法:

private Element[] dexElements;

public Class<?> findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        Class<?> clazz = element.findClass(name, definingContext, suppressed);
        if (clazz != null) {
            return clazz;
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

注意一下这里的Element[]为什么要用一个数组呢,因为我们的apk有时候不止有一个dex文件,可能会有两个或者多个,所以这里采用了数组。

然后接着往下看element.findClass():

private final DexFile dexFile;

public Class<?> findClass(String name, ClassLoader definingContext,
                          List<Throwable> suppressed) {
    return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
            : null;
}

然后就到了DexFile 的findClass方法,下面就是native方法了。

五,插件化原理分析

通过上面源码我们知道,要想加载插件apk的clas文件,最关键的就是通过dexElements这个数组。正常加载自身apk的话,就是直接获取到自身的dexElements,然后加载class就可以了。

但是如果要加载其他apk的class,那么就必须将这个dexElements替换为同时拥有自身dex文件和插件apk的dex文件的数组。且自身的dex文件必须在前面,如果在后面,那就是热修复了。

所以大致的步骤为:

1. 创建插件的 DexClassLoader 类加载器,然后通过反射获取插件的 dexElements 值。

2. 获取宿主的 PathClassLoader 类加载器,然后通过反射获取宿主的 dexElements 值。

3. 合并宿主的 dexElements 与 插件的 dexElements,生成新的 Element[]。

4. 最后通过反射将新的 Element[] 赋值给宿主的 dexElements 。

原来的:

插件化的:

六,代码实现

 1, 获取BaseDexClassLoader类的成员变量pathList

Class<?> clazz = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = clazz.getDeclaredField("pathList");
pathListField.setAccessible(true);

 2,获取自身apk的类加载器

ClassLoader pathClassLoader = context.getClassLoader();

3,获取DexPathList类的对象

Object hostPathList = pathListField.get(pathClassLoader);

4,获取DexPathList的类的成员变量dexElements

Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);

5,获取宿主apk的dexElements对象

Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);

6,我们把插件的apk随便放在sdcard/data/ 下面,然后获取插件的类加载器

ClassLoader dexClassLoader = new DexClassLoader("/sdcard/data/yztest.apk",context.getCacheDir().getAbsolutePath(),
  null, pathClassLoader);

7,获取DexPathList类的对象

Object pluginPathList = pathListField.get(dexClassLoader);

8,获取插件的dexElements对象

Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);

9,创建一个新的数组

Object[] newDexElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(),
                    hostDexElements.length + pluginDexElements.length);

10,将宿主apk的dexElements添加到新数组中

System.arraycopy(hostDexElements, 0, newDexElements,
                    0, hostDexElements.length);

11,将插件apk的dexElements添加到新数组中

System.arraycopy(pluginDexElements, 0, newDexElements,
                    hostDexElements.length, pluginDexElements.length);

12,将新数组赋值到宿主的dexElements中

dexElementsField.set(hostPathList, newDexElements);

我们的插件apk的测试类如下:

public class YZTest {

    public static void testPlugin() {
        System.out.prientln("调用了testPlugin方法");
    }
}

宿主apk可以直接通过反射调用如下:

try {
    Class<?> clazz = Class.forName("com.yuanzhen.plugin.YZTest");
    Method print = clazz.getMethod("testPlugin");
    print.invoke(null);
} catch (Exception e) {
    e.printStackTrace();
}


http://www.kler.cn/a/457029.html

相关文章:

  • 自动化测试-Pytest测试
  • 力扣-数据结构-2【算法学习day.73】
  • 数据结构(哈希表(中)纯概念版)
  • 【ACCSS】2024年亚信安全云认证专家题库
  • Cadence学习笔记 12 PCB初始化设置
  • 【生信圆桌x教程系列】如何安装 seurat V4版本R包
  • vue项目搭建规范
  • Cadence学习笔记 16 HDMI接口布局
  • 续写上一篇《C++学习指南》
  • 深度学习利用Kaggle和Colab免费GPU资源训练
  • Word论文交叉引用一键上标
  • java 构建树型结构
  • 数字设计实验:RISC-V指令单周期CPU
  • 简单的skywalking探针加载原理学习
  • apifox
  • Vulnhub靶场morpheus获得shell攻略
  • spring url匹配
  • WordPress Elementor Page Builder 插件存在任意文件读取漏洞(CVE-2024-9935)
  • python编译为可执行文件
  • 读书笔记-《乡下人的悲歌》