组件化是将一个app拆分为多个模块进行协作开发,每个模块都是一个单独的组件,这些组件可以相互依赖,也可以单独调试运行。但是最终发布的时候,这些组件会合并在一起,组成一个整体的apk,这就是组件化开发。
插件化开发和组件化是有所不同的,插件化开发就是将一个app拆分成多个模块,但是每一个模块都是一个apk,最终打包的时候将宿主apk和插件apk分开打包,独立分发。宿主apk发布到市场,插件apk通过动态下发到手机存储空间,然后进行插装操作,宿主apk就能够加载到这个插件包,完成整个业务流程链路的闭合。
提高编译速度
开发过程中,每个插件都是独立编译运行的,会是当的提高我们的开发速度。
业务模块完全解耦
每个业务模块都是完全独立的,可以随意删除和增加某一部分的功能。
利于团队开发
每个团队负责自己的功能,减少沟通的成本。
动态更新,按需下载
不需要重新安装即可实现功能的升级,对于一些不常用的功能,可以让用户按需下载,缩减整个程序包的大小。
解决65535的限制
每个业务都是一个独立的apk,所以完全可以解决分包的问题。
插件化的实现思路有很多,市面上开源框架也比较多,其中以滴滴公司研发的一款插件化框架VirtualAPK做的相对较优秀,功能也比较强大。
特性 |
DynamicLoadApk |
DynamicAPK |
Small |
DroidPlugin |
VirtualAPK |
支持四大组件 |
只支持Activity |
只支持Activity |
只支持Activity |
全支持 |
全支持 |
组件无需在宿主manifest中预注册 |
√ |
× |
√ |
√ |
√ |
插件可以依赖宿主 |
√ |
√ |
√ |
× |
√ |
支持PendingIntent |
× |
× |
× |
√ |
√ |
Android特性支持 |
大部分 |
大部分 |
大部分 |
几乎全部 |
几乎全部 |
兼容性适配 |
一般 |
一般 |
中等 |
高 |
高 |
插件构建 |
无 |
部署aapt |
Gradle插件 |
无 |
Gradle插件 |
最近腾讯推出了一个由Kotlin实现的插件化框架Shadow,这款框架号称是全动态、零反射、无Hack实现的新一代插件化技术,因为没有用到实战项目中,所以暂时还没有做深入研究。
通过上面的概述,可以知道插件化和组件化不同之处在于每一个插件都是一个可以独立安装运行的apk,在用户使用的过程中,只会安装宿主apk,其他的插件会通过网络等方式的下发到手机的内部存储中。所以想实现插件化,就必须将手机中的插件和宿主做关联,达到可以相互通信的效果,但是宿主模块和插件是不能直接进行相互通信的,下面我们会说明原因,所以我们需要一个中间件来作为媒介,这个中间件就是插件SDK。
所以在实现插件化的方案时,项目的结构搭建应该是分三部分:
宿主模块:app(主工程)
中间件:pluginSDK(插件化工具包)
插件模块:loginPlugin、registPlugin ……(拆分的业务插件)
因为宿主和插件模块只能做业务逻辑相关的功能,所以要想实现插件化,宿主必须通过pluginSDK来加载和调用插件,那么在pluginSDK中就必须解决以下几个问题:
如果想在主模块中去使用用插件,就必须将插件加载到主模块中,这里就会用到类加载器。我们知道Android中的虚拟机是Dalvik,它不是标准的Java虚拟机,所以在类加载机制上,和Java中的类加载器是有一些区别的。
Java虚拟机运行的class字节码,Android虚拟机运行的是dex字节码。
Android中使用的类加载器是PathClassLoader和DexClassLoader,PathClassLoader只能加载已经安装到手机的dex,而DexClassLoader可以加载未安装的dex,所以我们可以通过DexClassLoader加载器来加载插件apk,在pluginSDK的PluginManager类中来实现插件加载初始化的方法
上图中的代码就是pluginSDK加载插件的核心实现,是对调用插件的初始化操作。这里主要是在宿主模块中调用这个初始化方法来加载插件,并获取插件中的dexClassLoader、packageInfo、resources三个对象,这三个对象我们会后续提供给代理类来使用
上面提到宿主模块和插件是不能直接进行相互通信的,这是因为插件中的Activity虽然继承了Activity,但是它只能看作是一个普通的类,并不是真正意义上的Activity,因为它不具备Activity最经典的两个特性:【生命周期】和【上下文】。这是为什么呢?因为我们都知道,插件是直接通过宿主app的动态加载来使用的,插件本身并没有经过Android系统安装的过程,那也就是说,插件本身没有经过AMS的处理,Context对象就是在AMS的main函数中返回的,并且四大组件也是由AMS来统一调度的,所以说没有经过AMS的处理的Activity是没有“灵魂”的,那怎么办呢?我们可以通过“代理伪装”的方式来强制赋予插件Activity生命周期的能力。
先在pluginSDK中创建一个PluginInterface接口,接口类中定义Activity具有的所有的生命周期函数和参数,用来模拟插件Activity的生命周期。
然后再在pluginSDK中创建一个BaseActivity来实现这个接口,并实现attach方法。attach方法中接收的是一个Activity实例,这样只要传入我们后续提到的ProxyActivity代理类实例,Context对象也就具备了。最后将插件Activity都继承这个BaseActivity即可。
插件加载完成,我们最常用的操作就是路由跳转,我们需要从宿主模块跳转到各个插件中去。如果用Intent的方式,那我们必须在宿主模块中的AndroidManifest中去注册插件中的Activity,这么做肯定是不合理的,如果引入第三方的路由框架,一个是违背了插件化框架设计的原则,另一个是会限制功能的开发,这显然也是不可取的。所以我们要在pluginSDK中创建一个ProxyActivity,让这个代理类来实现路由的跳转。
首先我们应该先实现从宿主模块到ProxyActivity的跳转逻辑
这里需要注意的是,我们获取跳转的Activity的时候是通过activities[0]来拿到的,这就要求我们在开发插件的时候必须将启动Activity放到该插件的AndroidManifest中的第一个节点,这算是一个隐性的规范。
然后再继续实现ProxyActivity类中的路由逻辑
根据插件Activity类的名称,可以去加载到该类的实例,因为BaseActivity已经实现了PluginInterface接口,也就意味这插件Activity属于PluginInterface的子类,所以可以通过结构引用的方式通过接口来调用插件Activity的生命周期函数,达到唤起插件Activity的作用。
解决了上面的几个,其实就可以从宿主模块中实现跳转到插件中了,但是插件中的Activity并不能正常的进行UI显示和事件的监听,是因为插件不具备上下文对象,也就无法使用任何与上下文相关的api,就拿setContentView(R.layout.activity_login)这个类来说,他的本质实现应该是this.setContentView(R.layout.activity_ login),所以他也是需要上下文对象的,前面为了解决这个问题,我们已经将代理类ProxyActivity的实例传递给BaseActivity,而插件Activity又继承自这个BaseActivity,所以也就意味着插件Activity中的上下文对象是ProxyActivity代理类的上下文,那这里肯定是有问题的,插件类用代理类的上下文去获取资源肯定是不正确的,我们必须要在代理类中拿到插件类的资源并给到插件类才行。
上面我们已经在PluginManager中初始化并获取到了插件类的三个对象,分别是:dexClassLoader、packageInfo、resources,那我们可以在代理类中去重写getResources()方法并返回PluginManager.getInstance().getResources(),并且我们还需要在BaseActivity中重写setContentView()方法,插件中才能被调用执行,UI资源才能被加载成功。
通过上面的操作,我们已经可以从宿主模块跳转到对应的插件中了,但是如果在插件中的做一些与上下文相关的操作(如:findViewById),还是要调用BaseActivity中对应的方法,所以需要重写大量的Activity的方法来给子类做支持,如下图,that就是代理类传递给BaseActivity的上下文对象。这一系类的操作本质就是插件不具备上下文对象,我们只能强制给插件赋予这个属性。
关于android的插件化原理,本次的研究大概就是以上几点,本文是从apk动态加载的层面对插件化技术的一个研究,通过代理和反射获取到插件的实例,距离商用的技术方案,还需要大量的细节优化和测试。对于市面上的插件化技术方案,各个大厂也是研发了各自优秀的技术方案,比如滴滴公司的VirtualAPK插件化方案尤为优秀。
动态加载技术会围绕着插件Activity不具备生命周期这个特征做大量的手动映射和管理,开发侵入性比较强,而VirtualAPK通过替换了系统的Instrumentation,hook了Activity的启动和创建,省去了手动管理插件Activity生命周期的繁琐,让插件Activity像正常的Activity一样被系统管理,并且插件Activity在开发时和常规一样,即能独立运行又能作为插件被主工程调用,这种方案的实现难度无疑会更大。未来更多的插件化技术会被研究和开发出来,如果插件化技术稳定发展到了一定的程度,很可能会影响App以后的开发方式。
参考:
《Activity启动流程》
《Android apk动态加载机制的研究》
《Hook机制-AMS&PMS》