写在前面的前面

到草稿箱里翻看一下: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)的概念,相信这也是很多项目正在接触的领域。
敬请期待。

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

  1. 团贷网Android客户端架构演进之路(下)

    在上一篇<团贷网Android客户端架构演进之路(上)>中,我们为大家介绍了架构演进.业务重构的过程.在本篇中,我们会从CI出发,在新架构背景下,如何提高研发效率和质量,如何做好架构的维护 ...

  2. 抖音、美团等大厂千万级用户的Android客户端架构演进之路—

    在移动开发中,对开发者来说不同的人具有不同的能力.就像读一本书一样,一千个读者,有一千个哈姆雷特.但不管怎样,只要你是个软件开发者你就必须学习windows或Linux等操作系统的运行原理.Andro ...

  3. 微信Android客户端架构演进之路

    去年7月,笔者在InfoQ举办的ArchSummit深圳2014的架构师峰会上,分享了微信Android客户端的架构演进史.可以说,这是一个典型的Android应用在从小到大的成长过程中的" ...

  4. 人人车Android客户端架构演进实录

    前言 对于大多数创业公司而言, 初版开发时采用的简单架构,在历经数次快速迭代后,已经成为了一个"大泥球"(源于Brian Footer和Joseph Yonder的论文<大泥 ...

  5. 微信Android客户端架构演进及其对开发流程的影响

    微信Android客户端架构演进及其对开发流程的影响 http://www.infoq.com/cn/presentations/android-client-architecture-evoluti ...

  6. 精华阅读第 9 期 |滴滴出行 iOS 客户端架构演进之路

    「架构都是演变出来的,没有最好的架构,只有最合适的架构!」最近,滴滴出行平台产品中心 iOS 技术负责人李贤辉接受了 infoQ 的采访,阐述了滴滴的 iOS 客户端架构模式与演变过程.李贤辉也是移动 ...

  7. 滴滴android架构演进,滴滴出行iOS客户端架构演进之路

    自从蘑菇街的李忠老师在移动前线群里做了一次关于iOS组件化的分享之后,大家对于iOS客户端的架构非常感兴趣,展开了热烈的讨论.我很认同一句话,架构都是演变出来的,没有最好的架构,只有最合适的架构,刚好 ...

  8. [2016.10.17日更新]各大互联网公司架构演进之路汇总

    大型网站架构演化历程 大型网站架构技术一览 Web 支付宝和蚂蚁花呗的技术架构及实践 支付宝的高可用与容灾架构演进 聚划算架构演进和系统优化 (视频+PPT) 淘宝交易系统演进之路 (专访) 淘宝数据 ...

  9. 各大互联网公司架构演进之路汇总

    大型网站架构演化历程 大型网站架构技术一览 Web 支付宝和蚂蚁花呗的技术架构及实践 聚划算架构演进和系统优化 (视频+PPT) 淘宝交易系统演进之路 (专访) 淘宝数据魔方技术架构解析 淘宝技术发展 ...

最新文章

  1. (学)DEV在设计界面部分组件显示红叉并报错的问题
  2. unity2D限制位置的背景移动补偿效果
  3. wx.createInnerAudioContext seek方法执行后,监听事件onTimeUpdate无效?
  4. pygame外星人2
  5. 【牛客 - 373A】翻硬币问题(博弈,结论,分析)
  6. 表单相关标签之textarea,select
  7. 图像处理之基础---大话小波和卷积
  8. VSCode如何返回上一步
  9. Gensim库之Doc2Vec模型详解
  10. 【可收藏】3W字,Docker 从入门到精通
  11. 如何从零开始搭建公司自动化测试框架?
  12. 音视频多媒体开发基础概述之颜色空间(2)YUV YIQ YCrCb CMY颜色空间
  13. 【C++】引用以及关联函数(详解)
  14. 使用跟踪查看器查看 ASP.NET 跟踪信息
  15. 彻底掌握 Javascript(九)数组【讲师辅导】-曾亮-专题视频课程
  16. 个人项目记录 -- VChat
  17. 基于android的记账APP大作业项目
  18. '.'和'..'还有'./'和'../'
  19. Python函数部分2
  20. ajax对日期处理,散景如何使用Ajax更新数据源处理日期时间

热门文章

  1. linux创建raid步骤,Linux 软Raid创建方法:
  2. 飞机荷兰滚产生原因、受力分析、解决方法
  3. 佳明手表大数据应用_如何看待悦跑圈、咕咚这两款 APP 一直没能接入佳明的数据?...
  4. ResizeObserver loop limit exceeded
  5. LINUX在中国的前景
  6. 《完全用Linux工作》(原版)作者:王垠
  7. Ubuntu 更新源地址列表更改方法
  8. 微软封禁俄罗斯下载 Windows 启示录
  9. 容颜易老心憔悴,岁月易逝不留痕
  10. word标题和目录制作方法