android插件化介绍
转载:https://blog.csdn.net/suyimin2010/article/details/80958742
插件化介绍
image.png插件化技术最初源于免安装运行apk的想法,这个免安装的apk可以理解为插件。
支持插件化的app可以在运行时加载和运行插件,这样便可以将app中一些不常用的功能模块做成插件,一方面减小了安装包的大小,另一方面可以实现app功能的动态扩展。
想要实现插件化,主要是解决下面三个问题:
- 插件中代码的加载和与主工程的互相调用
- 插件中资源的加载和与主工程的互相访问
- 四大组件生命周期的管理
插件化技术
技术的发展,根据实现原理可以将这几个框架划分成了三代。
image.png
第一代
dynamic-load-apk最早使用ProxyActivity这种静态代理技术,由ProxyActivity去控制插件中PluginActivity的生命周期。
该种方式缺点明显,插件中的activity必须继承PluginActivity,开发时要小心处理context。
而DroidPlugin通过Hook系统服务的方式启动插件中的Activity,使得开发插件的过程和开发普通的app没有什么区别,但是由于hook过多系统服务,异常复杂且不够稳定。
第二代
为了同时达到插件开发的低侵入性(像开发普通app一样开发插件)和框架的稳定性,在实现原理上都是趋近于选择尽量少的hook,并通过在manifest中预埋一些组件实现对四大组件的插件化。
另外各个框架根据其设计思想都做了不同程度的扩展,其中Small更是做成了一个跨平台,组件化的开发框架。
第三代
VirtualApp比较厉害,能够完全模拟app的运行环境,能够实现app的免安装运行和双开技术。
Atlas是阿里今年开源出来的一个结合组件化和热修复技术的一个app基础框架,其广泛的应用与阿里系的各个app,其号称是一个容器化框架。
插件化技术原理
类加载
Android中常用的有两种类加载器,DexClassLoader和PathClassLoader,它们都继承于BaseDexClassLoader。
// DexClassLoader
public 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文件。
Android对于外部的dex文件,主要通过 DexClassLoader 类加载。
//第一个参数为apk的文件目录
//第二个参数为内部存储目录
//第三个为库文件的存储目录
//第四个参数为父加载器
new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent)
类加载器的构造函数代码例子:
private DexClassLoader createDexClassLoader(String apkPath) {
File dexOutputDir = mContext.getDir("dex", Context.MODE_PRIVATE);
DexClassLoader loader = new DexClassLoader(apkPath, dexOutputDir.getAbsolutePath(),
null, mContext.getClassLoader());
return loader;
}
插件调用主工程
在构造插件的ClassLoader时会传入主工程的ClassLoader作为父加载器,所以插件是可以直接可以通过类名引用主工程的类。
主工程调用插件
若使用多ClassLoader机制,主工程引用插件中类需要先通过插件的ClassLoader加载该类再通过反射调用其方法。
插件化框架一般会通过统一的入口去管理对各个插件中类的访问,并且做一定的限制。
若使用单ClassLoader机制,主工程则可以直接通过类名去访问插件中的类。
注意:该方式有个弊病,若两个不同的插件工程引用了一个库的不同版本,则程序可能会出错,所以要通过一些规范去避免该情况发生。
资源加载
Android系统通过Resource对象加载资源。
因此,只要将插件apk的路径加入到AssetManager中,便能够实现对插件资源的访问。
//创建AssetManager对象
AssetManager assets = new AssetManager();
//将apk路径添加到AssetManager中
if (assets.addAssetPath(resDir) == 0){
return null;
}
//创建Resource对象
r = new Resources(assets, metrics, getConfiguration(), compInfo);
由于AssetManager并不是一个public的类,需要通过反射去创建.
资源路径处理
和代码加载相似,插件和主工程的资源关系也有两种处理方式:
- 合并式:addAssetPath时加入所有插件和主工程的路径;
- 独立式:各个插件只添加自己apk路径
合并式
由于AssetManager中加入了所有插件和主工程的路径,因此生成的Resource可以同时访问插件和主工程的资源。
但是由于主工程和各个插件都是独立编译的,生成的资源id会存在相同的情况,在访问时会产生资源冲突。
独立式
各个插件的资源是互相隔离的,不过如果想要实现资源的共享,必须拿到对应的Resource对象。
Context处理
通常我们通过Context对象访问资源,光创建出Resource对象还不够,因此还需要一些额外的工作。
对资源访问的不同实现方式也需要不同的额外工作。
以VirtualAPK的处理方式为例。
创建Resource
if (Constants.COMBINE_RESOURCES) {
//插件和主工程资源合并时需要hook住主工程的资源
Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath());
ResourcesManager.hookResources(context, resources);
return resources;
} else {
//插件资源独立,该resource只能访问插件自己的资源
Resources hostResources = context.getResources();
AssetManager assetManager = createAssetManager(context, apk);
return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
关联resource和Activity
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
activity.setIntent(intent);
//设置Activity的mResources属性,Activity中访问资源时都通过mResources
ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources())
四大组件支持
Android开发中有一些特殊的类,是由系统创建的,并且由系统管理生命周期。
如常用的四大组件,Activity,Service,BroadcastReceiver和ContentProvider。
仅仅构造出这些类的实例是没用的,还需要管理组件的生命周期。
其中以Activity最为复杂,不同框架采用的方法也不尽相同。下面以Activity为例详细介绍插件化如何支持组件生命周期的管理。
大致分为两种方式:
- ProxyActivity代理
- 预埋StubActivity,hook系统启动Activity的过程
ProxyActivity代理
ProxyActivity代理的方式最早是由dynamic-load-apk提出的,其思想很简单,在主工程中放一个ProxyActivy,启动插件中的Activity时会先启动ProxyActivity,在ProxyActivity中创建插件Activity,并同步生命周期。
下图展示了启动插件Activity的过程:
image.png
具体的过程如下:
首先需要通过统一的入口(如图中的PluginManager)启动插件Activity,其内部会将启动的插件Activity信息保存下来,并将intent替换为启动ProxyActivity的intent。
ProxyActivity根据插件的信息拿到该插件的ClassLoader和Resource,通过反射创建PluginActivity并调用其onCreate方法。
PluginActivty调用的setContentView被重写了,会去调用ProxyActivty的setContentView。由于ProxyActivity重写了getResource返回的是插件的Resource,所以setContentView能够访问到插件中的资源。同样findViewById也是调用ProxyActivity的。
hook方式
image.png- Activity1调用startActivity,实际会调用Instrumentation类的execStartActivity方法,Instrumentation是系统用来监控Activity运行的一个类,Activity的整个生命周期都有它的影子。
- 通过跨进程的binder调用,进入到ActivityManagerService中,其内部会处理Activity栈。之后又通过跨进程调用进入到Activity2所在的进程中。
- ApplicationThread是一个binder对象,其运行在binder线程池中,内部包含一个H类,该类继承于类Handler。ApplicationThread将启动Activity2的信息通过H对象发送给主线程。
- 主线程拿到Activity2的信息后,调用Instrumentation类的newActivity方法,其内通过ClassLoader创建Activity2实例。