0. 前言
在Android开发中,插件化就是让我们的应用可以把一些功能和逻辑单独抽出来放在插件apk中,然后主apk做到按需调用,这样的好处是一来可以减少主apk的体积,二来可以做到热插拔,在不发布新版本的情况下实现更新某些模块,更加动态化。
网上有很多插件化的解决方案和框架,但是它们都必须解决几个基础问题:插件类的加载,插件资源的加载和插件Activity的管理。
1. 插件类的加载
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
...
this.dexElements =makeDexElements(splitDexPath(dexPath), optimizedDirectory);
...
}
Android的类是由DexClassLoader加载的,DexClassLoader又继承自BaseDexClassLoader。DexPathList类的构造参数dexPath一般情况下是"/data/../*.apk",并通过makeDexElements()方法创建dexElements。
private static final String DEX_SUFFIX = ".dex";
private static final String JAR_SUFFIX = ".jar";
private static final String ZIP_SUFFIX = ".zip";
private static final String APK_SUFFIX = ".apk";
private static Element[] makeDexElements(ArrayList<File> files,File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
...
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
//压缩包加载代码块
zip = new ZipFile(file);
dex = loadDexFile(file, optimizedDirectory);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
可以看到,DexClassLoader支持.dex,.jar,.zip和.apk,通过Element ()创建一个dex元素。
大部分插件化框架都是由.zip文件生成一个dex元素,再通过反射添加到宿主类加载器的dexPathList里。
2. 插件资源的加载
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mPath);
Resources superRes = context.getResources();
Resources mResource =
new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
Android中的资源由AssetManager加载,可以使用反射的方式调用AssetManager中的addAssetPath方法,传入apk的路径参数即可将其中的资源加载到Resources对象中。在使用时通过判断assetManager和mResource是否为空来决定是否加载插件中的资源,为空则调用super.getAssets()和super.getResources()。
其实APP的换肤也可以用这种方式来做,当用户点击换肤时,加载已保存在SD卡中的apk,并通过AssetManager反射添加资源路径的方法将apk的资源加载进去,然后通过new Resource将Assetmanager管理器注册,得到一个全新的资源管理者AssetManager,通过这个管理者与app原生的资源管理者作为区分。当需要换肤时将这个新对象resource赋值给全局的resource,通过Resource对象实现换肤;若切换回默认app的皮肤时,就将默认app生成的resource赋值给resource,从而实现换肤。
3. Activity注册和生命周期问题
大部分插件化框架解决办法都是采用在宿主工程里预先注册Activity占坑,然后通过占坑Activity将生命周期回调传回给插件Activity的方式。这里以Small框架的处理为例进行介绍。
3.1 Activity启动流程
要明白Small框架的原理,必须得了解Activity的启动流程,这个流程还是挺复杂的,具体的Activity启动流程源码解析可以参考这篇博客。
这里做一个简单的流程概述。
(1) Activity的startActivity方法,具体调用的是ContextImpl的同名函数。
(2) ContextImpl中会调用Instrumentation的execStartActivity方法,其中checkStartActivityResult()用于权限校验。并通过aidl进行跨进程通信,最终调用AMS的startActivity方法。
(3) 在系统进程中,AMS的stratActivityAsUser()中通过调用ActivityStackSupervisor和ActivityStack进行一系列的交互用来确定Activity栈的使用方式。
(4) 通过ApplicationThread进行跨进程通信,转回到app进程。
(5) 通过ActivityThread中的H(一个handler)传递消息,最终调用Instrumentation来创建一个Activity。
3.2 插件化中的问题
我们要在主apk中启动一个插件apk的Activity,上面的权限验证会通不过,因为我们主apk的manifest中没有声明插件中的Activity。因此可以在manifest中预先定义好几个占坑Activity,在校验权限之前把我们插件apk中的Activity替换成定义好的Activity,这样就能顺利通过校验,而在之后真正生成Activity的地方再换回来。因此Small框架中通过反射替换 ActivityThread 里的mInstrumentation的方式就可以理解了。
Small中的ApkBundleLauncher类内部有一个setUp方法,通过反射获取了Instrumentation并且赋值成了自定义的InstrumentationWrapper。而在自定义的InstrumentationWrapper中,重写了execStartActivity和newActivity这两个方法。在第一个方法中,系统会通过校验权限,而第二个方法则生成真正Activity的实例。