Android插件化之旅
一、概述
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框架相比其余具备更丰富的功能,除了可以按需加载相应的功能板块外,还具有热修复功能。
六、能否使用插件化技术的思考:
- 能否存在版本较多需要不断升级发版的情况?
- 能否有较多的业务板块?
- 能否开发人员众多?
- …..
Demo地址: CKTim/DynamicAPKTest
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Android插件化之旅