×

悦跑圈Android单业务开发,提高编译效率15倍

96
键盘男
2017.04.24 00:14* 字数 5085

关于标题

“15倍”是怎么算出来的呐?

配置:
Mac mini,双核 Intel Core i5 2.6GHz,8G内存,SSD(自行更换)

性能:

全量编译application平均需要5分钟,单业务application编译运行,平均仅需7~20秒. (300s/20s = 15)

(如果你用相同配置windows系统,基本是编译不过去的,至少要i7。)

什么是单业务编译

如果读者你看过笔者写的《App组件化与业务拆分那些事》《Android使用Provider做业务数据交互》,就能了解如何对工程划分业务了,此处不累赘。

那两篇文章,是为本文做铺垫。前两篇讲了开发思路、理念方面,说了那么多优点,貌似也没什么实质数据支撑。那么,本文让你见识一下分业务开发的力量。

常见的开发模式,是一个工程,一个application、多个module或无module

单application,多module

这种模式不足够支撑日益庞大的工程。于是,我们提出:

一个工程,多个application、多个module。

多application,多module

(由于排版问题,此图省略了Base Library)

Main Application跟“单业务application”时的Application一样,就是我们打包的application. 那么,application A、application B...又是什么呢?

从图里面看出,Application A只依赖Module A,Application B只依赖Module B....每个application可独立编译、运行。

独立编译业务优势

1.编译快、单元测试快
2.简化调试流程
3.更专注业务
4.可嵌入不需要在Main Application执行的代码
5.隔离不必要的业务和组件

1.编译快

正如前文提到,悦跑圈从全量编译平均5分钟,降到单业务编译7~20秒。就这么一个优势,就有充足的理由让我们从单application改造成多application

单元测试快

说说单元测试吧,很多同学都冲着“单元测试快速反馈结果、能提高开发效率”。然而,自身工程臃肿不堪,跑一个单元测试gradle会全工程扫描(部分或全量编译),时间上压根谈不上快(尽管比运行app再手动debug要好很多),有error还报错。这种情况还是挺常见的,例如你在重构代码,改几个业务,必须所有业务代码都写好(至少没有error),才能跑单元测试。

当项目按业务分成若干module,跑单元测试时,gradle仅扫描该业务module,即使其他module有报错,也不影响本次单元测试。而且因为编译代码量少,单元测试就能跑快了。

我们目前跑一个junit单元测试,算上编译和运行时间,仅需几秒。如果用robolectric跑DAO测试,就比较慢了,这个跟robolectric框架机制有关。

2.简化调试流程

业务比较多的APP,不一定每个功能的入口都很明显,有可能依赖其他功能流程。单业务application运行很好地解决了这个问题。

例如,你要调试“查看某用户勋章”,那么你必须先进入该用户界面,再点击“他的勋章”,才能进入“勋章墙”界面。你要找到用户名叫“键盘男”(uid=4)的用户,必须先去“发现用户”界面,输入“键盘男”或“4”,点击搜索,从搜索结果列表,找到想要找的“键盘男”用户,再点进去......光想想就觉得蛋疼,更别说做代码调试。

发现用户 -> 搜索“键盘男” -> 进入用户详情界面 -> 进入uid=4勋章墙

全流程

由于单业务application不会编译进Main application,因此我们在单业务application做什么都可以。于是,我们可以在“勋章application”写一个页面,有个Button和EditText,EditText输入uid,点Button直接进入该uid的勋章墙界面。是不是很简单?

EditText输入4 -> 进入uid=4勋章墙

单业务流程

同学们回忆一下,自己是不是把很多调试时间浪费在繁琐的流程上?

3.更专注业务

这个话题在笔者前两篇文章提及很多次了。一个业务一个module,能让你更清晰地管理业务代码和资源;现在,一个业务一个application,能让你更清晰地梳理业务流程,单业务application仅依赖当前业务module,让开发人员隔离无关业务代码。

对于经验尚浅的同学,这点可能稍微难理解。说实话,“专注业务”听起来比较虚,但恰恰能无形地让程序员提高开发效率。

例如,单业务开发把业务之间代码隔离,一来不会让你的业务代码干扰其他开发人员,二来其他业务代码也不会干扰你的代码

再举个反例,如果你在业务A写了ImageUtils.getSize(),其实这个ImageUtils跟业务A相关,例如根据dpi获取A界面某图片显示大小;在没有分业务开发情况下,业务B也调用你的ImageUtils.getSize(),当业务A修改ImageUtils.getSize(),业务B就有bug了.....如果是分业务开发,业务A下的ImageUtils(应该命名为AImageUtils)不可能被业务B调用,那么开发业务B的同学,只能乖乖地自己写一个BImageUtils.getSize()

CodeReview时,审核人要review业务A代码,他只需要看A module下的代码即可,不需要关心其他module代码,那么review速度和质量,自然有所提高。

这些情况,在平常开发中经常碰到,分业务开发可以很大程度上避免上述低级错误。归根到底,分业务就是高内聚,低耦合的编程理念:单业务高内聚,业务之间低耦合。

