记录一下,这是2020年第一篇帖子,今年立了一个flag–经常写帖子。因为疫情的原因,只能每天在家养肚皮,躺床上为社会做贡献。实在是坐不住了,就开始写这篇文章吧。希望新的一年自己越来越厉害,也希望疫情早点过去
插件化技术最初源于免安装运行apk的想法,这个免安装的apk就可以理解为插件,而支持插件的app我们一般称之为宿主
1.app功能模块越来越多,体积越来越大,维护变得困难;
2.模块之间的耦合度高,协同开发沟通成本越来越高;
3.方法数目可能超过65535,APP占用的内存越来越大;
4.增加功能只敢做加法。
各大平台方案对比
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
不再一一介绍原理,本次主题是基于 360的DroidPlugin
接下来是插件化的核心:
- 如何加载插件的类?
- 如何加载插件的资源?
- 如何调用插件的类(四大组件)?
毫无疑问,类是通过类加载器classLoader加载的,我们先看几种类加载器.
首先我们来看DexClassLoader:
DexClassLoader 用于加载.jar .apk .dex中的类,也可以用来加载不是应用程序中的代码
api-26-8.0 -->
api-28-9.0 -->
可以发现,DexClassLoader里什么也没有做,只是重写了BaseDexClassLoader的构造函数,注意api28里面的这段标红注释:该参数在api 26之后就废弃了,后面会讲到。
再看BaseDexClassLoader:
我们把焦点放在optimizedDirectory上:应该被写入的dex所在的目录。通过构造三个版本的函数可以看出:8.0之前使用了optimizedDirectory参数,8.0及8.0之后不再使用该参数。
我们再移步看一下PathClassLoader:PathClassLoader是系统类和系统应用的类加载器:
PathClassLoader8.0前后并没有什么区别,同样重写了四个参数的构造函数,optimizedDirectory为null。
总结:
- 8.0之前,DexClassLoader供系统(谷歌工程师)使用,PathClassLoader提供给开发者使用;
- 8.0及之后并无区别。后来谷歌工程师越来越觉得写两个加载器简直是闲得蛋疼,干脆就不再区分了。
区别: 在8.0之后并无区别,
/**
*注意这个类派生自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等。
这里我们重点了解一下类加载阶段主要做的事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流;
- 将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构。
- 在java堆中生成一个代表这个类的Class对象,作为方法区域数据的访问入口。
其它阶段不是我们本章的重点,这里不再赘述,可参考Java类的生命周期。
注:演示代码是基于Android API 9.0的
注:演示代码是基于Android API 9.0的
注:演示代码是基于Android API 9.0的
重要的话说三遍
首先通过代码演示如何加载一个类来请出一个传说中的大“人物”——双亲委托机制:
String dexPath = "";
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, this.getCacheDir().getAbsolutePath(),
null, getClassLoader());
Class<?> clazz = dexClassLoader.loadClass("com.margin.plugin.ProxyActivity");
这样,一个类(插件中的类)就被加载成功了。那么,类究竟是如何加载的呢,请小助理位于ClassLoader中的loadClass把源码拿过来:
首先通过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层级关系为:
这样,一层一层向上查找的过程,最终到了BootClassLoader的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库被随意篡改。
至此,双亲委托机制介绍完毕。
最终到达Native层,然后先这样再这样在那样,就完成了,哈哈哈哈哈哈
查看BootClassLoader的findClass方法:
此处只是重写了ClassLoader的findClass方法,调用了Class的native方法。
我们在看DexClassLoader和PathClassLoader,他们的findClass由其父类BaseDexClassLoader重写:
这个方法迭代dexElements,通过Element查找目标class,Element是DexPathList的一个内部类,咱们再看看其方法:
如果dexFile不为空,则调用dexFile.loadClassBinaryName返回class。dexFile是Element的一个变量,由构造器传入。那么我们就得看看Element从何而来,从上面可知,Element由dexElements而来,那么我们就得看dexElements是怎么来的。
DexPathList的过个构造函数中都有dexElements的创建:
关键是这一行:
this.dexElements = makeDexElements(splitDexPath(dexPath),optimizedDirectory,suppressedExceptions, definingContext, isTrusted);
可见dexElements由该方法创建的,我们接着查看该方法,在看一下,不妨先看一下第一个参数的方法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中的所有文件。
如果classLoader是PathClassLoader,elements就是应用内的所有类,如果classLoader是DexClassLoader,则需要看创建实例时传递的第一个参数path路径下的。
1.创建一个插件:
public class Test {
private static final String TAG = "Test";
public static void sayHi() {
Log.d(TAG, "sayHi: Hello , this is Plug-in");
}
}
-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();
}
}
加载到类之后,反射调用方法,(别忘了添加文件访问权限),结果如下:
看到日志的那一瞬间,泪目了,终于类加载成功了,辛辛苦苦养了多年的猪,终于会拱白菜了。
可是,这就完了嘛?当然不是,如果是这么简单,我们就不会在前面长篇大论讲类加载远离了,而且这种方式每次调用都要使用类加载器加载及其不方便,怎么办呢,请看下文。
上面第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