【Android】 插件化原理
什么是插件化?
也叫动态加载技术,应用在不发布新版本的情况下更新,增加或者移除某些功能模块的技术;
对Android来说插件化通常是指将App拆分成多个功能模块,包括一个宿主和多个插件,宿主的形式是APK,插件的形式可以是APK(未安装), zip,Jar, dex;
Android的插件化最主要两个功能:
免安装,免修改;
区别于组件化:
组件化是将一个App分成多个模块,每个模块都是一个组件(Module),开发的过程中让这些组件相互依赖,并且可以单独开发和调试单个组件,但最终发布的是所有模块合并打包的APK。
插件化可以解决什么问题?
-
并发开发
由于每个插件相对独立,所以可以针对每个插件单独开发和调试,这样可大幅度提高APP开发和迭代速度; -
宿主和插件分开编译
宿主和插件编译都是独立的,插件App编译完后期再下发到宿主App里,可加快编译速度,提高开发效率; -
动态更新插件
插件App出问题或者更新新功能时可以直接下发到宿主App里,不用更新发版宿主App; -
按需下载模块
插件模块可按需下载,减少宿主Apk的大小; -
解决方法数爆炸问题
插件化的发展历史?
从2012年开源的第一款插件化AndroidDynamicLoader以来,到现在已经有十几款优秀的插件化框架;
几款比较常用的插件化框架(按时间顺序):
-
2014年3月,dynamic-load-apk (非Hook方案)
-
2015年8月,VritualApp (虚拟机技术)
-
2015年8月,DroidPlugin (Hook系统AIDL接口方法)—360张勇
-
2017年3月,阿里巴巴 Atlas
-
2017年6月,360手机卫士 RePlugin(唯一Hook方案)
插件化原理
插件化需要解决的问题
-
插件中的类如何加载?
-
插件中资源如何加载?
-
插件中的四大组件是如何正常运行的?
宿主加载插件类的四种方式
方式一:dex合并
原理:
ClassLoader 在 findClass的时候总是会在Elements类型的数组dexElements里获取,并且如果两个dex有相同的类和方法,那么将直接返回位于dexElements数组前面的类和方法。
步骤:
- 反射获取宿主App ClassLoader的dexElements字段;
- 获取插件apkFile,反射获取Element类型的对象dex;
- 把dex和宿主dexElements合并一个新的dex数组,替换宿主的dexElements数组;
方式二:为插件创建自己的ClassLoader
原理:
修改LoadedApk的ClassLoader;
具体来说就是利用ActivityThread对于LoadedApk的缓存机制,将含有自定义的ClassLoader的插件信息
LoadedApk添加进mPackages中,进而完成了类的加载过程。
大致步骤:
- 获取ActivityThread类中的mPackages对象,该对象是一个map,key为包名,value是LoadedApk;
- 通过反射创建插件apk的LoadApk;
- 替换插件LoadApk中的ClassLoader;
- 将插件LoadApk添加到mPackages对象中。
优点:
- 隔离性好,插件之间,插件与宿主之间类加载互不影响;
缺点:
- 兼容性差,要适配多个Android版本;
- Hook API较多,AMS, PMS;
方式三:修改宿主的ClassLoader
原理:
自定义ClassLoader,并持有每个插件的Classoader,在loadClass的时候通过遍历不同的ClassLoader去查找相应的类。
步骤:
-
自定义ClassLoader,并接管系统的ClassLoader;
-
插件初始化时创建插件自己的DexClassLoader;
-
将插件的ClassLoader 添加到自定义的ClassLoader中。
方式四:反射加载插件的类
步骤:
- 获取插件apk;
- 通过apk获取dex;
- 通过DexClassLoader的loadClass方法获取插件dex的类;
- 反射获取方法并调用。
示例:
File apkFile = getFileStreamPath(apkName);
ClassLoader mClassLoader = new DexClassLoader(apkFile.getPath(), dstPath, null, getClassLoader());
Class mClass = mClassLoader.loadClass("com.example.cjl.plugin");
Object object = mClass.newInstance();
Method mGetId = mClass.getMethod("getId");
mGetId.setAccessible(true);
int id = mGetId.invoke(object);
弊端:
1. 工作量大;
2. 插件类在宿主中不存在。
解决方案:
面向接口编程。