在Twitter工作的那两年,我学习了Gradle编译Android,了解了很多Gradle的配置;在Facebook的一年里我又学习了Buck编译工具,跟Buck的开发人员有过交流。现在市场上主流的Android编译工具还是Google官方推行的Gradle,Github上很多Android开源项目也会带上Gradle文件,只有很少的项目会选用Buck。
本文就来分享一些我对两者的使用体验。不过我对编译工具的使用还停留在当做配置文件的阶段,还没有到达当做开发工具做深入自定义的程度,也没有足够的经验来总结说Gradle和Buck两个编译工具到底哪个比另一个更好,所以只能描述一些比较浅显的表层对比。
语言(Language)
Gradle使用的语言是Groovy。为了能更好的了解Gradle,我曾特尝试学习了Groovy,它算是Java的超集,可以跟Java无缝结合,只不过Groovy加入了诸多丰富的元素来弥补Java的不足。其中一点就是Groovy的闭包形式似乎又结合了JavaScript里的概念,却又不是非常成熟,当年学习的时候没有特别能领悟其中的奥秘就中途放弃了。另一方面,Gradle实际上是Groovy提供的“领域特定语言(Domain-specific language, DSL)”,然后用Groovy的语法解析去执行。然后再读Gradle的文档了解提供的特定API,内容无比繁多。好在Google总结了Android开发需要的内容,平时也只要复制粘贴必要的内容即可。
Buck使用的语言是Python。可以在BUCK文件里书写任何Python指令来执行,比如动态配置源码范围等等。但最长使用的还是对Buck规则做一层包装来进行自定义规则。不过通过跟Buck的开发人员的交流,Buck实际上是用Java开发出来的,在编译时开启了Python环境来编译Buck。
对比Groovy和Python,前者几乎没有听到过其他的应用,深入学习不会带来其他好处,后者应用广泛,但是在Android开发上也不会有更多帮助。再者,要使用Gradle和Buck这两个编译工具,也并不需要对相应的两门语言有深入的理解,只要掌握基本使用即可。
模块(Module)
这里的模块是指Intellij的项目模块,是为了更好的组织、测试、重用而创建的独立单元。每个模块都有它的属性:比如主模块是应用程序,此外我们会创建资源模块,Java库模块,Android库模块等等。当开发多个应用程序时,就可以引入部分模块来重用。
在Gradle里,模块的信息定义在build.gradle文件里,该文件中会包含应用的plugin来决定模块的属性,比如apply plugin: 'java'
指明这是Java库, apply plugin: 'com.android.application'
指定应用程序等等。
Buck的模块由定义在Buck文件里的project_config()
规则(Rule)生成。每个模块的属性有所使用的规则决定,比如android_resource()
代表Android的资源文件内容,java_library()
表示Java库等等。
关于两种编译工具对于模块的申明方式和支持的类型,还要去查阅官方文档。当模块被其他模块引用时,就形成了依赖关系,当不同模块引用第三方库的时候,可能会使用不同版本。这里就要讲讲我体会到的Gradle和Buck在处理依赖关系和版本冲突时的不同方式。
循环依赖(Circular Dependency)
循环依赖是指两个模块A和B相互依赖:A->B同时B->A。也可能通过其他模块形成相互依赖:A->C,C->B, B->D, D->A。于是依赖关系图会形成一个环形。
当被依赖的模块没有完成编译,依赖者就需要等待。所以循环依赖关系对于编译来说非常糟糕,不知道要先编译哪个模块,进而造成阻塞。
因此Gradle和Buck都不支持循环依赖关系。若是不小心引进这种关系,在编译时就会报出错误终止编译执行。解决方法网上很多,不属于本文讨论范围。
传递依赖(Transitive Dependency)
传递依赖是指三个模块A、B和C有如下依赖关系:A->B并且 B->C,即使A显示指明依赖于C,也会通过B的传递获得对C的访问能力进而形成隐性依赖。
传递依赖的好处是不用深究依赖到底來自于哪个模块,当模块有改动时也只需改动几处即可;坏处是如果B移除了对C的依赖,那么模块A中对C的引用就会失效从而形成编译错误。
Gradle 支持传递依赖,所以当使用的第三方库A用到了另一个开源库B,就可以不用在应用层再显示依赖另一个库B就能直接使用了。而且这个依赖可以通过多层传递下来。
(更新:就在完成这篇文章三个月后的Google I/O上,Android 和 Gradle 发布了依赖声明的修改,原来 compile
指令被改变成了两个指令:implementation
和 api
。使用 implementation
引入的依赖库不支持依赖传递,要实用传递依赖需使用api
。)
Buck默认不支持依赖传递,但是有些Buck规则(比如java_library()
, android_library()
)中有个参数参数叫exported_deps
可以指定某个模块A被其他模块B依赖时,B自动加入A的exported_deps
列表中的模块作为依赖。不过这个传递只能是直接的一层,当C依赖于B时,C不会获得A指定的那些依赖。
个人觉得禁止依赖拥有传递性是个好事,因为这样就很清楚的知道模块到底依赖了哪些模块,也容易分析指定模块被哪些模块所依赖。这样也方便分析这些模块是否是必要的进而做更好的优化。
版本冲突(Version Conflicts)
当几个不同的模块使用了一个库的不同版本,比如使用的Pull-To-Refresh库用了24.0.0版的RecyclerView,而开发应用中用的突出23.0.0。在最终的的依赖图(Dependnecy Graph)上就会出现不同版本行程版本冲突。但应用程序只关心库的内容,并不知道也不关心版本的存在。所以就需要版本解决版本冲突来让程序可以正常编译和运行。
Gradle解决版本冲突的方式是强制使用最新的一个版本,其他依赖在低版本的模块都会自动使用统一后的较高版本。如果升级后应该不向后兼容造成编译错误,那么就只能通过人为去修复。
Buck没有版本冲突的问题,因为Buck对使用的第三方库会下载到本地做拷贝,因此所有对该库的依赖都是同一本地版本。当需要升级版本时,就升级那份拷贝即可。
Gradle的自动版本冲突解决方案,让开发者可以专注在功能实现上,并能随意简便地升级使用的版本。但是当项目变得庞大以后,升级版本带来的负面作用就可能会很大,使用Buck可以让本地版本停留某个版本上直到必须要升级时才能更新。当然Gradle也有方法可以约束库的版本,比如使用forced,定义Groovy方法活变量来返回指定版本号。
本文比较了Gradle和Buck编写和模块处理方面的区别,下一篇文章将对比两者在使用上的体验差异。