一、前言
首先思考一个问题:为什么需要应用插件化?插件化能带来哪些好处?
应用开发遇到的问题:
1、在不断迭代的过程中,代码越来越庞大,对项目的分工协作和管理都带来了挑战。
2、对于已经全量发布的版本突然发现严重bug需要紧急修复,或者产品有一个紧急需求需要立即上线,重新发布新版本十分耗时,迫切需要终端app的热更新能力。
3、虚拟机方法索引是16位的,造成65535方法数限制。
4、移动终端的内存限制要求app必须优化内存使用效率,对各个模块实现按需加载。
针对上面的问题,使用插件化技术能带来的好处:
1、项目模块解耦。
2、各个模块实现高效地并行开发。
3、变更实时发布上线,对bug进行热修复。
4、破除虚拟机方法数限制。
5、优化内存占用,提高用户体验。
因此,对中大型项目实施插件化开发是必由之路,也是对敏捷开发的最佳实践。
当前插件化技术的思路:
(1)基于虚拟机类加载原理,利用ClassLoader动态加载dex文件,这个是目前主流的技术方向。
(2)基于脚本文件,将需要插件化的逻辑放在脚本文件中,同时把脚本文件作为app资源进行管理,实现实时下载,实时加载。这个大多数是作为对(1)的一个辅助手段使用。
二、插件化技术的基石——ClassLoader的工作原理
2.1、ClassLoader双亲委派模型
ClassLoader在java中就有了,android因为有了dex文件等原因做了些改造,最终都是实现了双亲委派模型。我们可以对比一下,在java中的双亲委派模型是这样的:
图1 java JVM 中的ClassLoader体系
(1)BootStrap ClassLoader:启动类加载器,负责加载存放在%JAVA_HOME%\lib目录中的或者-Xbootclasspath参数所指定的路径中的类,启动类加载器不能被java程序直接引用。
(2)Extension ClassLoader:扩展类加载器,负责加载%JAVA_HOME%\lib\ext目录中的或者java.ext.dirs系统变量所指定的路径中的类,app可以使用扩展类加载器。
(3)App ClassLoader:应用程序类加载器,负责加载用户类路径classpath上所指定的类库, getSystemClassLoader()方法返回的就是它,app可以使用应该加载器,如果app中没有自定义类加载器,该加载器就是app中默认使用的类加载器。
(4)Custom ClassLoader: 自定义类加载器。一般会为了一些特殊特性(比如打破双亲委派模型实现一些hack效果)去自定义加载器,并重载loadClass或者findClass方法。
图2 dalvik虚拟机的类加载体系
DexClassLoader和PathClassLoader类都继承自BaseDexClassLoader,唯一的区别是PathClassLoader构造函数的optimizedDirectory参数为null,只能加载data/app/…这种固定的内部的目录下的dex,而DexClassLoader的optimizedDirectory目录不为null,因此可以通过设置路径去加载外部未安装的dex、apk、jar、zip等。这里把两个的构造函数实现展示出来,一看就明了:
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) { //PathClassLoader
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
实现一个简单的自定义类加载器应该重点关注两个方法,即loadClass和findClass方法,前一个方法已经被默认实现了双亲委派模型,一般无需改动,后者可以灵活编码。
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className); //检查当前是否已经加载过
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false); //递归调用parent类去查找
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className); //调用findclass去本地相关路径下面加载类
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
此外,实现整个类加载过程还涉及到另外几个重要的类:
(1)DexPathList封装了指定路径下面dex、res、so文件的抽象,即 Element[] dexElements 和 File[] nativeLibraryDirectories。
(2)Element 每个Element就是一个jar,dex,apk,zip文件
(3)DexFile dex文件
(4)ZipFile jar、apk、 zip文件
具体的代码实现细节有兴趣的同学可以自行去阅读,这里不再赘述。
关于ClassLoader有对实现插件化非常重要的几点需要总结一下:
1、如果一个类被位于树根的ClassLoader加载过,那么在以后整个系统的生命周期内,这个类永远不会被重新加载。
2、不同继承路线上的ClassLoader加载的类肯定不是同一个类。
3、虚拟机判断类相同的条件:相同的 ClassName + 相同的PackageName +相同的 ClassLoader
4、如果你希望通过动态加载类的方式替换一个原有类已达到改变执行逻辑的目的,那么有两种可行的方式:
(1)新类能够在原有的类加载体系(父子关系确立的继承树)中优先于旧类被加载;
(2)在原有的继承体系之外,用自定义的ClassLaoder(没有继承关系)加载新类,使得新类和旧类被同时加载,这种方式需要注意可能爆出类型异常,因为新类和旧类此时在虚拟机中会被当作两个不同的类。
三、如何实现一个可用的插件机制
要正真实现一个可用的插件机制,涉及到方方面面的细节,处理不好会出现在不同rom上的兼容性问题,但就原理上来讲,大致需要以下几个步骤:
(1)插件的打包和加载
(2)插件资源res的获取
(3)插件中组件的生命周期管理
3.1 、插件的打包和加载
插件的打包形式有很多,可以打包成jar、zip或者 apk,然后把插件放入宿主apk中的asserts文件夹下面,如果想要减少最终包的体积,也可以做成网络下载插件的形式。在需要加载插件的时候,把插件释放到data/data/app/xxx/下面或者外置sd卡下面,然后执行正式的加载,所谓加载就是通过默认的类加载器或者自定义的ClassLoader将插件的dex加载到虚拟机内存中。
3.2、插件资源的获取
由于插件本身没有自己的上下文环境,系统不会默认为其创建资源Resource对象,所以要想获取插件中的资源必须在代码中去创建。这里面最重要的方法就是AssetManager的addAssetPath,通过这个方法可以把插件的资源路径添加进来。
private static AssetManager getAssetManager(String pluginPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(
assetManager, pluginPath);
return assetManager;
} catch (Exception e) {
}
return null;
}
public static Resources getPluginResource(Context context, String pluginPath){
AssetManager assetManager = getAssetManager(pluginPath);
return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
}
通过创建的资源Resource对象就可以访问插件中的资源了,但是其上下文环境依然是宿主apk的上下文环境。
3.3、插件中组件的生命周期管理
最重要的组件就是activity了,做到对插件中activity生命周期的管理是整个插件化机制实现的关键。主要的思路有三个:
(1)代理的方式:通过宿主acitivty提供代理实现插件组件生命周期的驱动。
代理的方式中宿主提供的activity就是我们通常所说的“壳”,这种方式下我们的android系统并不知道插件activity的存在,当“壳”的生命周期发生变动时,回调插件activity的生命周期函数,从而驱动插件生命周期的变化。由于此时插件没有自己的上下文环境,在插件的一切与context有关的逻辑都需要借助宿主activity来执行。
(2)预注册的方式:将插件的组件在宿主apk的Amanifest中预先注册,使用时再创建插件activity的实例。
这种方式的优点是插件的 activity有自己的生命周期,对各种rom的适配性也更强。缺点就是灵活性不如代理的方式,且res依然需要自己创建。
(3)动态创建的方式:这是一种hack的方式,同时也是稳定性最差的方式,核心思路就是“冒名顶替”。
在宿主apk中先注册有一个空的activityA, 当需要启动插件中的activityB时,动态创建另外一个activityA extends activityB,这个动态创建的activity和宿主中注册的acitivtyA有相同的包名和类名,这样就骗过了系统,有了自己的生命周期和上下文环境,成为一个完全正常的 activity。
方式1示例:
插件示例:
public class BaseActivity extends Activity {
...
public static final String PLUGIN_VIEW_ACTION = "plugin.view.activity.action";
// 持有宿主上下文的引用
protected Activity mProxyActivity;
public void setProxy(Activity proxyActivity) {
mProxyActivity = proxyActivity;
}
@Override
public void setContentView(int id) {
if(mProxyActivity != null)
mProxyActivity.setContentView(id);
}
@Override
public View findViewById(int id) {
if(mProxyActivity != null)
return mProxyActivity.findViewById(id);
else
return null;
}
...
// 启动插件内部的activity本质是借宿主上下文环境创建一个新的ProxyActivity“壳”
public void startActivity(String className) {
if(mProxyActivity != null){
Intent intent = new Intent(PLUGIN_VIEW_ACTION);
intent.putExtra("ClassName", className);
mProxyActivity.startActivity(intent);
}
} ...}
宿主示例:
public class ProxyActivity extends Activity {
...
// 插件的上下文环境不会自动创建插件的资源对象,想要访问插件资源先要自己创建一个Resources对象然后替换掉“壳”的Resources
private Resources mPluginResources;
public void replaceWithPluginResources(Context context){
try {
Field field = context.getClass().getDeclaredField("mResources");
field.setAccessible(true);
if (mPluginResources == null) {
mPluginResources = buildPluginResources(pluginPath);// apt or zip path.
} field.set(context, mBundleResources); } catch (Exception e) { e.printStackTrace(); } }
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
replaceWithPluginResources(ProxyActivity.this);
// 类加载器加载,然后反射创建插件的activity实例
String className = getIntent().getStringExtra("ClassName");
try {
Class<?> pluginClass = CustomDexLoader.loadClass(className);
Constructor<?> constructor = pluginClass.getConstructor(new Class[ ] { });
Object instance = constructor.newInstance(new Object[ ] { });
// 把代理的引用传给插件,使插件获取有效的上下文环境
Method setProxyMethod = pluginClass.getMethod("setProxy", new Class[ ] { Activity.class });
setProxyMethod.setAccessible(true);
setProxyMethod.invoke(instance, new Object[] { this });
// 代理主动调用插件的onCreate(),开始同步生命周期
Method onCreateMethod = pluginClass.getDeclaredMethod("onCreate", new Class[] { Bundle.class });
onCreateMethod.setAccessible(true);
onCreateMethod.invoke(instance, new Object[] { null });
} catch (Exception e) {
e.printStackTrace();
}
}
...
}
方式二示例:
方式三示例:
(1)动态生成一个class或dex文件,使得我们可以获得一个继承任意插件activity的目标activity,这种操作叫做“动态子节码编译”,比如借助dexmaker实现。
(2)自定义ClassLoader并重载loadClass方法,在加载宿主目标activity的地方,替换成我们动态创建的同名的目标activity。
三种插件化方式中,最成熟稳定的是第一种代理方式,其余两种方式都有明显的缺陷,要么灵活性不足,要么稳定性不足。