4.可嵌入不需要在Main Application执行的代码

这主要针对调试时修改数据的场景。例如,调试“无本地缓存,重新请求数据”场景,如果每次调试都要清空整个app数据,很麻烦,而且可能引起流程上的问题。

单业务application为这种场景提供了解决方案:在单业务application某Activity,点击某个Button,执行清空某数据的代码。

调试场景:
1.断网 -> 勋章墙 -> 显示缓存勋章
2.断网 -> 清空勋章数据 -> 无勋章,并显示默认图

清空缓存

如果没有单业务application,怎么做?很可能在某个界面,写一个Button,点击清空数据勋章缓存。但这里有问题,Button是写在勋章业务module,Main Application会依赖,在打包时需要去掉这部分代码,或者加if(BuildConfig.DEBUG)等条件。笔者非常不支持这种会影响Main application的做法,有可能因为这个改动,引起不必要的bug。

单业务application还能实现很多场景,希望同学们能自己去发掘!

5.隔离不必要的业务和组件

关于“隔离业务”,上文已经提到了部分,这里补充一下更多场景。

隔离其他业务数据

悦跑圈Android 勋章业务 需要获取 跑步业务 中个人跑量数据。按常理,为了实现这个需求,勋章业务 要依赖 跑步业务。我们的《Android使用Provider做业务数据交互》方案,正好解决依赖问题。

使用Provider可以让勋章业务不需要依赖跑步业务,但问题来了,个人跑量数据谁提供?在单业务application中,我们可以声明一个MockRunProvider,跟跑步业务RunProvider实现同样的RunProtocol接口,并保持authority一致;或者修改服务中心路由配置,让原来指向RunProviderrun authority,改成指向MockRunProvidermock authority。当然,MockRunProvider返回的数据不是真实的,是写死的。

由于编译运行单业务application很快,你可以快速地修改MockRunProvider数据来调试,这让开发人员省去很多不必要的流程。例如,需求当跑量大于1000KM显示文案X:正常开发流程,服务器上必须有某用户信息跑量大于1000KM,跑步业务请求回本地,再调用RunProvider才返回个人跑量,又或者在debug模式动态修改变量;而单业务application+MockRunProvider只需要写死跑量1000+KM就可以走后面的流程了,根本不需要关心服务器数据和请求数据,也不需要debug模式修改数据,开发人员只需要关心勋章业务如何实现即可!

隔离、替换组件

悦跑圈Android使用各种开源库,在《国内Top500Android应用分析报告》就看到我们使用RxJava(logo还挺醒目的_)。不是每个业务都需要所有组件,那么单业务application,可以按需要引用开源库(当然rxjava无处不在),原理跟按需依赖业务module一样。

替换组件,目前我们还不需要做。之前去GMTC听天猫团队介绍,他们的日志组件、网络组件等,代码量都很大,而且做了很多事情,如果在业务开发时使用,一来加大编译压力,二来调试比较麻烦。他们业务、组件本身是接口隔离,在开发业务时,可以使用更简单的日志、网络组件,来替换笨重的原组件,可以缩短编译时间,而且调试方便。这一点跟上文提到“业务隔离”思路如出一辙。


遇到困难

上述侃侃而谈单业务开发那么多好处,事实上我们也是匍匐前进、连爬带滚地一路走过来。要做到业务高度隔离,并不是一件容易的事:

1.对框架熟悉
2.基础框架必须高度解耦
3.开发团队要有足够经验

1.对框架熟悉

对框架熟悉是重中之重。不像很多年轻APP,悦跑圈从第一行代码到今天,经历了不少年头,当年还是用httpClient啊!笔者有幸从悦跑圈第一行代码待到现在(居然还未被老板炒鱿鱼)。项目早期框架,也是各种耦合,有不少无注释隐晦的代码,笔者还是挺熟悉,毕竟不少烂代码出自笔者手,,这给后期重构带来很大帮助。

开发团队还会遇上人员变更的状况,写某个业务的程序员离职了,然后这个版本要对这个业务加需求。如果原业务代码很隐晦,注释又少,这就非常蛋疼了。笔者建议,如果原代码真的非常难懂,直接重构吧!(记得做单元测试)

2.基础框架必须高度解耦

原则上,基础框架是不能跟任何业务耦合的。要做到这点相当不容易,例如请求接口时,需要带上用户信息,用户信息需要依赖用户业务,怎么办?

目前的解决方案,把用户信息作为静态变量,用静态方法获取,如果获取不到,读取本地数据。因此,读取用户本地数据,作为基础框架一部分。用户业务还有很多功能,请求用户信息、用户界面等,这些跟基础框架是解耦的。

还有组件,日志、网络等组件,这些是不是应该互相解耦?目前我们还是有相互依赖,一并放在基础框架,日后我们会改善这一块。

总而言之,基础框架越轻量越好,保证必要的功能,不是经常使用的组件、业务,让单业务application分别依赖即可。

3.开发团队经验

不怕神一样的对手,只怕猪一样的队友。

这年头,要找靠谱的程序员,十分不容易。遇到水平低的小伙伴,王者农药团灭多痛苦就不用提了。幸好笔者领导挑人的水平,还是不错的。

