插件化算是去年到今年一直比较火的一个技术了,各个开源库的方案实际上原理都大相径庭,但在集成和使用上有一定的区别,不同的方案针对的场景也不同,下面就主要分析一下 Small 和 Atlas ,他们的使用场景,优缺点,以及踩过的坑都会一一介绍。
作为一个公司 Android 团队架构组成员,肯定就要接触最前沿的技术嘛,这之前我一直在研究长连接相关的技术,封装了一套以 Netty 为核心的 TCP 客户端库,当时架构组另一位成员一直在研究插件化,但他手上还有一些别的事要做,插件化就搁置了,技术组需求评审的时候,我自告奋勇,决心要完成项目的插件化改造。
面对未知的挑战,人在拥有一定程度自信的时候,会毫不畏惧,但真正上战场的时候,可能就不是那么一回事了。
在具体介绍一个具体的插件化框架之前,我们得先区分好 组件化,和插件化,这两个术语的含义,这两个术语都是最近移动端,特别是 Android 端非常火的概念,但能把这两个概念理清楚的人并不多。
组件化实际上是一种编程思想,而插件化是一种实实在在的技术,组件化是为了代码的高度复用而出现的,我们可以通过把不同模块的业务做成一个个独立的 Library ,可以单独对这些 Library 进行版本管理,从而可以供给给想使用它的一切 Apk,这样做的好处不仅可以提高代码的复用性,而且也可以帮助项目进行业务解耦,提升开发效率。
插件化是为了解决应用越来越庞大、占用内存越来越高、Apk 体量过大、解决65535方法数等复杂问题而出现的,插件化会把各个解耦业务单独的封装到 APK 插件中,通过插件附属到宿主 APK 中,从而完成功能的实现,这些独立的 APK 插件甚至可以单独打包成应用。比如手机淘宝,它不仅有淘宝这个宿主应用本身的一些功能,还有一些像聚划算、书城这类的其他应用也需要接入进来,那么如果没有插件化,像聚划算的开发人员就要同时维护两套代码,一套是自己的聚划算项目,一套是接入到手淘的聚划算项目,出现了Bug也得同时去两套代码去更改,这在管理和开发效率上就会非常受限。但如果我们把聚划算做成一个插件,那么哪个应用要接入聚划算,只要把这个 APK 包接入进来即可,在代码中的就是一些 compile 和 gradle 的配置,插件内部的代码完全不用你操心了。
当然,即使你是一个独立的应用,你也可以把插件化和组件化思想结合起来,把你的业务模块组件化后,再分别把这些业务做成可以独立打包的插件,这样,你的应用不仅具备了插件化的能力和优势,还可以通过 so 包替换或者打补丁,完成线上热修复的功能,甚至可以替代常规的功能发版。
之前做插件化的那个同事建议我尝试 Atlas,因为 Atlas 是目前阿里系一个比较成熟的方案,而且有一个完整的团队在维护,值得信赖。那么就先看看 Atlas 是怎么回事吧。
Atlas 是伴随着手机淘宝不断发展而衍生出来的一个运行于 Android 系统上的插件化框架,也可以叫动态组件化框架,主要提供了解耦化、组件化、动态性的支持。是目前比较成熟的方案,功能强大,但相对的,使用和集成的难度也比较大。
这里就要吐槽一下了,实际上我对 Atlas 文档不是很满意,说实话写的非常不详细,对观看者的技术要求特别高,甚至连常规的接入说明都不是太清晰,gradle 的一些配置说明都不全,我在学习和使用的过程中可谓是非常痛苦。大家接入这些框架的时候,最好加入一下他们官网提供的讨论群,里面有很多前辈或者是已经踩坑过的老手,互相交流会事半功倍。这篇文章是一个大概的概述,以后会再写专门针对各个插件的接入和原理分析的文章。
Atlas 的 GitHub 地址:https://github.com/alibaba/atlas
Atlas demo 的项目结构如下图:
这里面,app 是宿主,这里配置一些启动页和一些初始化的操作,同时,Atlas 在 Gradle 中主要配置代码都在 app 下面的 gradle.build 完成,Atlas 一个非常重要的配置就是基线版本的配置:
patchConfigs {
debug {
createTPatch true
}
}
buildTypes {
debug {
if (apVersion) {
baseApDependency "com.taobao.android.atlasdemo:AP-debug:${apVersion}@ap"
patchConfig patchConfigs.debug
}
}
}
这里的 apVersion
代表的是基线版本号,在平时的开发工程中,这个版本号往往是我们通过渠道上线的大版本,比如 1.0.0
,patchConfig.debug
代表了 Debug 版本下的一些补丁配置,这里我们设置 createTPatch true
代表我们使用 Atlas 的动态部署补丁修复方案,这里也可以设置为阿里另一款热修复方案 Andfix
。
官网的动态部署方案是根据版本号的,比如我现在需要发布一个 1.0.2
的补丁包,那么我将会在项目工程下输入指令
./gradlew clean assembleDebug -DapVersion=1.0.0 -DversionName=1.0.2
这个指令任务结束后,将会生成一些对应的版本差异文件,将这些差异文件导入到用户手机特定的目录中后,APP 就会通过解析这些文件完成动态部署。这里需要注意的是,以上指令将会产生两个补丁文件,1.0.0-1.0.2
,1.0.1-1.0.2
,修复补丁的时候,服务端需要根据用户当前的版本号来下发相应的补丁。当然如果你觉得这样有点太麻烦,想像 Tinker 一样,只需要一个补丁文件就可以完成功能替换,可以根据 Atlas 的源码进行修改,然后将插件的 so 包上传到服务器,进行插件 bundle 的完整替换,这里就需要各位小伙伴花时间自己研究了。因为我在研究过程中请教的一个老哥是这么做并上线的,所以这种方案肯定是有可行性的。
上面的工程目录图中,middlewarelibrary
是一个公共模块,插件和宿主都可以进行依赖,但这里 Atlas 提供了一种特殊的依赖方式—providedCompile,插件 bundle 中使用这种依赖公共模块将不会把依赖库打入宿主 apk 和插件 so 中,只是用来帮助你在编译的时候使用依赖代码。
firstbundle
和secondbundle
都是插件 bundle,他们在打包时候,会以 SO 的形式在 apk 的 libs 目录下,这里的 secondbundlelibrary
是 secondbundle 单独依赖的公共包,这里它会使用 compile project 而不是 providedCompile。
remotebundle
是远程 bundle,它不会打入到 apk 中,不占用 apk 体积,比如某些计算器小工具,当用户点击的时候,才开始进行下载插件,下载完毕后才能使用功能,这是 atlas 提供的远程插件的功能,需要在宿主 gradle 中进行特殊配置,指定插件为 remotebundle。这里的思路也可以作为上面的动态部署 so 替换思路。
Atlas 的优点:
Atlas 的缺点:
挖过的坑:
gradlew publish
发布到 mavenLocal 中,如果在项目 gradle 中的maven配置有问题,将会在打补丁的时候找不到 mavenLocal 中的基线包。repositories {
mavenLocal()//mavenLocal()要放在 jcenter() 上面
jcenter()
}
当然你也可以自己配置 maven ,自定义路径。
2. 插件中如果依赖了插件 libs 文件夹下的 aar 文件,可能会出问题,需要注意几个点:1. 关闭 Instant Run 功能,防止打包不完整。2. 查找库模块中是否有 gradle 远程依赖 3. 查看是否做了混淆,配置是否有问题,如果实在解决不了只能提 issue,扒帖子了。
3. 插件内部无法用 ButterKnife ,可以通过 databinding 等方式改造。
4. 宿主无法反射获取插件中的类,插件 A 也无法反射插件 B 的类,当然如果你有这种需求,说明你的解耦不够彻底,还需要进行分包改造。
Small 是一款轻量级的跨平台插件化框架,更侧重于业务的解耦,组件化开发。所有的插件支持内置于宿主包中,并且高度透明,插件编码、布局编写方式、调试与独立整包开发无异,通过 URL 来进行宿主与插件之间的通信和传递参数。
Small GitHub 地址:https://github.com/wequick/Small
Small 的集成异常简单,只需要在项目下的 build.gradle 文件dependencies
中添加 Small 编译插件 gradle-small:
classpath 'net.wequick.tools.build:gradle-small:1.3.0-beta2'
并且在文件末尾引用插件:
apply plygin: 'net.wequick.small'
这样,整个集成就完成了,是不是很简单?但 Small 对于项目结构的要求比较严格,比如宿主最好就是刚开始新建项目时候的 app
应用,这个名字最好不要改,Small 会通过名字来寻找宿主和插件。插件业务模块命名以 app.* 作开头,报名最好也要以 app.或者 app 结尾,插件业务模块都是可以单独打包的模块,这些模块和 app 一样,都有个小手机在开头,是 application 而不是 library。
公共模块以 lib.* 为开头,这些模块中放置了一些供宿主和插件使用的 java 代码和资源文件。还有一种 stub 模块,这个模块是宿主的分身模块,内部引用的 compile 包,插件模块将会默认引用。
完成这一系列如上图的工程结构的建立后,通过指令完成插件编译。
./gradlew cleanLib
./gradlew buildLib //编译lib插件
./gradlew buildBundle //编译业务插件
插件之间的跳转通过bundle.json
文件进行路由设置,然后通过方法 Small.openUri()
进行插件跳转,这里的 API 就不细说了,大家可以自己去官网看。
Small 是一个轻量级的插件化框架,他没办法承载过大体量的应用,像手机淘宝那种,每个插件实际上是一个 APP 的这种场景就不太适合 Small,它难以承载那样庞大的方法数,你可以把 Small 理解成一个可以热修复的组件化框架,它可以帮助你进行高效的分布开发,每一个插件都可以轻松的进行单独编译和打包,也不用特别的针对插件和宿主配置,版本管理也非常简单清晰。同时能支持 AppCompat 资源共享,整合插件资源的过程中,会自动分配插件资源 ID 端,避免资源 ID 冲突。业务插件也可以拥有自己的 application。
同时,Small 的动态部署就是简单的 SO 替换了,这样虽然简单,但可能每次上传的补丁大小不会小,因为相当于是插件的整包上传。如果是大版本的更新,可能不太适合用这种动态部署的方式了。总而言之,无法完全替代发版功能。
Small 对于项目的结构要求非常严格,甚至命名包名都有一定的要求规范,所以如果是把旧项目迁移到 Small 中,特别是以前 eclipse 转 Android studio 的项目,迁移的成本将会异常巨大。而且在迁移过程中要时刻关注每个分包的大小,如果过大,Small 可能会无法加载。
Small 中需要注意的地方和挖过的坑
实际上不是每一个应用都需要进行插件化改造的,你得结合自己项目的需要以及衡量一下投入产出比再做决定,一个老项目要进行插件化,首先得进行组件化,而恰恰组件化本身是整个插件化改造过程中最为繁琐和人容易出问题的地方。各位朋友在决定进行插件化改造之前,一定要慎之又慎,做好充足的预研准备。
插件化改造的过程还有一些可预料到的问题:插件化改造过程因为较为繁琐复杂,所以整个过程可能要持续很久,可能要几个月时间,但这几个月,原来的项目又不能放着不管,总有新的需求需要你开发,所以这就涉及到插件化改造和需求开发的并行问题。因为两套代码结构不一样,如果你把一个业务模块插件化了,但需求又有变动,那么你就需要在这个并行阶段同时维护两套代码,这极大的降低了容错率并指数级别的降低你的开发效率。
所以我的建议就是,先捡一些比较稳定的功能,比如聚类搜索、一些小工具如计算器,天气啊这种。然后在需求评审阶段,如果确定这个版本某些关键模块改动较小,可以在这个开发周期将这部分的组件化列入到技术需求之中。
一言以概之,组件化插件化的过程不能一蹴而就,是一项需要整个团队之间悉心协作,认真统筹才能做好的巨大工程,如果能克服困难,完成这项工程,为以后的开发和项目管理将会带来极大的帮助。所以说,插件化对团队要求很高,还是那句话,大家权衡好成本和产出。
针对插件化原理以及各个插件化技术的具体分析,我会继续在之后的文章中带来。