不论是“QQ音乐”亦或是“全民K歌”,其Android客户端目前都是功能繁多、体量庞大、方法数超过10万的庞大应用。庞大体量的工程带来了构建工程的一个突出问题:构建耗时过长。耗时问题既影响了本地开发又影响了服务器上的持续集成,而且,随着产品功能不断迭代,应用体量势必还要进一步攀升,导致了工程全量构建耗时越来越长。为了减少构建耗时,提高开发效率,我们也在不断学习、尝试一些加速构建的策略,除了使用常见的Gradle
守护进程、增量构建等Gradle
已有的加速方式,市面上常见的加速构建工具也有所涉猎,例如LayoutCast, FreeLine, Instant Run以及Buck等等。
总的来说Layout Cast
和Instant Run
的策略比较相似,都是通过生成差异构建包再使其在运行期生效的策略。区别主要在二者的实现方式上,Layout Cast
通过反射插入dex的方式插入差异化代码,这和很多插件化、补丁包的机制相同,至于Google最近推出的Instant Run
,则是通过在每个类的构造函数中添加插桩代码的方式插入差异化代码。(也有一些国内开发团队的热补丁方案借鉴了Instant Run
的思路,例如美团的热补丁方案,和Instant Run
思路就比较接近。但是这种方案对工程入侵较大,而且成本比较高,每个类的构造函数中都需要校验一次补丁标记位。)虽然就目前来说,两种方案都有一些缺陷,比如说API版本的限制,分dex的限制,或者修改资源之后无法生效的Bug,但是增量构建的方式在大多数情况下可以极大加快我们的调试速度,上述问题也可以期待Google在Instant Run
的后续版本中得到解决。遗憾的是这两种方式本质上并没有加速构建,因而当我们需要全量构建工程时,它们都不能带来速度上的提升。
FreeLine
则是蚂蚁金服开发并开源的一种加速构建工具,其核心思想和Buck
相同,即采用多任务并发的构建方式,并且抽取、使用了Buck
的dx
,DexMerge
组件工具替换原生的dex
生成工具,以加速全量构建,FreeLine
还具备和Instant Run
和Layout Cast
相似的增量构建策略,缺点是目前还缺乏足够体量的应用验证其可靠性,以及后续的对工具的维护情况还不明朗。
至于本文介绍的重点:Buck
构建工具,其实早已不是什么新奇的事物,它是一款由Facebook开发、维护并开源的性能强大的构建工具。不仅在Facebook的全系列产品中广泛应用,而且在国内的微信团队也有使用。其构建的目标代码相当广泛,且对Android工程有所优化,核心思想是多任务并发的构建策略,充分发挥多核优势。相比较于Gradle
构建工具,其最大的优点是可以极大的加快Android工程全量构建的速度,是目前Android全量构建策略中的不二选择。本文无意讨论模块化架构的优劣,仅就Buck
工具而言,粒子度尽可能细的模块化架构方能发挥其“并发构建”的最大优势。
构建一个Android工程,是一个相当复杂的过程。造成其复杂的原因不仅因为构建过程本身步骤梳理、任务依赖关系复杂,还因为Android平台碎片化严重,只看一个版本的代码并不能代表所有版本的构建过程。这里也仅仅是简单介绍一下传统的Android工程构建中,主要步骤之间的依赖,构建过程中生成的重要缓存文件。
传统的构建方式,这里理解为Google基于Gradle
脚本编写的插件com.android.application
和com.android.library
作为Android工程的构建工具,二者的区别在于一个针对主工程,一个针对module。
在主工程的.gradle
脚本里,接入
apply plugin: 'com.android.application'
在module中,接入
apply plugin: 'com.android.library'
阅读源码,可以看到在构建Android工程的过程中,具体执行了哪些任务,核心的任务位于groovy/com/android/build/gradle/tasks
中,主要包括:
Dex.groovy//----------------------生成Dex文件
AidlCompile.groovy//--------------编译Aidl库
GenerateBuildConfig.groovy//------分析编译配置
Lint.groovy//---------------------进行Lint扫描
MergeAsserts.groovy//-------------整合Asserts目录下的资源
MergeResources.groovy//-----------整合资源文件
NdkCompile.groovy//---------------编译NDK库
PackageApplication.groovy//-------打包App
PreDex.groovy//-------------------生成Dex的准备工作
ProcessAndroidResources.groovy//--处理资源文件
ProcessAppManifest.groovy//-------处理Manifest文件
ZipAlign.groovy//-----------------压缩并对其操作
这些任务最终会生成shell指令,调用位于[Android SDK home]/build-tools/[build tools version]
目录下的构建工具。
忽略掉混淆、编译配置、对齐、压缩、签名等等我们不关心的任务,分析Gradle
工具构建的主要过程:
1.首先需要对资源文件进行编译:
2.之后编译那些依赖资源文件的类:
3.接着编译那些不依赖资源的类:
4.随后,编译工具开始把.class
文件整合成dex
文件:
5.最后,结合编译的资源文件,组合成.apk
文件
在Gradle
工具构建时,可以使用--profile
选项以输出详细的构建耗时报表,位于[project floder]/build/report
目录下,这个报表可以方便我们检查哪个Task
耗时最长。基本上耗时最长的步骤在dex生成这一步,主要是由于代码文件过于庞大。由于目前Gradle
工具(Gradle 3.1
)尚不支持多任务并发构建,而且前面提到,生成Dex
文件本质上是调用了Android SDK
的dex
脚本来实现的,所以仅从加速Gradle
构建的角度入手,对提升构建速度,很难有比较明显的效果。
Buck
工具便从这两个角度着手,一是支持多任务并发构建,每个module都会产生一个独立的dex
文件,最后再通过Dex Merge
操作,将多个独立的dex
合并成一个;二是重新开发dx
与DexMerge
组件,按照Buck
官方给的文档,Google原生的dex
脚本时间复杂度为O(N^2),而改进后的组件的时间复杂度仅为O(NlogN),而按照Freeline团队给出的测试数据,Buck
的dx
组件比原生组件快40%左右。
在安装Buck
工具之前,请先确认以下支撑工具已经正确的安装在你的电脑上:
如果是构建Android工程,还需要安装Android SDK和NDK。
国内的一些介绍Buck
的文章普遍认为其只可以在Mac OS
和Linux
系统上运行,但我在官网上https://buckbuild.com/about/overview.html并没有找到这方面的描述,尝试在Windows
系统上运行,也是可以使用的,我使用的buck的版本:
>buck --version
buck version 97cdd2a490868a9dcf40148d8421ed27cf720410
可惜的是Buck
工具的一个重要支撑watchman
还不能在Windows
系统下运行。不过就算没有watchman
也无伤大雅,并不影响Buck
的正常运行,而且从watchman
的官网:https://facebook.github.io/watchman/,可以看到开发团队正在开发适配Windows
系统的版本。
抛开watchman
,单看Buck
工具的安装,可以使用如下git
命令:
git clone https://github.com/facebook/buck.git
下载完成后,可以在buck
目录下通过help
指令查看是否正确安装:
>buck --help
buck build tool
usage:
buck [options]
buck command --help
首先,我们需要新建一个Android工程,一个符合Buck风格的工程目录结构如下:
>Project Root
- .buckconfig //构建工程的整体配置
- java //代码目录
- BUCK //BUCK脚本
- com
- tencent
- XXX
- res //资源目录
- BUCK //BUCK脚本
- layout
- values
- drawable
- lib //第三方应用目录
- apps //工程目录
- AndroidManifest.xml
- BUCK //BUCK脚本
- debug.keystore //debug包的签名文件
- debug.keystore.properties //debug签名文件的配置文件
.buckconfig
文件位于工程的根目录下,主要配置整个工程的整体属性,例如其文件内容可以是:
[java]
src_roots = /java/
[project]
default_android_manifest = //app/AndroidManifest.xml
[android]
target = Google Inc.:Google APIs:23
[alias]
app = //apps:app
每个参数的详细解释,可以在官网上找到,这里仅做简单解释。
[java]
参数指定了工程的源码路径,这里配置的源码路径为/java/
,在所有的buck
脚本中,用斜杠/
表示和当前脚本同一路径,用双斜杠//
表示当前工程的根目录。
[project]
参数指定了一些工程的核心配置项,例如这里配置了工程的AndroidManifest.xml
文件的路径。
[android]
参数指定了一些关于工程所运行的Android版本信息,例如这里指定的Target API=23。
[alias]
参数表示构建工程的别名,这里的配置:
[alias]
app = //apps:app
即表明,在这个工程里,我们为//apps:app
这个Buck
任务设置了一个别名:app
。所以在这个工程里用Buck
构建或者安装一个Android工程,使用:
>buck build app
>buck install app
和下面语句的效果是相同的:
>buck build //apps:app
>buck install //apps:app
在上述的目录结构中,可以看到,一个工程中可以有多个BUCK
文件,每个BUCK
文件是由一条条Buck Rule
组成,Buck Rule
有很多种,涉及编译源码,编译aar包,编译ndk,编译aidl,编译资源,整合打包,签名文件等等,详细的解释可以参考官网上的解释。这里,以//apps/BUCK
的BUCK文件为例,简单介绍一下,其文件内容如下:
android_binary(
name = 'app',
manifest = 'AndroidManifest.xml',
keystore = ':debug_keystore',
deps = [
'//java:activity',
],
)
keystore(
name = 'debug_keystore',
store = 'debug.keystore',
properties = 'debug.keystore.properties',
)
project_config(
src_target = ':app',
)
BUCK
脚本是基于python编写的,这里不赘述python的语法,但有必要注意每行的缩进格式。
先看第一条Buck Rule: android_binary
。这条Rule代表了一个Android工程的构建目标,即产生一个.apk
文件。它包含的属性例如name
, manifest
, keystore
的含义都是显而易见的,而deps
属性表示这个Rule需要依赖其他Rule的完成。前文提过,双斜杠//
表示项目根目录,出于简化考虑,不需要指定BUCK
文件,而冒号:
表示BUCK
文件里的某条Rule,因此,根据//java:activity
这条属性,可以看到,android_binary
这条Rule的执行,依赖于[Project Root]/java/BUCK
中的activity
这条Rule先执行完毕。
我们等会儿再看[Project Root]/java/BUCK:activity
这条Rule,先看keystore
,这条Rule的含义不必多说,由于Android-Gradle
工具会在打Debug包时,自动添加默认签名,而BUCK
则不会这么做,所以我们需要手动指定。签名配置文件debug.keystore.properties
如下:
key.alias=my_alias
key.store.password=android
key.alias.password=android
不必赘述这些配置的含义,我们直接看最后一条Rule:project_config
,这条Rule最主要的工作是给我们的构建工程起一个名字。可以理解成类似于Gradle
的buildType
之类的配置项可以用来实现多渠道打包等功能。再结合上一小节的[alias]
参数,我们所设计的别名app
,就是针对这条Rule
设计的。
看完了这个BUCK
文件,我们再看之前提到的[Project Root]/java/BUCK
,该文件的内容如下:
android_library(
name = 'activity',
srcs = glob(['*.java']),
deps = [
'//res:res',
],
visibility = [ 'PUBLIC' ],
)
project_config(
src_target = ':activity',
)
可以看到和上一个BUCK
文件相比,该文件里不再包含android_binary
,而是使用android_library
这条Rule,这是因为一个构建类型只能包含一条android_binary
,而android_library
可以有多条。对应到Android工程的系统架构,android_binary
相当于是主工程,而android_library
对应了多个module。前文亦有提及,Buck
工具鼓励以粒子度足够细的模块化架构,每个模块都对应一个android_library
,以充分发挥并发构建的优势。至于这个文件中的各项参数的含义,可以在官网上找到权威解释,这里就不复述了。
Buck
工具在构建的不同阶段会生成三个重要的文件:R.txt
, .jar
, .apk
,分别对应三种Rule:android_resource
, android_library
, android_binary
。如果以module为单位,每个module会对应一个R.txt
和.jar
(如果这个module和UI元素无关,那么就没有R.txt
文件),最终Buck
将这些R.txt
汇总成R.java
文件,将.jar
文件汇总成dex
文件。
分析Buck
工具的构建过程,可以看到:
1.首先,它会并发的开始多个module的资源编译:
R.txt
文件的内容大致如下:
int anim fade_in_seq 0x7f04000b
int attr actionBarSize 0x7f0100af
int color fbui_bg_dark 0x7f060071
int dimen title_bar_height 0x7f07000f
int drawable map_placeholder 0x7f0204ca
int id embeddable_map_frame 0x7f0a00e8
int layout splash_screen 0x7f030172
int string add_members_button_text 0x7f0900f8
int[] styleable ThreadTitleView { 0x7f01018a }
不同于R.java
,这里的资源属性的描述符并不是static final int
而是int
,因为在最后一步我们需要把所有的R.txt
文件集合成一个R.java
,这一步中可能需要对冲突的资源ID进行修改。
2.之后,Buck
工具开始编译各个module的源码文件,并生成dex
文件:
3.最后,分别合并资源文件以及dex文件,在打包生成apk:
至此,Buck
工具的构建就已经完成,当我们修改现有逻辑时,没发生改动的module将会直接使用缓存数据,这也在很大程度上提高了我们构建工程的速度。从这一节Buck
工具的构建过程的分析中可以看出,只有粒子度足够细的模块化结构才能充分发挥Buck
多任务并发以及缓存的优势。
全民K歌工程在3.7版本中尝试过接入Buck
工具,为了保证外网版本稳定性,Buck
工具只在本地调试时使用,用以加快全量构建的速度。对工程的入侵性主要表现在以下几个方面:
switch(view.getId())
的写法,需要换成if-else
的方式。Gradle
编译生成的BuildConfig.java
文件,需要手动拷贝出来,放到一个指定位置,在Buck
编译时,包含该文件。Buck
不会给Debug包自动签名,需要手动配置签名文件。对比一下使用Buck和Gradle全量构建的耗时:
使用Buck:51.3s
使用Gradle:85.3s
硬件环境:Windows7 sp1(64bit),Intel I7-4790,16GB RAM
可以看到,接入Buck工程后,K歌工程构建速度大约提高了40%,不过一个工程在接入Buck
工具后构建速度究竟能加快多少,主要取决于这个工程的模块化划分策略。
Buck
工具是一种可以有效加快全量构建的构建工具,他鼓励开发人员将工程尽可能的划分成粒子度更小的模块,以便其并发构建的优势充分发挥。它由Facebook团队开发,而且经过大体量应用的使用验证,可靠性和稳定性均有保障,而且接入Buck
也不需要对现有工程进行过大的修改。总而言之,是一个值得尝试的加速构建策略。以上都是个人理解,可能有错误或者纰漏的地方,欢迎大家指正交流。