有靠谱的团队,是框架改进的重要因素。团队在技术上一拍即合的概率,不比遇到一见钟情的伴侣高。如果萍水相逢的队友,在技术上有分歧是正常的,如果由于技术前瞻性不足,对前沿技术抱着抵触心理,麻烦就大了。之前有位2015年末离职的同事,工作经验比笔者要久,他在使用rxjava上存在分歧,认为需要更长时间观察rxjava。经过一番讨论,他选择离职(当然并不主要是rxjava的原因,更多的是对团队氛围的不适应)。现在,做Android的同学谁不知道rxjava?

本文所说的分业务开发、单业务application,基础框架解耦,实现起来并不简单,在笔者刚提出这种方案时,也不是所有队友都同意。有队友认为这样做太麻烦,还不如用Freeline来得直接。不过笔者不习惯使用Freeline,因为使用时遇到不少坑,还是老老实实把代码分业务,减少单次编译量这种做法才能从根本上解决编译慢的问题。目前Android项目是允许Freeline存在,毕竟分业务开发和Freeline是不冲突的,多尝试前沿技术,对团队来说好处多余坏处。

如果你有改进框架的想法,在靠谱的团队里面,遇到的阻力会少很多。当然你提出的方案并不一定合适,至少大家会一起讨论可行方案,给你更多意见。

好的框架,一定是最适合你的团队,而不一定是最先进的;
靠谱的团队,在框架改进遇到坑时,能及时提出适当的解决方案。


小技巧

多个业务流程联调

笔者一直强调单业务application开发,其实可以不止单业务,可以是多业务application。当需要几个业务联调(功能测试),多业务application的价值就体现出来了。

例如,报名“广州线上马拉松”,立即获得“广州线上马勋章”,并需要弹窗提示。这里涉及两个业务:线上马业务勋章业务,线上马拉松报名页是 线上马业务,勋章弹窗是勋章业务。这时一个application依赖 线上马module勋章module,线上马报名完毕后,调用 勋章provider,请求后端数据,执行弹窗逻辑。

当需要多个业务联调,application可以依赖多个业务,但必须保证业务module之间是解耦的。

反向调用Main Application代码

在重构过程,不时会遇到某些代码互相耦合,一改就要改一大片。在有限的开发时间里,这样做是很危险的,因为你不知道遗留代码牵连到多少旧代码,太大的改动很可能引起各种bug不说,紧张的开发周期不允许你这么做。

本文不断提到provider做业务之间数据交互,不仅仅是平行的业务module之间能通过provider互相调用,业务module也可以用provider反向调用Main Application的代码。因此,遇到相互耦合代码,可以把已明确的某业务的代码放到该业务module,依赖交给provider去解耦。


不足

代码合并

我们开发使用git-flow工作流程,我相信很多同学也这么做。由于单业务application开发模式,也是把所有业务(不是所有组件)放在一个工程下,所以,单一工程git-flow合并代码时,同样有单一工程的诟病:所有代码都有可能merge和冲突

merge和解决冲突是合并时必不可少的环节,为什么笔者说成诟病?笔者强调的是“所有代码都有可能”,并不是指merge和冲突。

开发新功能到merge时,最怕就是别人改了的代码你也改了,需要解决冲突。单一工程merge分支时,对于代码审核人员来说,所有代码都可能被改动。当他Review时就非常蛋疼了,要不仔细查看每次commit的代码,要不只挑重要代码看,要不不审直接merge。当大量代码merge,审核人员根本没时间看每次commit的代码,基本只能先merge,再运行或者单元测试看有没问题。这样很容易隐藏的bug,只能相信测试工程师了.....

如果每个业务是一个工程,业务发布到maven仓库,主工程和业务之间通过gradle依赖,merge时就放心多了。因此一个业务一个工程,merge时只针对当前工程,因此代码有什么改动,审核人员心里有数,即使有bug也是该业务的bug,不会影响其他业务。

git-flow工作流程


总结

业务解耦手段有很多,本文提及的“provider解耦”仅仅是笔者习惯做法,对于文中描述需要解耦的地方,可以使用其他解耦方式。

一个工程多application并不是最好的开发方式,它不适合业务非常庞大的APP,例如支付宝、天猫、携程、链家等,超级APP必然是多project玩耍;也不适合业务量很少的APP,仅仅适合当前悦跑圈Android团队。

我们不会止步于现状,未来会做业务持续集成,有可能单个业务为一个工程,分多个工程开发(目前部分组件为单独工程,主工程通过maven仓库依赖)。

我相信有不少读者经验比我们要丰富,如果你有其他观点或疑问,欢迎吐槽本文。

(本文所有观点,仅代表笔者个人,不代表悦跑圈开发团队)

demo:https://coding.net/u/kkmike999/p/MultiApplication/git


关于作者

我是键盘男。
在广州生活,悦跑圈Android工程师,猥琐文艺码农。每天谋划砍死产品经理。喜欢科学、历史,玩玩投资,偶尔旅行。

Android
Web note ad 1