一、概述
Android插件化技术一直是安卓开发中一个重要的方向,大概12年就被提出,发展至今已逐渐趋于成熟,很多大厂都有自己的一套插件化方案,诸如淘宝的Atlas,滴滴的VirtualAPK,360的RePlugin等。插件化技术的发展得益于业务的不断新增,诸如淘宝APP,里面有聚划算,拍卖,饿了么,淘票票等业务功能模块(这里只考虑原生界面),如果今天饿了么有个Bug要修复发版,明天淘票票想加多个功能,是否每次都需要去更新淘宝客户端?这个代价未免太大,同时,作为淘宝的开发人员,我是否还需要帮忙去维护饿了么的第三方业务代码?而作为饿了么开发人员,我自己又要维护自己客户端的代码,又要维护在你淘宝上的代码吗?在这种拥有众多业务的大厂里,插件化技术就应运而生。
二、概念区分
近年来,除了插件化技术,组件化技术,热修复等也同样广受关注,这里主要做一下概念的区分:
插件化:也叫动态加载技术,分宿主APK和插件APK,宿主APK可以理解为就是安装到手机的主APK(诸如手机淘宝),各个功能模块抽取变成插件APK(诸如饿了么,淘票票),这些插件APK可以随着宿主APK一起编译打包安装到手机上,也可以变成远程APK放在服务器,按需下载安装,实现功能的动态配置。从广义上理解,可以把Android系统当成一个宿主APK,各个安装到手机上的软件当成插件APK,从而组成一个插件化系统。
组件化:组件化技术实现了在Debug调试阶段,每个功能模块可以独立变成APP调试,但在打包编译阶段,其最终还是将所有模块打包成一个APK。
热修复:热修复技术有助于我们在用户无感知的时候修复APK,悄无声息的将Bug修复掉,我们希望热修复它是不新增资源文件,四大组件等操作,只是单纯的解决代码逻辑上的Bug,可以简单理解插件化技术是热修复的高级版
三、插件化的优缺点
优点:
- 让用户无需安装APK就能升级应用功能,减少发版频率,增加用户体验
- 按需编译加载,有效减小主APK体积,实现功能的灵活配置
- 模块化,降低耦合性,有利于多人合作开发同一个项目
缺点:
- Android上的黑科技越来越不被Android新系统待见,诸如Android 9.0系统已禁止非 SDK 接口的调用,而插件化技术中又或多或少使用了一些反射。这会使得插件化技术在新系统的表现上存在一些欠缺。
- 项目的构建过程变得复杂
四、插件化技术中的两个主要问题
正常情况下,apk被安装后,apk里面的代码和资源会被存放到系统的某处,以便系统能找到它。而插件APK未被安装,系统是找不到它里面的代码和资源的,所以如何加载插件APK中的代码和资源就成为了主要问题。针对这两个问题,下面主要介绍一种经典思路,达到抛砖引玉,有助于我们对插件化有个更好认识
如何加载插件APK中的Java代码?
Android中两个主要的Classloader,PathClassLoader和DexClassLoader,都是继承自BaseDexClassLoader:
DexClassLoader:可以加载包含classes.dex实体的.jar或.apk文件
PathClassLoader:只能加载已安装APK的dex文件
显然DexClassLoader可以满足我们插件化中对Java代码的动态加载,如下代码所示可以通过传入APK路径获取相应的DexClassLoader,接着通过调用DexClassLoader的loadClass方法获取相应的类实例:
//dexPath传入当前插件APK在SD卡中的路径
DexClassLoader pluginDexClassLoader = new DexClassLoader(dexPath, context.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(), null, context.getClassLoader());
//根据类名获取字节码对象
Class<?> mClass=pluginDexClassLoader loadClass("这里传入需要加载的完整路径类名");
//通过字节码对象创建类的实例
Object newInstance = mClass.newInstance();
类的实例可以通过上述拿到,然而这又会出现另外一个问题:已知Android系统中Activity页面的生命周期是由系统控制的,如果单纯使用DexClassLoader加载插件APK中的Activity,加载出来的也只是一个普通的对象,不具备页面的生命周期,曾看到过一个很生动的比喻:如果说系统创建的Activity是一个拥有四肢能动能跳的人的话,那么我们手动创建的Activity只是一个人偶,这个人偶虽然也有四肢,但是他动不了,因为他没有对应的掌控者。
针对这个问题,可以使用代理来实现,就如为了让这个木偶动起来,可以将这个木偶绑到活人身上,当活人动的时候,木偶也能跟着动。
具体的思路:
如何使用代理模式?可以先在宿主APK中注册好一个空的代理Activity页面,这个代理Activity拥有正常的生命周期,然后将插件Activity和代理Activity绑定起来,当代理Activity触发某一个生命周期的时候,也去通知插件Activity,让插件Activity拥有一个伪生命周期。
之前人们的采用的方法是使用反射去管理代理Activity的生命周期,但这样存在一些不便,比如反射代码写起来复杂,并且过多使用反射有一定的性能开销,后来采用了一种更为优雅的方式,就是采用接口机制,将代理Activity的生命周期提取出来作为一个接口,暂命名为PluginInterface,然后让插件Activity实现他:
public interface PluginInterface {
void onCreate(Bundle saveInstance);
void attachContext(Activity context);
void onStart();
void onResume();
void onRestart();
void onDestroy();
void onStop();
void onPause();
}
接着回到代理Activity,第一步,当调用插件Activity的时候,实际是调用了代理Activity,在代理Activity的onCreate生命周期里,使用之前说的加载类的方法创建插件Activity类实例,然后在代理Activity的各个生命周期动态的调用插件Activity的伪生命周期,以此达到同步效果,代理Activity的具体代码如下:
public class ProxyActivity extends Activity {
private PluginInterface pluginInterface;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//拿到要启动的Activity
String className = getIntent().getStringExtra("className");
try {
//加载该Activity的字节码对象
Class<?> aClass = PluginManager.getInstance().getPluginDexClassLoader().loadClass(className);
//创建该Activity的示例
Object newInstance = aClass.newInstance();
//面向接口编程,插件Activity需要实现PluginInterface接口
if (newInstance instanceof PluginInterface) {
pluginInterface = (PluginInterface) newInstance;
//将代理Activity的实例传递给插件Activity,以此让插件APK用于宿主的上下文
pluginInterface.attachContext(this);
//创建bundle用来与插件apk传输数据
Bundle bundle = new Bundle();
//将当前生命周期同步给插件Activity
pluginInterface.onCreate(bundle);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
@Override
public void onStart() {
pluginInterface.onStart();
super.onStart();
}
@Override
public void onResume() {
pluginInterface.onResume();
super.onResume();
}
@Override
public void onRestart() {
pluginInterface.onRestart();
super.onRestart();
}
@Override
public void onDestroy() {
pluginInterface.onDestroy();
super.onDestroy();
}
@Override
public void onStop() {
pluginInterface.onStop();
super.onStop();
}
@Override
public void onPause() {
pluginInterface.onPause();
super.onPause();
}
/**
* 在插件APK中,插件Activity调起其本身的Activity,实际还是一直调用代理Activity,不断重复上述流程
*/
@Override
public void startActivity(Intent intent) {
Intent newIntent = new Intent(this, ProxyActivity.class);
newIntent.putExtra("className", intent.getComponent().getClassName());
super.startActivity(newIntent);
}
}
如何加载插件APK中的资源文件?
宿主APK中是没有插件APK中的资源的,如果在代理Activity中直接像平时一样使用R.来引用插件APK中的资源的话是会报错的。Activity中有两个系统方法是和加载资源有关,我们需要在代理Activity中重写这两个方法,返回相应插件APK的Resource对象,这样才能顺利引用插件APK中的资源。
/** Return an AssetManager instance for your application's package. */
public abstract AssetManager getAssets();
/** Return a Resources instance for your application's package. */
public abstract Resources getResources();
AssetManager 中有一个addAssetPath方法,该方法可以通过传入指定的APK路径然后获取该APK的AssetManager,但这个方法是一个隐藏方法,需要通过反射来获取,紧接着将获取到的AssetManager传入Resources构造方法中,以此拿到相应插件APK中的Resources对象,示例代码如下:
//dexPath是Plugin的路径,
//optimizedDirectory是Plugin的缓存路径,
//libraryPath可以为null,
//parent为父类加载器
pluginDexClassLoader = new DexClassLoader(dexPath, context.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(), null, context.getClassLoader());
pluginPackageArchiveInfo = context.getPackageManager().getPackageArchiveInfo(dexPath, PackageManager.GET_ACTIVITIES);
{
AssetManager assets = null;
try {
assets = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assets, dexPath);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
pluginResources = new Resources(assets, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
接下来重写代理Activity中的getResources()方法,返回刚才新创建的Resources方法
/**
* 注意:三方调用拿到对应加载的三方Resources
* @return
*/
@Override
public Resources getResources() {
return pluginResources;
}
五、市场上的插件化框架
名称 | 团队 | Github |
---|---|---|
DroidPlugin | 奇虎360 | DroidPlugin |
PluginManager | 个人开发者 | PluginManager |
AndroidDynamicLoader | 个人开发者 | AndroidDynamicLoader |
dynamic-load-apk | 任玉刚 | dynamic-load-apk |
Small | 开源组织Wequick | Small |
DynamicAPK | 携程 | DynamicAPK |
VirtualAPK | 滴滴 | VirtualAPK |
RePlugin | 奇虎360 | RePlugin |
Atlas | 手机淘宝 | Atlas |
其中任玉刚的dynamic-load-apk插件化框架就是采用了上述所说的代理思路,上诉有些框架已经很久没有维护了,现在比较热门且还在维护的应属360的RePlugin,嘀嘀的VirtualAPK,手机淘宝的Atlas以及Small框架,其中Small框架支持Android和ios,较为轻量,但似乎还没办法做到按需加载。而淘宝Atlas框架相比其他具有更丰富的功能,除了可以按需加载相应的功能模块外,还具备热修复功能。
六、是否使用插件化技术的思考:
- 是否存在版本较多需要不断更新发版的情况?
- 是否有较多的业务模块?
- 是否开发人员众多?
- .....