Android插件化学习
文章目录
前言
什么是插件?
免安装运行的apk,这个apk就属于插件
能解决的问题:
1.原始app体积太大,使用插件化可以减少app体积大小
2.解耦
3.方法数超过65535 app占用内存过大
4.多个应用之间相互调用
插件化和组件化的区别
组件化:多个模块 相互依赖 单独调试 最后统一打包合并成一个apk文件
插件化:多个模块 这些模块包含一个宿主 和多个插件,每一个模块都是一个apk ,最终打包的时候宿主apk和插件apk分开打包,使用的时候加载对应插件就可以了
插件原理
使用类加载器加载外部的apk或者dex文件到项目中,然后项目调用外部的方法
一、双亲委托机制
1.结构
简单的获取方法
ClassLoader classLoader = getClassLoader();
PathClassLoader pathClassLoader = new PathClassLoader("",classLoader);
DexClassLoader dexClassLoader = new DexClassLoader("","","",classLoader);
PathClassLoader和DexClassLoader 在8.0之前有区别,8.0以后没有区别了
8.0之前 :PathClassLoader是系统加载器,加载系统的, DexClassLoader是外部加载器
8.0之后:PathClassLoader有两个构造方法,一个是加载系统的,一个是兼容DexClassLoader的,和DexClassLoader的一个构造方法一样
2.加载过程
首先检测这个类是否已经被加载了,如果已经加载了,直接获取并返回,如果没有被加载,parent不为null,则调用parent的loadClass进行加载,依次递归,如果找到了或者加载了就返回,如果没有找到也加载不了,才自己去加载,这个过程就是双亲委托机制
为什么这么做:
1.避免重复加载,当父类加载器已经加载了该类的时候,就没有必要子ClassLoader在加载一次
2.安全性考虑,防止核心API库被随意篡改
在ClassLoader类中,loadClass这个方法就有体现:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
//parent不为null,则调用parent的loadClass进行加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//等于空 走findclass
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
//不为空 直接返回
return c;
}
3.测试打印classLoader
代码打印classLoader,打印发现在最终加载的就是BootClassLoader加载的
ClassLoader classLoader = getClassLoader();
while (classLoader!= null){
Log.e("---->" ,"1111: "+classLoader);
classLoader = classLoader.getParent();
Log.e("---->" ,"2222: "+classLoader);
}
Log.e("---->" ,"3333: "+ Activity.class.getClassLoader());
Log.e("---->" ,"4444: "+AppCompatActivity.class.getClassLoader());
二、插件化加载
1.类的加载
加载阶段,虚拟机干了什么事情?
1.通过一个类的全限定名来获取此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为方法区域的运行是数据结构
3.在java堆中生成一个代表这个类的class对象,作为方法区域的数据的访问入口
2.反射
在运行状态中, 对于任意一个类, 都能够知道这个类的所有属性和方法; 对于任意一个对象, 都能够调用它的任意一个方法和属性。
3.插件化方案第一种:加载外部dex文件
1.生成一个点dex包 ,将class字节码文件生成dex文件
(1)先创建插件类:
public class Test {
public static void print(){
Log.e("---->" ,"打印:我是插件化类的测试类");
}
}
(2)然后运行,查看字节码文件:
在build-intermediates-javac里面找到Test字节码文件,然后在找到D:\ruanjian\Sdk\build-tools\下的任意目录,找到dx.bat这个文件,这个就是生成dex的文件
dex文件生成命令:
dx --dex --output=Test.dex com/renbin/plugin/Test.class
(3) 把com目录文件夹复制到build-tools下的任意文件夹内
(4)在复制的根目录下打开CMD黑窗口,执行dex文件生成命令
然后在本目录下,会生成一个的dex文件
然后把这个dex文件上传到手机SD卡中
2.把dex文件加载到类加载器中
3.不能影响自身的类加载器
public void PathClassLoader(View view) {
try {
PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/Test.dex", MainActivity.class.getClassLoader());
//已经加载了dex,去调用方法 反射获取class文件
Class<?> clazz = pathClassLoader.loadClass("com.renbin.plugin.Test");
//获取执行print方法
Method method = clazz.getMethod("print");
method.invoke(null);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
测试打印:发现已经打印出print结果,说明加载dex文件成功
4.插件化方案第二种:加载外部apk文件
1.加载单个具体文件
和加载dex方法一样,弊端:这是最简单的加载方式,只能加载普通类。这只能加载一个类
public void PathClassLoaderApk(View view) {
try {
PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/plugin-debug.apk", MainActivity.class.getClassLoader());
//已经加载了dex,去调用方法 反射获取class文件
Class<?> clazz = pathClassLoader.loadClass("com.renbin.plugin.Test");
//获取执行print方法
Method method = clazz.getMethod("print");
method.invoke(null);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
2.加载包里有多个文件,编写apk加载类
实现步骤:
- 创建插件的 DexClassLoader 类加载器,然后通过反射获取插件的 dexElements 值。
- 获取宿主的 PathClassLoader 类加载器,然后通过反射获取宿主的 dexElements 值。
- 合并宿主的 dexElements 与 插件的 dexElements,生成新的 Element[]。
- 最后通过反射将新的 Element[] 赋值给宿主的 dexElements 。
package com.renbin.uidrawdemo;
import android.content.Context;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
/**
* data:2021-08-24
* Author:renbin
*/
public class LoadUtil {
private final static String apkpath = "/sdcard/plugin-debug.apk";
//加载所有类
public static void loadClass(Context context) {
try {
//dexElements含有APk文件内所有的dex文件
//获取 BaseDexClassLoader含有DexPathList ,DexPathList含有dexElements 最后拼接dexElements(外部+内部)
//1.得到内部的DexPathList,通过反射BaseDexClassLoader类得到
Class<?> baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
//因为在baseDexClassLoaderClass类中,private final DexPathList pathList; pathList是私有的,所以需要设置可访问的状态
pathListField.setAccessible(true);
//2.得到DexPathList里面的 dexElements,通过反射类DexPathList得到
Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
//获取外部apk文件的加载器(插件)
DexClassLoader dexClassLoader = new DexClassLoader(apkpath, context.getCacheDir().getAbsolutePath(), null, context.getClassLoader());
//获取外部apk文件的PathList(插件)
Object pluginPathList = pathListField.get(dexClassLoader);
//获取外部apk文件的 dexElements的数组(插件)
Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);
//获取宿主的类加载器
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
//获取宿主的PathList集合
Object hostPathList = pathListField.get(pathClassLoader);
//获取宿主的dexElements数组
Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);
//拼接在一起
Object[] dexElements = (Object[]) Array.newInstance(pluginDexElements.getClass().getComponentType(),pluginDexElements.length+hostDexElements.length);
//拷贝数据
System.arraycopy(hostDexElements,0,dexElements,0,hostDexElements.length);
System.arraycopy(pluginDexElements,0,dexElements,hostDexElements.length,pluginDexElements.length);
//赋值给系统的dexElements
dexElementsField.set(hostPathList,dexElements);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
使用:现在application初始化
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
LoadUtil.loadClass(this);
}
}
调用:
public void loadUtil(View view) {
try {
Class<?> clazz = Class.forName("com.renbin.plugin.Test");
//获取执行print方法
Method method = clazz.getMethod("print");
method.invoke(null);
Class<?> clazz2 = Class.forName("com.renbin.plugin.Test2");
//获取执行print方法
Method method2 = clazz2.getMethod("print");
method2.invoke(null);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
打印:说明加载apk成功,可以调用里面的任意方法