关于标题
“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。
(由于排版问题,此图省略了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
一致;或者修改服务中心路由配置,让原来指向RunProvider
的run authority
,改成指向MockRunProvider
的mock 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,不会影响其他业务。
总结
业务解耦手段有很多,本文提及的“provider解耦”仅仅是笔者习惯做法,对于文中描述需要解耦的地方,可以使用其他解耦方式。
一个工程多application并不是最好的开发方式,它不适合业务非常庞大的APP,例如支付宝、天猫、携程、链家等,超级APP必然是多project玩耍;也不适合业务量很少的APP,仅仅适合当前悦跑圈Android团队。
我们不会止步于现状,未来会做业务持续集成,有可能单个业务为一个工程,分多个工程开发(目前部分组件为单独工程,主工程通过maven仓库依赖)。
我相信有不少读者经验比我们要丰富,如果你有其他观点或疑问,欢迎吐槽本文。
(本文所有观点,仅代表笔者个人,不代表悦跑圈开发团队)
关于作者
我是键盘男。
在广州生活,悦跑圈Android工程师,猥琐文艺码农。每天谋划砍死产品经理。喜欢科学、历史,玩玩投资,偶尔旅行。