前言
什么是插件化技术
插件化技术最初源于免安装运行apk的想法,对于免安装的apk可以理解为插件,而支持插件的app称为宿主app,宿主app在运行时加载运行插件apk,这个过程称之为插件化;
插件化的好处
- 减小安装包的大小;(相较于组件化需要所有组件都打包进apk,插件化更灵活,需要加载运行插件时才去安装)
- 实现app功能的动态扩展;(一个宿主可以支持多个插件apk,可插拔,拓展性更强)
类加载机制源码分析
java类加载与android类加载区别
我们知道,java源码文件编译后会生成一个个class文件,而在Android中,代码编译后会生成一个apk文件,将apk文件解压后可以看到有一个或多个dex文件,它就是安卓把所有的class文件进行合并,优化后生成的;
在java中JVM加载的是class文件,而安卓中DVM和ART加载的dex文件,两者都是通过ClassLoader
进行加载的,但还是有些区别的,我们这里主要看下安卓的ClassLoader
是如何加载dex文件的;
ClassLoader类关系
ClassLoader
是一个抽象类,实现类主要分为两种:系统类加载器和自定义类加载器;
而系统类加载器主要包括三种
1.BootClassLoader,用于加载Android SDK层的class文件;
2.PathClassLoader,用于Android应用程序类加载器,可以加载指定的dex、jar、zip、apk中的class.dex文件;
3.DexClassLoader,用于加载指定的dex、jar、zip、apk中的class.dex文件;
相关类继承关系如下:
我们来看下DexClassLoader和PathClassLoader源码
### PathClassLoader
package dalvik.system;
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
public PathClassLoader(
String dexPath, String librarySearchPath, ClassLoader parent,
ClassLoader[] sharedLibraryLoaders) {
super(dexPath, librarySearchPath, parent, sharedLibraryLoaders);
}
}
### DexClassLoader【9.0】
package dalvik.system;
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
### DexClassLoader【8.0及以前】
package dalvik.system;
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
注意
在8.0之前,他们二者唯一的区别就是第二个参数optimizedDirectory
,这个参数的含义是生成的odex【优化后dex】存放的路径,PathClassLoader
中直接为null,而DexClassLoader
是使用用户传递进来的路径,而在8.0以后,二者实现完全一致;
那我们如果使用类加载器去加载一个类呢?接下来我们先看下源码中是如何实现加载dex文件的;
类加载原理
我们直接找到类加载方法调用的地方ClassLoader.loadClass
方法
### java.lang.ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 首先检查class是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//如果parent【成员变量,类型是ClassLoader】不为null,则调用parent的loadClass去加载
c = parent.loadClass(name, false);
} else {
//正常情况下不会走这里,因为BootClassLoader重写了loadClass方法,结束了递归
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
//如果仍然没找到,就调用自己的findclass方法去查找;
c = findClass(name);
}
}
return c;
}
小结
首先检查当前类是否已经被加载,如果已经加载,直接获取并返回,如果没有被加载,parent不为null,则调用parent.loadClass
进行加载,依次递归,如果找到了或者加载了就返回,如果没有找到也没有加载,才自己去加载,这个过程就是我们常说的双亲委派机制
双亲委派机制
的好处:
1.避免类重复加载;【已加载的类不会再次加载】
2.安全性;【例如自己重写Activity.class方法瞎比搞,系统也不会加载你的类,保证了类加载的安全】
接下来,我们看下当所有的parent
都没有加载成功时,DexClassLoader
是如何加载的,我们发现它的父类BaseDexClassLoader
中,重新了findClass
方法
### BaseDexClassLoader
public BaseDexClassLoader(ByteBuffer[] dexFiles, String librarySearchPath, ClassLoader parent) {
super(parent);
this.sharedLibraryLoaders = null;
//初始化pathList
this.pathList = new DexPathList(this, librarySearchPath);
this.pathList.initByteBufferDexPath(dexFiles);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
...
//在pathList中查找指定的class
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
方法
### DexPathList
//dex文件数组
private Element[] dexElements;
public Class<?> findClass(String name, List<Throwable> suppressed) {
//在dex文件数组中遍历查找class对象
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;
}
至此,我们发现Class对象
就是从Element中获取的,每一个Element就对应一个dex文件,因为我们的dex文件可能存在多个,这里就对应数组Elements[]
;
反射实现插件类方法调用
明白了类加载的原理,我们自己尝试模拟宿主app中加载插件apk,并调用其中的class方法;
对应步骤如下:
1.获取宿主的ClassLoader
类加载器,通过反射获取宿主对应的dexElements
的值;
2.获取插件的ClassLoader
类加载器,通过反射获取插件对应的dexElements
的值;
3.合并宿主的dexElements
和插件的dexElements
数组,生成新的dexElements
数组;
4.赋值替换宿主的原有的dexElements
数组;
5.根据插件中调用类的包名,反射加载该类,调用其方法;
- 我们首先创建插件app,里面就一个
Print
类对应print打印方法; - 打包插件app,生成
plugin.apk
放到sdcard
目录下;【开发环境下应该从云端下载存放到/data/data/包名/目录下,这里为了省事】 - 利用反射技术合并宿主和插件dex文件
private fun mergeDexFile() {
//先获取pathList字段
val baseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader")
//是属于类的字段,可共用
val pathListField = baseDexClassLoader.getDeclaredField("pathList")
pathListField.isAccessible = true
//获取dexElements字段
val dexPathList = Class.forName("dalvik.system.DexPathList")
//是属于类的字段,可共用
val dexElements = dexPathList.getDeclaredField("dexElements")
dexElements.isAccessible = true
//获取宿主的类加载器
val hostPathList = pathListField.get(classLoader)
//获取宿主中的dexElement[]
val hostDexElements: Array<Any> = dexElements.get(hostPathList) as Array<Any>
//获取插件的类加载器,这里apk我们为了方便放在sdcard卡下,正常开发应放在/data/data/包名文件夹下
val pluginClassLoader =
DexClassLoader("/sdcard/plugin.apk", cacheDir.absolutePath, null, classLoader)
//插件对应的pathList对象
val pluginPathList = pathListField.get(pluginClassLoader)
//插件对应的dexElements数组
val pluginDexElements = dexElements.get(pluginPathList) as Array<Any>
//进行数组合并
val resultElements = java.lang.reflect.Array.newInstance(
hostDexElements.javaClass.componentType,
hostDexElements.size + pluginDexElements.size
) as Array<Any>
System.arraycopy(hostDexElements, 0, resultElements, 0, hostDexElements.size)
System.arraycopy(
pluginDexElements,
0,
resultElements,
hostDexElements.size,
pluginDexElements.size
)
//赋值给宿主dexElements数组
dexElements[hostPathList] = resultElements
}
- 验证插件中
Print.class
是否已加载;
private fun loadPluginDexFile() {
var clazz = Class.forName("com.dongxian.plugin.Print")
var newInstance = clazz.newInstance()
val method = clazz.getMethod("print")
//如果print是静态方法,则直接传null即可;
method.invoke(newInstance)
}
- 结果打印输出
小结
通过上面的内容,我们了解了类加载机制的原理,明白了双亲委派机制,对插件化的有了进一步的认知!
如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )