团贷网Android客户端架构演进之路(上)

写在前面的前面

到草稿箱里翻看一下:2018年5月18日,这一篇博文已经在草稿箱里躺了快整整一年了。5月份,恰好是项目第一次重构完成的时间节点,当时趁着总结汇报之际抽时间把项目演进历程写了一下,不知不觉就码了近1万5的字。当时也是由于各种原因,一直没把这篇博文发布对外。如今随着老东家成了网红,一切都已经人是物非,趁着这段时间在复习、总结之际,重新拎出来与大家分享探讨。
(博文重新做了编排和修正,欢迎留言交流)

写在前面

刚入职团贷网的时候,Android端App已经历经了两年多时间的迭代和沉淀。无论是从项目技术构成还是开发流程上来说,都非常吻合“短平快”的开发方式,这种模式对于小型团队的快速迭代,是非常舒服的。但随着公司规模的日渐扩大,业务快速发展,业务逻辑愈加复杂,团队成员不断增加,等等这些情况的出现,迫使我们不得不走出原来的舒适区。

当我们静下来重新审视过去代码,重新回看整个项目架构和业务逻辑的时候,自然而然地发现了其中的一些问题,如开源框架偏旧,模块代码耦合严重,不利于复用与扩展等等。而刚好在此时,我们公司也提出了技术升级的口号,自然而然地我们就走上了大规模重构的漫漫长路。

在这期间,很幸运能担任重构小组的Team Leader,感谢领导和老大的信任!项目经历了近1年时间的重构,我们做了很多事情,也否定了很多事情,也输出了一些东西,当然也多多少少获得了一些成果。截止今天,整个Android项目的重构已进入了尾声,但这篇文章并不是一个结束,只能说是我们第一次全面重构的总结与分享,后面依然还有很长的路要走…

走向MVP

重构的基调定下来之后,大家很快就进入了调研和讨论阶段,分析目前的问题,研究比较成熟的一些方案。当时Android组负责框架的同事有5个,不多不少,所以每次讨论都非常激烈。当时也是各种想法都有,要用MVP还是MVVM?要不要引入RxJava?要不把retrofit也一起加进来?还有Dagger2、ButterKnife等等一些当时比较火的技术,一片百家争鸣,恨不得把整个项目推倒重做的景象。

在是否选择MVP作为业务框架这个问题上面,我们出现了不同的声音,也许是因为我们对MVP的理解还不够深入,没有真正在项目中实践过,普遍觉得MVP只是单纯的将业务逻辑从Activity里面抽离到一个类(Presenter)中。这样做逻辑与界面可能可以彻底的解耦,但其中会增加很多接口类,调用起来的链路也比较长。但最终我们还是选择了MVP,因为在我们推进重构和引入一些开发流程的过程中出现了阻碍,就是如果在原来的传统MVC框架下,我们很难做到代码复用,而且最麻烦的是没法进行单元测试。

原业务框架

这里写图片描述
原先的架构,从界面层到业务(接口)层,再到网络层都是强耦合的。也就是如果要发起一个请求,必须以BaseActivity为父类创建一个子Activity,依赖其Handler和DisplayCallback接口进行通信。这样设计的好处就是快,开发业务只需要关心Activity其布局和数据展示即可,发起网络请求也只需要继承BaseBusiness,直接调用网络层的HttpRequest,然后数据通过Handler回调到Activity处理。整个链路简单清晰,非常适合业务的快速迭代,但其弊端也很明显,就是扩展性差,个别类臃肿,代码难以复用,甚至如果要单独在一个自定义View发起请求也很难实现(因为必须依赖于Activity)。

由此我们看到要在原来的架构上面改良,从而适应MVP的架构基本上是不可能的,必须有壮士断臂的决心——进行“改革”。
这里写图片描述
MVP的架构并不复杂,核心是在于处理逻辑的Presenter层,通过接口与View层进行交互,谷歌官方给出的Demo也比较通俗易懂。所以我们在对原先架构改造成MVP架构还是比较有信心的,如果要说难的地方,可能就需要优先将原来的网络层进行抽离。

二次封装OkHttp

网络层的设计主要还是解决现有的问题,使其能够独立的调用不再依赖于BaseActivity,并且适应于MVP架构的调用。而整个项目在经过多手的迭代之后,也引入了如HttpClient、HttpUrlConnection、OKHttp一些网络框架,在重新设计网络层的过程中,我们也同时将所有的调用发起改成新的一套请求框架。OKHttp是业内最推崇的网络请求框架,连谷歌官方都推荐使用,在Github也有2.6万个star,选择它进行二次封装是毫无疑问的。

在搭建MVP和重新封装OkHttp的过程中,我们面临了另外一个问题,到底要不要引入RxJava和Retrofit?RxJava、Retrofit两个开源框架的作者和OkHttp都是同一个人——JakeWharton大神,当然他是推崇用RxJava和Retrofit结合MVP搭建客户端架构的。RxJava属于响应式编程的范畴,区别于传统的函数式编程,语法和常规的写法大不相同,而Retrofit是原作者自己对OkHttp的二次封装,好让大家能够以更优雅的方式调用网络请求。两者与MVP的结合可以说是完美的,真正推广开之后能在代码执行效率上得到一定的提升。
这里写图片描述
但是(看到但是,前面的话其实都可以忽略),有那么一句话——任何脱离业务的架构设计,都是耍流氓。结合我们团贷网项目实际,旧框架在整个项目里面已经是根深蒂固,如果一下子切换到Rx的编程方式和Retrofit的注解式调用,无疑跟重写整个项目是没区别的。而且在当时我们的开发团队规模和水平来说,这个成本和风险都非常高。经过我们几次讨论,决定采取了折中的方案逐步推进,既先在原来OkHttp的调用上进行二次封装,在这套封装的网络层上过度到新的MVP架构,然后在后面适合的时机,再在P层与M层之间引入RxJava,最后再逐步切换到Retrofit。这个战线很长,但关键的还是需要优先做好到MVP架构的过度。

新MVP业务框架

这里写图片描述
紧接着,我们很快搭建好了针对团贷网项目的MVP架构,在View层重新封装了两层Activity父类,分别作为对网络请求回调的处理,和对统一布局、情感图的处理。并在最底层Activity父类中,做了对BasePresenter的绑定。数据处理统一放在Model层,并增加了DataRepository数据仓库,作为P层与M层交互的统一出入口。这样对于P层只需通过DataRepository获取数据,而不需关心这个数据到底来自网络请求、SharePreference还是数据库。这样的分层,我们可以非常容易地对Persenter层进行单元测试,这个在下面会介绍。

用MVP架构作为我们业务层重构的方案,既达到了解耦的目的,也符合我们引入单元测试的方向。但在代码复用方面,却显得有些鸡肋。当初的想法是,对于业务逻辑相同的一些模块,我们是可以复用P层,从而对应不同的V层和M层。但在实际操作中,结果并没有预期中的那么理想,这个我们在多项目模块复用阶段就更加明显,在最后的一节我们再给大家详细分析。

组件化、模块化

组件化是很早之前就被提出的一个概念,常常和模块化联系在一起提及,在Android领域组件化&模块化已渐渐成为了一个主流的方向。尤其是在MDCC 2016中国移动开发者大会上,由冯森林大神发表了一篇《回归初心,从容器化到组件化》的主题演讲之后,组件化技术在Android业界内就越加被推崇了。

概念区分

这里写图片描述
组件化和模块化的界定,可能大家都比较含糊。在我的理解内,组件化的核心在于重用,其次附带着解耦的特性,我们生产出一系列组件了,就可以被依赖和引用,所以应该是纵向分层的概念。而模块化的核心在于隔离,相同性质的内容被封装在一起,更多的是业务上的聚合,所以应该是横向平级的关系。
这里写图片描述

纵向组件化

纵向分层上面,我们划分了基础服务层、公共业务层,和子业务层三个层级。

  1. 基础服务层,对外提供统一的基础实施功能,包括了网络请求、图片加载、自动更新、本地缓存、权限管理、JSWebView、公共控件、公共工具等,以及一些第三方库的依赖管理。
  2. 公共业务层,对子业务模块层提供公共业务服务,如存管服务、用户信息服务等。
  3. 子业务层,顾名思义包括了团贷网所有的子业务,如一级页面、消息中心、签到、投资等。子业务的每一个业务模块,采用的是我们上面提到的MVP框架开发。

实际上在最上层,还有一层宿主层,包括团贷网 App壳和其他 App壳,用于开屏页启动、Application初始化、路由初始化、Bugly初始化等等。

纵向的组件化分层,我们采用独立工程的方式进行隔离,既基础库(基础服务层)为一个工程,业务库(公共业务库)为一个工程,子业务统为一个工程。重构的过程中,我们保留原来团贷网项目作为子业务工程,不断的将基础服务层和公共业务层的组件、模块往下沉,从而逐步拉伸出3层架构。

横向模块化

横向模块化的解耦比纵向分层的要稍微复杂一些,我们为了统一整个App端业务拆解的边界,在与iOS的同事经过多次讨论之后,确定了将整个业务体系拆解成约20个子模块,如下图所示。
这里写图片描述
子模块的边界约束,考虑到了团队的规模和开发的效率,我们没有将其拆成独立的工程,而且分拆到不同的Module,也能很好的起到依赖隔离的作用。
这里写图片描述

从基础服务开始

基础库包括了网络请求、图片加载、自动更新、本地缓存、权限管理、JSWebView、公共控件、公共工具等,以及一些第三方库的依赖管理。网络请求框架,根据上面所提及的对OkHttp进行二次封装,我们封装了一个高级入口,并将其命名为“Smart”,其他我们还封装了如图片加载框架“Monet”,JS与原生交互的"PsJsBridgeWebView",日志工具“LoggerManager”等等。

基础库的模块化

基础库是一系列的基础设施,为上层提供各种服务和工具,因此我们可以很轻松地放在一个新项目里面使用。所以在我们基础库搭建起来之后,在面对后面陆续的各种项目需求,我们可以很轻松的搭建框架,只需要依赖基础库,所有这些功能都能拿来使用。但在基础库的功能越来越丰富的时候,我们发现了另一个问题,就是基础库的功能组件太多,有些是个别项目不需要的。因此我们开始对基础库工程进行模块化,将相对独立的功能模块隔离成Module,将必要的功能保留下来,让各应用工程做到可配置的依赖。这个工作我们还在根据不同项目需求进行拆分的过程中。
这里写图片描述
从上图我们还看到一个smart_rxjava2的module,这个就是MVP框架和Smart网络库逐步向RxJava迁移的一个调研模块。

优化流程

建立自己的一套基础库,优势是非常明显的,各个项目都可以复用,相同的工作无需重复做。而在基础库模块化之后,为了优化版本管理、打包发布、依赖方式等等这些方面,我们做了以下一些措施:

  1. 搭建nexus进行maven仓库管理,并区分开发版(snapshot快照版本)和正式版,使用gradle依赖
  2. 各基础库Module的开发版统一使用整数版本号,如60、70、80;正式版统一使用整数版本号+2,如62、72、82
  3. 搭建Jenkins CI(持续集成)服务器,在基础库代码审核合并后,自动发起构建 -> 打包 -> 部署nexus 的流程

在这些工作完成后,整个基础库的管理和发布流程都变得非常轻松。在这里我们首次提到CI 持续集成这个概念。

持续集成(CI,Continuous integration)是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

简单概括,就是将本地编译、构建、发布的过程放在远程服务器多次的自动执行。在一个团队扩展到一定规模,一个项目发展到一定程度,如果依然单靠人手来进行构建、打包、发布,这显然就不够“互联网”了。能用工具去解决的问题,就不必用人手,这应该是互联网时代最大的一个特性。显然,从研发流程和效率上来讲,CI必然是一个趋势,所以这部分我们在后面会重点展开。

业务重构

在这一部分里面,我们逐步将上面罗列的20个子模块往MVP框架重构,基本的实施流程如下图所示。
这里写图片描述
业务重构是我们整个项目重构的核心,除了做技术升级,我们还需要保证质量和性能的提升。在业务重构推进初期,我也也遇到了一些坑:

  1. 各个模块独立成Module了,很多类之间脱离了依赖,模块之间的通信成了最大的问题
  2. 重构的同事对业务代码不熟悉,逻辑上容易有遗漏的地方,而同时也加到了测试的压力,难以对每个模块做到全面覆盖测试
  3. 原工程分支管理不规范,如果单独开新工程迁移的成本又过大,如何在原工程平稳过度成了关键点
引进路由框架ARouter

模块间通信是模块化必须解决的一个问题,而这个在网上也已经有很多比较成熟的解决方案,在这里就不做展开。我们经过调研测试之后,选用了阿里开源的ARouter。ARouter的功能相对来说还是比较强大的,除了可以做到模块间跳转、数据回传,还有如服务、拦截器、依赖注入等高阶用法,很适合我们项目的实际情况。

在使用ARouter之前,我们主要解决了与我们App加固冲突的问题,由于我们App使用了第三方加固功能,将dex文件进行了加壳操作,而ARouter的原理则是app启动的时候读取dex文件进行解析,生成全局的路由映射表,从而实现跨模块跳转,两者刚好是冲突的。不过在我们与第三方沟通之后,这个问题很快就得到解决了。在后来,ARouter也优化了这个问题,除了提供默认的dex文件生成路由表的方式,也提供了使用 arouter-register 实现自动注册生成路由表,这个在编译时执行的,按理来说效率和性能会更好一下,有待我们进步一测试。
这里写图片描述
引入了ARouter路由框架之后,我们并没有急着把所有的代码都抽成Module,一方面我们在几个边缘的模块内引入并且上线测试,另一方面我们在合适的时机再将重构好的模块逐步抽离成子Module,这样子可以将重构的风险降到最低。其次,我们约定了在模块间必须使用ARouter跳转,模块内允许原生跳转。但在后面在对ARouter的使用越来越有信心之后,开始逐步将模块内的跳转也统一替换成ARouter。

子业务Module化

按照原来的重构计划,是优先将个模块的代码重构成MVP框架之后,再归档到一个目录里面(加图),然后等大部分模块重构完之后再一个个独立成Module。但突然来的一个需求,加快了我们重构的步伐,那就是要做一个“新手体验版”。新手版的核心功能不变,主要是是样式、图片稍微改动一下,app图标更换,一级页面样式调整。接到这个需求,我们的第一反应就是在原来模块化的基础上,替换掉一级页面模块——home。代码依赖如下图所示。
这里写图片描述
要实现这个设计,首先将home模块独立成Module是大前提,其次我们还需要将我们的公共业务层CommonBiz抽离出来。原本以为我们代码重构好,并且已经归档到一个文件目录下之后,独立成Module也只是将代码再搬迁一下而已,实际执行并么想象中那么简单。文件目录的隔离,和Module化的隔离差别还是很大的,毕竟独立成Module之后,原来所有依赖的一些资源、model、基础类都要做相应调整。每搬动一个类,都需要考虑在其他模块有没有依赖,如果有两个模块都需要引用的类,则需要下沉到CommonBiz。

按照这个思路,我们调整了原来重构的流程,每重构完一个模块都直接将其独立到一个Module,也减少了在最后抽离Module的时候,需要再回头重复检查原本代码的流程。而且我们如果在一个Module下面重构代码,习惯性会使我们遗漏一些依赖的隔离,所以率先引用Module来隔离会更好一些。

Module与ButterKnife冲突

独立成Module还有一个需要注意的地方,就是与ButterKnife框架的冲突。如果项目使用了ButterKnife框架,在拆分Module之后应该会有如下的一些报错提示。
这里写图片描述
Attribute value must be constant.
这里报错的原因是注解里面必须使用常量——带final声明的int,而在子Module里面生成的R文件常量,都去掉了final声明。解决方案有两个:

  1. 不是用ButterKnife,用官方提供的findViewById() 来获取控件
  2. 使用ButterKnife提供的R2

使用R2似乎比较简单,改动也比较少,但有另外一个细节需要注意的。
这里写图片描述
在ButterKnife提供的OnClick注解内需要用R2,而在view.getId判断时候需要用R来判断。这就可以理解,ButterKnife作为编译时框架,它在编译的过程中实际上是将R2映射成R来判断的。

引入GitFlow工作流

我们开展业务重构之后,出现了迭代业务需求开发、多人协同重构一个模块、多个模块并行重构的现象,项目工程原来是只有一个版本分支的,如:4.8.0、4.9.0之类的,显然这样子是很难做好重构的分支管理,所以我们引入了GitFlow工作流方式,并将原来的项目工程的git域做了扩充,以便更好的统一整个团队的工程规范。
这里写图片描述
有了GitFlow工作流的管理,我们就有了代码合并Merge Request的流程,同时我们还根据我们项目实际,制定了一套《团贷网Android语言编程格式规范》,并通过Checkstyle插件在开发环境里面做自动检测,同时也作为我们MR检测的一个标准。

单元测试

此外,在每个模块重构完成之后,我们要求Presenter层的逻辑需做好单元测试。Android工程的单元测试在很多公司都很难推进,一方面是因为单元测试的维护成本比较高,尤其在业务每个迭代版本都有改动的情况下,维护起来就更加费劲;另一方面是由于Android系统本身的特殊性,它除了业务逻辑的测试,还包含如界面交互这些跟设备相关联的测试。所以我们除了使用JUnit对java的逻辑代码做单元测试以外,还引入了一些如Mockito 、Powermock 这些针对android代码的测试框架。在单元测试上面我们花了很多功夫去研究,也花了很多的时间去写和维护,但一直没用一个好的自动化执行、统计和反馈机制,所以落实的效果并不明显。

小结

这一期主要从架构演进出发,我们引入了MVP框架,引入了组件化/模块化的思想,从而推进我们整个项目结构的改变。下一期,我们会讲述新架构所带来的副作用,以及我们处理的方案。还有我们会为大家引入持续集成(CI)的概念,相信这也是很多项目正在接触的领域。
敬请期待。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值