​使用Buck构建Android工程

2,115 阅读14分钟
原文链接: mp.weixin.qq.com

0.导语

不论是“QQ音乐”亦或是“全民K歌”,其Android客户端目前都是功能繁多、体量庞大、方法数超过10万的庞大应用。庞大体量的工程带来了构建工程的一个突出问题:构建耗时过长。耗时问题既影响了本地开发又影响了服务器上的持续集成,而且,随着产品功能不断迭代,应用体量势必还要进一步攀升,导致了工程全量构建耗时越来越长。为了减少构建耗时,提高开发效率,我们也在不断学习、尝试一些加速构建的策略,除了使用常见的 Gradle守护进程、增量构建等Gradle已有的加速方式,市面上常见的加速构建工具也有所涉猎,例如LayoutCast, FreeLine, Instant Run以及Buck等等。

总的来说Layout CastInstant Run的策略比较相似,都是通过生成差异构建包再使其在运行期生效的策略。区别主要在二者的实现方式上,Layout Cast通过反射插入dex的方式插入差异化代码,这和很多插件化、补丁包的机制相同,至于Google最近推出的 Instant Run,则是通过在每个类的构造函数中添加插桩代码的方式插入差异化代码。(也有一些国内开发团队的热补丁方案借鉴了Instant Run的思路,例如美团的热补丁方案,和 Instant Run思路就比较接近。但是这种方案对工程入侵较大,而且成本比较高,每个类的构造函数中都需要校验一次补丁标记位。)虽然就目前来说,两种方案都有一些缺陷,比如说API版本的限制,分dex的限制,或者修改资源之后无法生效的Bug,但是增量构建的方式在大多数情况下可以极大加快我们的调试速度,上述问题也可以期待Google在Instant Run的后续版本中得到解决。遗憾的是这两种方式本质上并没有加速构建,因而当我们需要全量构建工程时,它们都不能带来速度上的提升。

FreeLine则是蚂蚁金服开发并开源的一种加速构建工具,其核心思想和 Buck相同,即采用多任务并发的构建方式,并且抽取、使用了Buckdx,DexMerge组件工具替换原生的 dex生成工具,以加速全量构建,FreeLine还具备和 Instant RunLayout Cast相似的增量构建策略,缺点是目前还缺乏足够体量的应用验证其可靠性,以及后续的对工具的维护情况还不明朗。

至于本文介绍的重点:Buck构建工具,其实早已不是什么新奇的事物,它是一款由Facebook开发、维护并开源的性能强大的构建工具。不仅在Facebook的全系列产品中广泛应用,而且在国内的微信团队也有使用。其构建的目标代码相当广泛,且对Android工程有所优化,核心思想是多任务并发的构建策略,充分发挥多核优势。相比较于 Gradle构建工具,其最大的优点是可以极大的加快Android工程全量构建的速度,是目前Android全量构建策略中的不二选择。本文无意讨论模块化架构的优劣,仅就Buck工具而言,粒子度尽可能细的模块化架构方能发挥其“并发构建”的最大优势。

1.传统的构建Android工程过程

构建一个Android工程,是一个相当复杂的过程。造成其复杂的原因不仅因为构建过程本身步骤梳理、任务依赖关系复杂,还因为Android平台碎片化严重,只看一个版本的代码并不能代表所有版本的构建过程。这里也仅仅是简单介绍一下传统的Android工程构建中,主要步骤之间的依赖,构建过程中生成的重要缓存文件。

传统的构建方式,这里理解为Google基于Gradle脚本编写的插件 com.android.applicationcom.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 SDKdex脚本来实现的,所以仅从加速 Gradle构建的角度入手,对提升构建速度,很难有比较明显的效果。

Buck工具便从这两个角度着手,一是支持多任务并发构建,每个module都会产生一个独立的 dex文件,最后再通过Dex Merge操作,将多个独立的 dex合并成一个;二是重新开发dxDexMerge组件,按照Buck官方给的文档,Google原生的 dex脚本时间复杂度为O(N^2),而改进后的组件的时间复杂度仅为O(NlogN),而按照Freeline团队给出的测试数据,Buckdx组件比原生组件快40%左右。

2.Buck工具的安装

在安装Buck工具之前,请先确认以下支撑工具已经正确的安装在你的电脑上:

  • Git

  • JDK 7+

  • Ant 1.8+

  • Python 2.7+

如果是构建Android工程,还需要安装Android SDK和NDK。

国内的一些介绍Buck的文章普遍认为其只可以在 Mac OSLinux系统上运行,但我在官网上 buckbuild.com/about/overv…并没有找到这方面的描述,尝试在Windows系统上运行,也是可以使用的,我使用的buck的版本:

>buck --version
buck version 97cdd2a490868a9dcf40148d8421ed27cf720410

可惜的是Buck工具的一个重要支撑 watchman还不能在Windows系统下运行。不过就算没有 watchman也无伤大雅,并不影响Buck的正常运行,而且从 watchman的官网: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

3.一个简单的使用Buck工具的Android工程

首先,我们需要新建一个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签名文件的配置文件

3.1 buckconfig文件

.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

3.2 BUCK文件与Buck Rule

在上述的目录结构中,可以看到,一个工程中可以有多个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最主要的工作是给我们的构建工程起一个名字。可以理解成类似于 GradlebuildType之类的配置项可以用来实现多渠道打包等功能。再结合上一小节的 [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,以充分发挥并发构建的优势。至于这个文件中的各项参数的含义,可以在官网上找到权威解释,这里就不复述了。

4.为什么Buck工具可以加速构建

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多任务并发以及缓存的优势。

5. 全民K歌工程接入Buck工具的实践

全民K歌工程在3.7版本中尝试过接入Buck工具,为了保证外网版本稳定性, Buck工具只在本地调试时使用,用以加快全量构建的速度。对工程的入侵性主要表现在以下几个方面:

  • Buck不支持远程访问maven库的方式下载第三方依赖,需要我们手动下载,并添加到buck-libs目录下,在Buck编译时,包含该目录的依赖库文件

  • Buck不支持switch(view.getId())的写法,需要换成 if-else的方式。

  • Gradle编译生成的 BuildConfig.java文件,需要手动拷贝出来,放到一个指定位置,在 Buck编译时,包含该文件。

  • Buck不会给Debug包自动签名,需要手动配置签名文件。

  • Windows系统下不支持以大小写区分文件名,因此,资源文件、代码文件、第三方依赖库,均不可以出现仅以大小写区分的文件。

对比一下使用Buck和Gradle全量构建的耗时:

使用Buck:51.3s
使用Gradle:85.3s
硬件环境:Windows7 sp1(64bit),Intel I7-4790,16GB RAM

可以看到,接入Buck工程后,K歌工程构建速度大约提高了40%,不过一个工程在接入Buck工具后构建速度究竟能加快多少,主要取决于这个工程的模块化划分策略。

6.总结

Buck工具是一种可以有效加快全量构建的构建工具,他鼓励开发人员将工程尽可能的划分成粒子度更小的模块,以便其并发构建的优势充分发挥。它由Facebook团队开发,而且经过大体量应用的使用验证,可靠性和稳定性均有保障,而且接入 Buck也不需要对现有工程进行过大的修改。总而言之,是一个值得尝试的加速构建策略。以上都是个人理解,可能有错误或者纰漏的地方,欢迎大家指正交流。