在工作中会遇到关于Android插件化开发出现的问题,随着对这种技术的了解,我发现在插件开发的一些基础问题上下需要花费更多的时间。就比如在主工程Context和插件Context的区别所在,权限必然要在主工程申明,本着学习的心态,对插件的历史花了些时间进行了解,随后便写了两个Demo进行总结。这篇文章主要是通过对插件的实现原理进行直观的说明,以此来对插件内开发进行更进一步的加深和了解,所以这篇文章也是主要对核心逻辑进行更多的探讨,所以也就不会对其背景和原理进行一个深入的探讨.
原理和背景:
首先从android插件化的技术起源开始,插件化最初是从免安装运行apk这个方向的一个初步想法从而发展出来的技术,插件也可以称之为一个免安装的apk,app在运行过程中因为其支持插件化,从而可以将aap中一些平常不适应的模块做成我们所需要的插件,以此来实现app中的动态扩展功能,更是进一步减少了其安装包的大小,当然需要实现其插件化,主要是要如何解决这三个问题:
载类与主程序如何进行相互调动
资源的加载如何与主工程进行相互访问
组件的生命周期如何管理
我将相对来说比较有名的开源插件化框架进行了时间排序,以此来了解它们的原理,如何实现,这样我们就可以看出插件化技术的发展历程,我将其插件化框架进行了分层,根据其实现原理划分很了三代
第一代:dynamic-load-apk相较于其他插件框架,它最先使用了ProxyActivity静态代理技术,由ProxyActivity进行控制插件PluginActivity生命周期。但是这种方式对的缺点很明显,插件里activity就必须要继承PluginActivity,所以如果要进行开发就必须要仔细处理context。相对而言DroidPlugin就是通过Hook系统服务进行启动该插件中的Activity,这样我们在开发插件的过程与普通的app开发的是一样的,区别不大,但hook会过多的进行系统服务,会显得复杂而且相对而说就不够稳定。
第二代:VirtualAPK为了跟普通app一样开发插件并且让插件开发达到低侵入性,同时也让其拥有框架的稳定性,所以在实现原理上都会尽量选择比较少的hook,在通过manifest中实现对四大组件的插件化进行一个预埋。而且各个框架都从设计的思想进行了一定程度的扩展,其中更是将Small做成了一个具有跨平台,组件化功能的开发框架。
第三代:VirtualApp相对于前面两代就比较厉害了,不仅能够app的运行环境进行完全模拟,还能够讲app的免安装运行和双开技术进行实现。Atlas是阿里19年开源出来的一个具有热修复技术和结合组件化的一个app的基础框架,至今仍然应用于阿里系的app中,因此也将其号称为一个容器化框架。
下面详细介绍插件化框架的原理,分别对应着实现插件化的三个核心问题。
类加载:
从外部apk中类的加载来看,通常在Android中常用的共有两种类加载器,PathClassLoader和DexClassLoader,这两种类加载器都继承于BaseDexClassLoader。
Android对于外部的dex文件,主要通过 DexClassLoader 类加载,因此,只需要给定插件的路径,就可以构造对应的类加载器
// DexClassLoaderpublic class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
// PathClassLoader
public class PathClassLoader extends BaseDexClassLoader { public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
这种区别就是在调用父类构造器时,DexClassLoader会多传了一个optimizedDirectory参数,并且这个目录的存储路径必须是内部存储路径,具体是用来缓存系统创建的Dex文件。而PathClassLoader的参数是为null,所以就只能从内部存储目录中加载Dex文件。
那么我们就可以使用DexClassLoader拿去加载外部的apk,所使用的方法如下
//第一个参数为apk的文件目录//第二个参数为内部存储目录//第三个为库文件的存储目录//第四个参数为父加载器new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent)
资源加载:
Android系统是通过Resource对象进行加载资源,下面代码的生成过程展示了该对象
所以也就说明现在插件Activity中就不会有所限制:
//创建AssetManager对象
AssetManager assets = new AssetManager();
//将apk路径添加到AssetManager中
if (assets.addAssetPath(resDir) == 0){
return null;
}
//创建Resource对象
r = new Resources(assets, metrics, getConfiguration(), compInfo);
所以,我们只要将插件apk中的路径加入到AssetManager之后,就可以实现访问其插件资源。
在具体实现时,因为AssetManager跟public并不是一类,所以就需要通过反射让其去创建,而且部分Rom会对其创建的Resource类进行修改,那么我们就有必要考虑不同Rom的兼容性。
生命周期:
在Android进行开发中会有一些特殊的类,全是才系统创建而来,并且会让系统来管理生命周期。
对生命周期的管理在插件化中是较为复杂的,就如同常用的四大组件,Service,Activity,ContentProvider和BroadcastReceiver。其中插件化中Activity是最为繁琐的,其中早期的dynamic-load-apk所采用的基本上是用代理的方式,主要是通过使用一个空壳的Activity让其成为代理(Proxy),系统会将Activity的回调都一一映射到这个Activity插件,这样的话就可以将插件的生命周期实现通过系统来进行管理,不得不说这种方式及其直观,但其所拥有的插件Activity需要继承这样代理的PluginActivity (Demo中的命名),不仅拥有侵入性强的特点,更可以结合我们后面的例子并加以理解。
代理实现:
首先我们将建立一个 PluginManager 类从而来实现插件的加载:
public class PluginManager {
static class PluginMgrHolder {
static PluginManager sManager = new PluginManager();
}
private static Context mContext;
Map<String, PluginApk> sMap = new HashMap<>();
public static PluginManager getInstance() {
return PluginMgrHolder.sManager;
}
public PluginApk getPluginApk(String packageName) {
return sMap.get(packageName);
}
public static void init(Context context) {
mContext = context.getApplicationContext();
}
public final void loadApk(String apkPath) {
PackageInfo packageInfo = queryPackageInfo(apkPath);
if (packageInfo == null || TextUtils.isEmpty(packageInfo.packageName)) {
return;
}
// check cache
PluginApk pluginApk = sMap.get(packageInfo.packageName);
if (pluginApk == null) {
pluginApk = createApk(apkPath);
if (pluginApk != null) {
pluginApk.packageInfo = packageInfo;
sMap.put(packageInfo.packageName, pluginApk);
} else {
throw new NullPointerException("PluginApk is null");
}
}
}
private PluginApk createApk(String apkPath) {
String addAssetPathMethod = "addAssetPath";
PluginApk pluginApk = null;
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod(addAssetPathMethod, String.class);
addAssetPath.invoke(assetManager, apkPath);
Resources pluginRes = new Resources(assetManager,
mContext.getResources().getDisplayMetrics(),
mContext.