这篇文章是我在 2018【携程技术沙龙移动技术工程化】技术分享时所讲内容的文字版本,修改删减了演讲时的冗余言语。 发布在【掘金专栏】,但愿能给买不到票参加大会的朋友带来帮助。java
你们好,今天跟你们分享的主题是《Android工程模块化平台的设计》git
首先自我介绍一下:我叫张涛,目前就任于饿了么移动技术部。可能有些朋友认识我,我以前也会在我博客【开源实验室】写一些Android相关的技术点,若是对今天讲的模块化设计,你以为有什么问题或者能够深刻探讨的,也欢迎加我微信kymjs123
详聊。编程
今天咱们讲的主题是基于项目模块化来讲的,模块化是什么你们确定都是知道了的,这里问一下你们,有多少人在此以前有作过模块化的,举个手我看一下;了解过据说过模块化的呢?此次比较多。
咱们说,作模块化其实跟项目重构很像,都是从这几个点来作的,只是侧重点不一样。分别是:删除、组织、降级、解耦。那么这四点是什么意思呢,那么接下来跟你们分享一下我是如何理解这四大块的:后端
删除:删除没必要要的文件,尽量减少工程体积。这里有一组数据,是我统计咱们饿了么的一款 APP 在模块化先后一些文件的数量。
能够看到,.java
文件从1677个减小到了1543个。其实这不是重点,重点是下面的drawable
,这里drawable
只包含图片、和xml
布局,当通过模块化重构后文件数从 693 减小到 538 个。图片资源减小接近 200 个,apk 的大小也会随之下降。微信
而组织呢,指的是:按照有意义的标准将代码分组。这其实也是java
的包所存在的目的之一。
可是随着项目的不断迭代,需求很紧的状况下是很难有时间去真正规范的将类分组的。看到图中,咱们以前的结构很乱,就是由于项目快速迭代和人员更替的过程当中,难免会有这样的现象。因此这也是模块化重构时所做的一件大事。架构
接下来就是咱们常常说的内聚和耦合了,降级。咱们以前有一个类叫:Navigator
,它是负责几乎全部Activity
直接跳转的。就是咱们会把全部的startActivity()
的跳转放到这个类里面去写。以前少的时候还好,结果等我看到这个类的时候,这个类已经有 200 多个方法了,全是Activity
跳转的方法,其中还有重复的,就是很早以前有人写了一个跳到某个界面,结果以后来了我的,他不知道又写一个。app
而咱们在作模块化重构时的作法就是,首先观察本身的项目,这是重构很重要的一步,就是要结合自身。把这个类拆分红了三大部分,咱们有两块业务是会频繁跳转的但这两个业务跳转的页面又都是在自身的模块内,分别是用户模块和商户模块。所以咱们将这两个模块中分别创建两个用于模块本身内部的跳转叫UserNavigator
和ShopNavigator
,而模块间的跳转或一些小模块内部的则使用Router
去作,咱们本身定义了一个路由库,其实实现跟如今开源的区别不大。模块化
最后解耦,也是今天的重点,如何优雅移除模块间的耦合。 到目前为止,咱们已经可以作到让全部不包含业务状态接口的模块的增删,不须要改动任何一行代码。 具体到一个示例就是这样:工具
或者,也能够是这样:组件化
这两个段代码的区别就是一个是手动管理Debug
的状态,另外一个是交给Gradle
的编译任务去控制,原理上是同样的。
而这么作是如何实现的呢,其本质就是:一个模块就是一个功能,你想要让你的 apk 具有这个功能,就添加这个模块一块儿编译就能够了。这才是咱们说的真正的组件化,模块之间零耦合,增减模块零改动。
例如图中:debug
这个模块,确定不会用在正式的生产环境;而相反的tinker
这个模块,热补丁确定也不会用于调试阶段。因此我在开发时就能够不使用这个模块相关的代码。
另外再举个使用的例子:我有一个订单模块,订单模块须要播放铃声,好比你们在饭店常常听到“您有新的饿了么订单,请及时处理”。但我在开发订单模块的时候,若是我已经肯定铃声播放是没有问题的,那我能够选择开发阶段不打铃声的包,直到发布到线上了再去加上铃声的包。那我没有添加这个铃声模块的时候,我就默认不具有播放铃声的功能,但彻底不影响其余的订单模块的业务功能,而这个铃声模块的增删,是不须要修改任何代码的。
听到这里相信你们都很好奇这是怎么实现的。接下来就跟你们讲讲内部的原理。
全部的核心功能都来自咱们本身写的一个库:IronBank
。取《自冰与火之歌》中的【铁金库】,叫铁金库不容拖欠。
铁金库的内部实现,实际上是使用了 APT 注解处理器,去在编译时解析注解生成一个类,让这个类去生成跨模块的对象。铁金库使用了与后端 SOA 设计思路相似的方式:将模块之间的主动依赖倒置,变为功能的提供与使用。
那什么是 SOA 的设计思路呢,咱们看到一张我画的漫画图:SOA 它是一种面向服务的架构模型。
例如图上左边有一个对外提供媒体功能的服务提供者,他告知IronBank
我提供媒体服务:“嘿,老铁,我这有个媒体服务,你那边有谁要用的时候能够用个人。”
到了另外一边,若是此刻有模块说是,我须要媒体服务:“老铁,你那有没有媒体服务,我这边须要播一个铃声啊!”。
“有的,给你。”
IronBank
就会将以前服务提供者提供给他的媒体对象交给服务使用者。
接下来咱们来看具体到代码上是如何使用的:首先是做为服务使用方,也就是上一张图右半部分。咱们看到传统的作法是首先声明一个接口类型,而后new
出接口的实现类给他赋值。
而使用了IronBank
的时候,你是不须要关心接口的实现类究竟是谁的。这就是IronBank
惟一的用处,隐藏实现类,作到完全的面相接口编程。
以前说过,IronBank
将模块之间依赖倒置,由以前的服务提供方被动的接受调用方调用变为,服务方主动提供服务给调用方。
那做为服务提供方须要作些什么事呢,很是简单,你只须要给你的对象提供public static
方法,并加上一个@Creator
注解,告诉IronBank
这是一个建立器方法就能够了,其余任何事情,都不须要考虑。
前面讲的IronBank
适用的场景是无状态的服务,而咱们作业务APP开发的时候更多的是有业务状态的对象,比方说咱们一般长链与推送功能是等到用户登陆了之后才会去启动,但具体到代码上,推送模块是根本不知道用户何时登陆的,这就是一个业务状态的问题。 而对此咱们引入了一个BizLifecycle的接口,他其实与Android上的Application对象功能相似。只不过他用来管理的是业务的生命周期,而不是应用的。
那么在代码逻辑上,每一个模块若是关心你所须要的业务生命周期,只须要注册一个Lifecycle就好了,同时注册的过程也只须要一个注解,由编译插件解决了。
能够看到,其实这样的一种能力用事件通知也能够作到,比方说广播或者EventBus,可是咱们刻意屏蔽了这种方式,就是由于事件通知这种功能你是很难去追踪的,你不知道一个消息发送了之后,他的接受者是在哪里。相信你们也能狗想象获得,一个应用若是广播泛滥,处处都是事件接收事件发送会项目代码会变得多么吓人。
讲到这里,整个模块化解耦的所有能力就跟你们介绍完了。接下来,咱们再从宏观角度去看一下整个项目的结构,分为三级,最上层是业务模块,紧接着是一些可选的功能组件,最底层则是与项目无关的公共依赖。
最终,项目结构就是如图中所示的这样。但若是你真直接这么作,你必定是会烦死的。
为何?
第一:这么多的模块,直接用源码依赖去编译,编译时间至少在10分钟以上;
第二:模块的隔离几乎为0,任何一我的依旧能够修改任何一个模块的代码,而且很容易;
第三:在发版本之后,若是某一个模块有BUG,再去修复,缺少一个版本的概念,尤为是在跨团队的时候,最终必定会出现版本分裂问题。
解决办法我想你们都知道,就是将模块引用改成aar引用。aar引用最大的优点就在于模块版本的管理与跨团队的协做。
目前国内对Android领域的探索愈来愈深,应用规模也愈来愈大,为了下降大型项目的复杂性和耦合度,同时也为了适应模块重用、多团队并行开发测试等等需求,你必须有一套合适的模块化平台。
这里是咱们饿了么目前使用的模块化平台,你们能够从这张图中感觉一下。
模块化平台,主要的功能是很明显的,就是用于构建模块,在这之上,还有隐含的功能,就是集中了构建模块的权限,能够更便于统一管理;
固然还有最重要的优点就在于模块版本的管理,你能够很清晰的知道当前主应用所接入的模块的版本是哪一个,当前最新构建的SNAPSHOT是哪一个,以及每一个版本的更新日志;
这样作了之后,在跨团队协做上的沟通就大大下降了,若是你已经接入或者即将接入的模块是另外一个团队开发的模块组件,那你能够直接关注它,它的全部版本变更日志,最新版本全都一目了然;
而且能够经过平台简化模块的测试与模块发布的流程,好比提测的时候,若是是一次兼容版本的发布,你只须要告诉测试提测分支,测试能够本身根据如今线上应用的tag,同时引入当前提测的模块替换老版本的模块从新编译,很容易就能控制变量。
引入了平台化之后,咱们再从工程结构的角度看一下:就目前咱们尝试下来,这两种结构是最合适Android工程模块化的。一种是submodule,一种是multi-project。
首先看submodule:这种结构是Android默认的多模块结构,在一个工程下面有多个模块。图上每一个绿色的方块都表明了一个git仓库,而后咱们看到全部子模块都包含在主工程模块内。这种结构也是git默认支持的submodule结构,你只须要用最下面的这句git命令就能够将他们关联在一块儿。
它的好处就是全部都是默认的,任何一我的理解起来都是很直观。固然,他也有不适合的,就是协做开发的时候,全部人都在app module
上测试本身的模块,很容易互相影响,主工程的git分支也会很是繁杂。
与之对应的,multi-project能很好的解决这个问题:全部模块都是一个独立的工程,他们在文件系统上是并列关系,每一个模块所在的工程才是一个git仓库。
可是这种结构就对工程名会有必定的规范要求,主要缘由是在模块联调的时候。
咱们看到这段代码是写在setting.gradle
文件中的,他根据读取本地的local.properties
文件,来include
一个模块的源码,方便在模块联调的时候能够很容易的修改多模块的代码。
可是他就要求每一个模块工程的文件夹名称是以模块名加上Project
这样来命名,好比order
模块所在的工程文件夹名就叫OrderProject
固然,你也能够不遵照,只不过不遵照就得写更多代码,我这里是直接用了循环,不遵照的话可能就须要把循环拆开手敲了。
以上两种工程结构各有各的好处,没有好坏,只有合不合适,咱们内部两种结构也都有团队在用。
而后,这里是模块联调的注意事项,就是若是你模块是以源码引入的,可能还有其余模块引用了一样模块的aar,就会形成冲突,你须要本身判断一下,加个自定义方法也好,用编译插件也能够,都能作到让源码引用与aar引用互斥。
模块化架构主要思路就是分而治之,在拆分的时候最重要的就是把依赖整理清楚,那些是业务模块,哪些是可选的功能组件。最后为了团队方便以及更快的适应,还须要开发一些辅助工具,比方说我前面说的IronBank、BizLifecycle、初始化脚本等等,都是必不可少的。
最后,今天的分享就到这里,谢谢你们。