Android工程构建
Why
为什么我要从官方指定的gradle构建方式迁移到buck?
我们的项目包含20+个模块。如果是纯工程依赖,在Android studio+gradle 下面做日常开发一旦进行项目构建,AS就变得又慢又卡。通过检测AS的jvm发现,高峰期可以占用7G的内存(AS本身进程+启动的gradle进程)。而且还持续进行GC,不卡才怪。
虽然可以使用AAR依赖的方式来组织,但是作为AAR库也是经常需要新增功能,频繁打包发布AAR,让使用方更新集成,对开发者来说并不友好。
不要提AS 的InstantRun了,刚发布的时候,那个激动啊,Android App终于可以热部署了。然而实际上,修改代码run一下经常不是最新代码,反而更影响开发效率。团队成员最终都很失望地关闭了这个特性。
gradle的脚本写起来确实比较简单,不过你需要有点groovy的语法基础,然后学习一下gradle 的project,task相关概念,明白task的lifecycle。最后还要看看google在gradle之上定制的 build tools api。这时候你才有能力修改build.gradle ,或者写个plugin。 gradle本身的文档算是详尽清晰,但是google的gradle build tools 从开始到现在,文档十分匮乏,而且有些重要API还在持续修改。为了实现一个功能不得不 google+stack voerflow,最后还是得读源码,浪费大量时间在不重要的事情上,真心折腾。
或许build tools api的定位本来就不是向开发者提供灵活强大的定制化功能。
总结下来就是:全量构建又慢又卡,增量构建不靠谱,定制化代价高。
Buck简介
官网https://buckbuild.com上对buck的定位:
Buck is a build system developed and used by Facebook. It encourages the creation of small, reusable modules consisting of code and resources, and supports a variety of languages on many platforms.
Buck是facebook公司开发并在使用的构建系统,支持很多平台和语言(对windows的支持不好)。Buck鼓励创建一些小的可服用的模块。
为什么Buck可以做到更快?
1.并行编译依赖
2.复用上次的编译结果,最大限度减少增量编译时间
3.内置了定制化的Dx工具
其实gradle也做了这2件事情,但是在第2点上一直做的不好,复用粒度是Task级别,最耗时的是classes->dex的步骤,如果你修改了一个类,必然导致classes跟上次一不一致,就会触发再次执行dex。于是谷歌憋出了instantRun这个大招。可惜instantRun经常不生效。
在https://buckbuild.com/article/exopackage.html 官方给出了一个小工程对比了buck和gradle的性能,解释了buck热部署的实现方式。简单地说buck将Android App拆分成 bootstrap + application code。bootstrap非常小,启动很快,然后再加载真正的代码。注:这个性能对比还是在AS没有发布instantRun 的时候,现在跑估计会有出入。
通过在android_binary()指定use_split_dex = True,
buck会将主模块、依赖的模块、第三方库分别打成dex。这样就生成了很多小dex。在app运行的时候动态加载这些dex。如果代码修改,只需要重新生成对应的dex。所以整个过程比较快。
完成迁移后的实际测试发现编译速度确实很快,现在全量编译1min6s。增量编译10+s, 热部署10s内完成。
核心概念
这里只做概括介绍,更多细节看官网
build rule
就是一段程序,将输入处理成输出。(这不废话么)
每一个build rule 都有0个或者1个输出,这个输出可以做为另外一个build rule的输入。
buck内置了一些常用的rule 比如java_library(), android_binary()等等。
build target
一个用于标识build rule的字符串。这样可以用 buck build xxxx 执行构建。
build file
更简单,命名为BUCK,保存build rule的文件。这里有点要注意,buck约定文件只能被"距离最近"的BUCK引用。这里的距离最近指文件目录层次,比如在同一个父目录下就比在另外目录的距离更近。
好了,核心概念就这些,剩下的就是用各种内置的build rule了!
安装
参照https://buckbuild.com/setup/install.html
macOS下可以直接 brew install buck,也可源码下载build。另外需要JDK,Ant,python2.7,Git。
还需要额外安装Watchman。也是个很有意思的工具,实时监控目录下的文件变化。
buck本身是用Ant构建的,关于为什么没有用buck构建自己的解答在这里https://buckbuild.com/concept/faq.html 主要是操作起来麻烦。
迁移步骤
ButterKnife兼容
https://buckbuild.com/article/exopackage.html 有说明,rule android_library() 代表 android library project ,这个库工程里的R值都不再是final类型,也就无法用switch case。
与gradle不同的是,在application 工程里的代码也是用android_library() 定义,不过还好使用IDE的智能提示可以很方便地变换为if esle。
还有另外一个影响点是ButterKnife,在application 工程里的注解都是传入了R的常量id。现在ButterKnife 还提供了 对library project的支持。可以看官网介绍
https://github.com/JakeWharton/butterknife
除了添加额外的依赖库还需要将之前类似 @BindView(R.id.user) 中的R替换为R2 即 @BindView(R2.id.user) 。
在脚本里要指定 final_r_name = 'R2'
脚本编写
我们的工程是multi-project的,为了支持跨工程依赖,在总工程的setting.gradle里面配置:
include(':core')
project(':core').projectDir = new File('../sdk/core')
就可以在其他模块里配置依赖':core' 模块了。
buck也支持类似的功能:
# .buckconfig
[repositories]
core = ../sdk/core
# ../sdk/core/lib/BUCK
android_library(
name = 'lib',
...
)
# BUCK
android_binary(
name = 'MyApp',
deps = core//lib:lib
...
)
工程的gradle脚本也比较多, 逐个写太耗费时间,网上有个okbuck的开源库可以提供从gradle脚本生成buck脚本。尝试了一下,会有些错误,比如对依赖的jar生成存在问题,需要自己修复。
buck与gradle很大的不同点在于buck没有使用maven那一套依赖管理,而是全部指定本地文件的依赖。不过buck提供了一个下载远程文件的rule:remote_file(),可以指定http url 或者mvn坐标。
另一个问题是buck里没有依赖传递。如果工程使用maven管理,通常配置pom文件声明依赖关系,比如A依赖B ,如果B依赖了C,那么A是间接依赖C的不需要特别声明。构建A的时候B和C都会被下载到本地maven库参与编译。对于buck需要在A里显示配置依赖B和C。这点优缺点都很明显,不过我个人还是比较喜欢支持依赖传递。对于多模块项目的第三方依赖jar/aar,比较推荐的方式是放到一个公共的目录下。
对于需要apt处理的库,比如依赖了dagger,butterKnife,按照下面的配置:
java_library(
name = 'apt_jar_productDebug',
deps = [
'//.okbuck/cache:com.google.guava.guava-18.0.jar',
'//.okbuck/cache:com.squareup.dagger.dagger-1.2.2.jar',
'//.okbuck/cache:com.squareup.dagger.dagger-compiler-1.2.2.jar',
'//.okbuck/cache:com.squareup.javawriter-2.5.0.jar',
'//.okbuck/cache:javax.inject.javax.inject-1.jar',
'//.okbuck/cache:com.google.auto.auto-common-0.6.jar',
'//.okbuck/cache:com.google.auto.service.auto-service-1.0-rc2.jar',
'//.okbuck/cache:com.jakewharton.butterknife-8.2.0.aar',
'//.okbuck/cache:com.jakewharton.butterknife-annotations-8.2.0.jar',
'//.okbuck/cache:com.jakewharton.butterknife-compiler-8.2.0.jar',
'//.okbuck/cache:com.squareup.javapoet-1.7.0.jar',
],
)
android_library(
name = 'src_productDebug',
srcs = glob([
'src/main/java/**/*.java',
], excludes = [APP_CLASS_SOURCE]),
manifest = 'src/main/AndroidManifest.xml',
annotation_processors = [
'dagger.internal.codegen.ValidationProcessor',
'dagger.internal.codegen.InjectAdapterProcessor',
'dagger.internal.codegen.ModuleAdapterProcessor',
'dagger.internal.codegen.GraphAnalysisProcessor',
'butterknife.compiler.ButterKnifeProcessor',
'com.google.auto.service.processor.AutoServiceProcessor',
],
annotation_processor_deps = [
':apt_jar_productDebug',
],
final_r_name = 'R2',
source = '7',
target = '7',
deps = [
'//.okbuck/cache:com.google.guava.guava-18.0.jar',
'//.okbuck/cache:com.squareup.dagger.dagger-1.2.2.jar',
'//.okbuck/cache:javax.inject.javax.inject-1.jar',
'//.okbuck/cache:org.javassist.javassist-3.18.1-GA.jar',
'//.okbuck/cache:com.jakewharton.butterknife-8.2.0.aar',
'//.okbuck/cache:buck-android-support.jar',
':res_productDebug',
],
visibility = [
'PUBLIC',
],
)
脚本的内容比较容易理解:
annotation_processors指定注解处理器,
annotation_processor_deps指定注解处理器的依赖。
在deps里也要依赖相应的jar。
gradle可以在dependencies里面分别配置:provided ,apt,compile 三种依赖形式,整体类似,只是gradle不需要指定annotation_processors而是通过jar的resources文件自动找到。
exopackage支持
可以参考https://buckbuild.com/article/exopackage.html 不再详述。
遇到的问题
首先是这个https://github.com/facebook/buck/pull/205,这个bug是说对于aar库buck不会copy空的res文件夹导致aapt失败, issue状态显示已经修复。按照android官方定义res是必须存在的目录,然而我遇到gms的一个aar本身就是没有res文件夹,导致aapt 步骤失败,无奈只能修改源码,检测是否存在res,如果不存在就创建一个。
最棘手的问题是https://github.com/facebook/buck/issues/828 ,由于工程依赖较多的aar库,aar库的R值都会被重新创建一遍。导致app最后生成的R.java的int value太多,超过了65535 dex失败。只得再次读源码,自行修复。修复的思路是拆分最后生成的R.java为2个组,一组是与app同包名的R,其他的分一组。第一组遵照原有的逻辑,跟app的源码一起打成dex,第二组做为依赖库打成dex。
有空再写一篇buck源码分析的文章。