一、概述
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 */@Overridepublic 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框架相比其他具有更丰富的功能,除了可以按需加载相应的功能模块外,还具备热修复功能。
作者:CKTim
链接:https://www.jianshu.com/p/ef7b12b64bbe