什么是插件化
首先我们区分一下组件化和插件化的概念
- 组件化
组件化开发就是将一个app分成多个模块,组件化强调功能拆分,单独编译,单独开发,根据需求动态配置组件。 - 插件化
插件化是将一个apk根据业务功能拆分成不同的子apk,插件化更关注动态加载、热更新。 -
热修复
热修复强调的是在不需要二次安装应用的前提下修复已知的bug。
插件化的优点
- 宿主和插件分开编译
- 并发开发
- 动态更新插件
- 按需下载模块
- 方法数或变量数爆棚
- 插件无需安装即可运行
插件化发展历程
- 静态代理
dynamic-load-apk最早使用ProxyActivity这种静态代理技术,由ProxyActivity去控制插件中PluginActivity的生命周期 - 动态替换(HOOK)
在实现原理上都是趋近于选择尽量少的hook,并通过在manifest中预埋一些组件实现对四大组件的动态插件化。像Replugin。 - 容器化框架
VirtualApp能够完全模拟app的运行环境,能够实现app的免安装运行和双开技术。Atlas是阿里的结合组件化和热修复技术的一个app基础框架,号称是一个容器化框架。
插件化框架对比
插件化技术原理
实现插件化需要解决的问题
- 插件类的加载,解决宿主加载插件以及插件加载宿主的问题
- 资源文件的加载,解决宿主和插件的资源文件的加载问题,以及资源合并和资源冲突的问题
- 四大组件的支撑,支撑包括Activity,BroadReceiver. ContentProvider,Service四大组件在插件中的正常使用
类加载原理
classloader介绍
其中:
- BootClassLoader
和java虚拟机中不同的是,BootClassLoader是ClassLoader内部类,由java代码实现而不是c++实现,是Android平台上所有ClassLoader的最终parent,这个内部类是包内可见。 - BaseDexClassLoader
负责从指定的路径中加载类,加载类里面的各种校验、检查和初始化工作都由它来完成 - PathClassLoader
继承自BaseDexClassLoader,只能加载已经安装到Android系统的APK里的类,主要逻辑由BaseDexClassLoader实现 - DexClassLoader
继承自BaseDexClassLoader,可以加载用户自定义的其他路径里的类,主要逻辑都由BaseDexClassLoader实现。
双亲委派模型
含义:双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先从缓存查找该class对象,找到就不用重新加载
Class> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果找不到,则委托给父类加载器去加载
c = parent.loadClass(name, false);
} else {
//如果没有父类,则委托给启动加载器去加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
}
if (c == null) {
// If still not found, then invoke findClass in order
// 如果都没有找到,则通过自定义实现的findClass去查找并加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {//是否需要在加载时进行解析
resolveClass(c);
}
return c;
}
}
为什么使用双亲委派模型?
- 带有优先级的层次关系,通过这种层级关可以避免类的重复加载;
- 其次是考虑到安全因素,java核心API中定义类型不会被随意替换。
如何动态加载APK的类文件?
主要依赖上述DexClassLoader:
我们看下DexClassLoader的构造方法:
DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
dexPath:要加载的类所在的jar或者apk文件路径,类装载器将从该路径中寻找指定的目标类,该类必须是APK或jar的全路径
optimizedDirectory:odex优化之后的dex存放路径,真正的数据是从这个位置的dex文件加载的,由于ClassLoader只能加载内部存储路径中的dex文件,所以这个路径必须为内部路径
librarySearchPath:目标类中所使用的C/C++库存放的路径
classloader:本装载器的父装载器,一般使用当前执行类的装载器就可以了,在Android用context.getClassLoader()就可以了
加载样例如下:
private void loadClass() {
// 获取推送到SDCard中的插件路劲
String apkPath = Environment.getExternalStorageDirectory() + File.separator + "test.apk";
// 优化后的dex存放路径
String dexOutput = getCacheDir() + File.separator + "DEX";
File file = new File(dexOutput);
if (!file.exists()) file.mkdirs();
DexClassLoader dexClassLoader = new DexClassLoader(apkPath, dexOutput, null, getClassLoader());
try {
// 从优化后的dex文件中加载APK_HELLO_CLASS_PATH类
clazz = dexClassLoader.loadClass("com.iflytek.test.HelloWorld");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
这样我们就可以加载一个指定路径下的apk文件的class文件
加载插件资源文件
加载插件资源文件原理
//获取资源文件的方式
Drawable drawable = context.getResource().getDrawable(R.drawable.error);
Resource构造函数
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
其中真正去进行资源加载的为AssetManager 。
AssetManager的addAssetPath()方法添加系统资源和apk资源,并构造Resource提供给Context上下文进行使用,所以真正加载资源是通过AssetManger去加载。
public final int addAssetPath(String path) {
return addAssetPathInternal(path, false);
}
思路
1. 反射调用AssetsManager的addAssetPath方法;
2. 将外部的apk路径添加进去,构建新的Resource对象
3. 通过classloader加载R.java获取drawable,对应的id
4. 通过上述构建的Resource获取drawable对象。
/**
* 反射添加资源路径,并创建新的Resources 对象
*/
private Resources getPluginResources() {
try {
AssetManager assetManager = AssetManager.class.newInstance();
//反射获取AssetManager的addAssetPath方法
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
//将插件包地址添加进行
addAssetPath.invoke(assetManager, apkDir+ File.separator+apkName);
Resources superRes = context.getResources();
//创建Resources
Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(),
superRes.getConfiguration());
return mResources;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 1. 先获取资源的名称对应的id(通过反射R.java文件的变量)
* 2. 再根据我们构造的Resources 获取对应的资源对象。我
*/
public Drawable getApkDrawable(String drawableName){
try {
DexClassLoader dexClassLoader = new DexClassLoader(apkDir+File.separator+apkName,
optimizedDirectoryFile.getPath(), null, context.getClassLoader());
//通过使用apk自己的类加载器,反射出R类中相应的内部类进而获取我们需要的资源id
Class> clazz = dexClassLoader.loadClass(apkPackageName + ".R$drawable");
Field field = clazz.getDeclaredField(drawableName);
int resId = field.getInt(R.id.class);//得到图片id
Resources mResources = getPluginResources();
assert mResources != null;
return mResources.getDrawable(resId);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
资源文件处理方式
合并式:addAssetPath时加入所有插件和主工程的路径;优点是插件和宿主可以相互访问,缺点是可能产生资源冲突。
独立式:各个插件只添加自己apk路径。不存在资源冲突,但是无法资源共享。
合并式资源冲突的解决方案:
修改aapt源码,定制aapt工具编译期间修改PP段
修改aapt的产物,即,编译后期重新整理插件Apk的资源,编排ID
插件Activity处理方案
代理模式
代理模式的定义:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。
动态代理的具体实现参考如下:
public class DynamicProxyHandler implements InvocationHandler {
private Object object;
public DynamicProxyHandler(Object object) {
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
breforeInvoke();
Object result = method.invoke(object, args);
afterInvoke();
}
}
如何动态加载插件中Acitivity
Activity插件化需要解决的问题:
- 怎么欺骗AMS去启动一个清单文件不存在的Activity ;
- 插件Activity的生命周期如何实现;
- 插件apk中用过的各种资源,如何动态的加载资源。
代理模式(DL框架)
ProxyActivity + 插件中没注册的Activity = 标准的Activity
主要流程如下:
- 宿主中通过启动ProxyAcitivity
- 代理activity通过AIDL通信和插件PluginActivity建立联系
- 当宿主中的代理ProxyAcitivity生命周期发生变化的时候,通过AIDL通知到PluginActivity。从而完成插件Activity生命周期的同步。
坑位占用模式
在AndroidManifest中注册,但并没有真实的实现 类,只作为其他Activity启动的坑位,通过HOOK AMS去加载插件中的Activity的class文件。
下面我们来介绍一下Replugin的Activity原理
- Pmbase根据Intent找到对应的插件
- 分配坑位Activity,与插件中的Activity建立一对一的关系并保存在PluginContainer中
- 让系统启动坑位Activity,因为它是在Manifest中注册过的
- Android系统会尝试使用RepluginClassLoader加载坑位Activity的Class对象
- RepluginClassLoader 通过建立的对应关系找到插件Activity,并使用
PluginDexClassLoader 加载插件Activity 的Class对象并返回 - Android系统就使用这个插件中的Activity的Class对象来运行生命周期函数