现在你已经知道了Gradle是如何工作的,如何创建你自己的任务和插件,如何运行测试,以及如何设置持续集成,你差不多可以称呼自己为Gradle专家了。本章会包含一些之前没有提及的窍门和技巧,使使用Gradle进行构建、开发和部署变得简单。
本章内容有:
- 减小APK体积
- 加速构建
- 忽略Lint检查
- 在Gradle中使用Ant
- 高级应用部署
减小APK体积
在过去的几年里,apk文件的体积有了显著的增大。这里有几个原因:Android开发者有更多的库可以使用,增加了更多的密度,以及应用有了更多的功能。
保证APK有更小的体积是很好的想法。不仅是因为Goole Play有50M大小的限制,还因为更小的APK意味着用户可以更快地下载并安装应用,以及占用更少的内存。
本节,我们会讨论几个Gradle构建文件的属性,来帮助减小APK文件体积。
ProGuard
ProGuard是一个java工具,在编译阶段不仅可以压缩,还可以优化,混淆,预先审核代码。它会遍历应用中的所有代码路径,找到不用的代码并删除它。ProGuard也会重命名你的类和成员。这个过程可以减少应用的内存占用,并增大反编译的难度。
Gradle Android插件有一个minifyEnabled
属性可以设置开启ProGuard:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
当你设置minifyEnabled
为true后,proguardRelease
任务就会执行,并且在构建过程中触发ProGuard。
最好在打开ProGuard后重新检查一下整个应用,因为它可能剔除一些你仍然需要的代码。这是ProGuard令很多开发者苦恼的问题。为了解决这个问题,你可以定义ProGuard规则,将不需要移除或者混淆的类排除出去。proguardFiles
属性用来定义包含ProGuard规则的文件。比如,要保持一个类不变,你可以添加如下规则:
-keep public class
getDefaultProguardFile('proguard-android.txt')
方法从proguard-android.txt
文件获取ProGuard设置,该文件在Android SDK的tools/proguard
目录下。proguard-rules.pro
文件由Android Studio默认添加到模块中,所以你可以很简单的在该文件添加这个模块的规则。
ProGuard规则对于每个app或者library都是不同的,所以本书不会深入研究细节。如果你想了解更多相关内容,可以浏览http://developer.android.com/tools/help/proguard.html
除了压缩Java代码,也可以压缩不用的资源。
压缩资源
Gradle和Gradle Android插件可以在构建时去掉不用的资源。这在你有一些旧的忘掉删除的资源时非常有用。另一个场景是你引入了一个包含很多资源的库,但你只用了很少一部分。你可以打开资源压缩来处理这种情况。有两种方式可以进行资源压缩:自动和手动。
自动压缩
最简单的方式是在你的构建中配置shrinkResources
属性。如果设置为true,Android构建工具会自动检测哪些资源没有用到,不应该包含到APK中。
使用这个特性有一个需求,就是你也必须使用ProGuard。这源于资源压缩的工作方式,因为Android构建工具在引用资源的代码被移除之前无法确定哪些资源是没有用到的。
下面的代码片段展示了如何在一个特定的构建类型中配置自动压缩资源:
android {
buildTypes {
release {
minifyEnabled = true
shrinkResources = true
}
}
}
如果你想准确知道开启资源压缩后,APK减小了多少,你可以运行shrinkReleaseResources
任务。这个任务会打印出包减小的大小:
:app:shrinkReleaseResources
Removed unused resources: Binary resource data reduced from 433KB
to 354KB: Removed 18%
通过给构建命令添加--info
标识,你可以得到关于APK去除的资源的详细概况:
$ gradlew clean assembleRelease --info
使用这个标识后,Gradle会打印出构建过程的更多的信息,包括最后的输出不包含的资源。
自动资源压缩的一个问题是它可能移除过多的资源。尤其是被动态使用的资源可能被意外删除。为防止这种情况,你可以在res/raw/
目录放置keep.xml
文件来定义一些特殊情况。一个简单的keep.xml
文件如下:
keep.xml
本身会在最终的输出中被移除。
手动压缩
一个不那么极端的移除资源的方式是除去特定的语言文件或者特定密度的图像。某些库,比如Google Play Services,包含很多语言。如果你的应用只支持一种或者两种语言,就没必要在最终的APK中包含所有的语言文件。你可以使用resConfigs
属性来配置你想保留的资源,其余的将被移除。
如果你只想保留英语、丹麦语和荷兰语的字符串资源,可以如下配置:
android {
defaultConfig {
resConfigs "en", "da", "nl"
}
}
你也可以配置密度:
android {
defaultConfig {
resConfigs "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"
}
}
你还可以结合语言和密度。实际上,可以使用此属性限制所有类型的资源。
如果你在艰难地设置ProGuard,或者你只想去除应用不支持的语言和密度资源,使用resConfigs
是一个很好的压缩资源的方式。
加速构建
很多刚开始使用Gradle的Android开发者会抱怨它编译时间长。Gradle比Ant的构建时间要长,因为每次你执行任务,它都需要走完生命周期的三个阶段。这使它的整个过程是可配置的,但是也会变慢很多。幸运的是,有几种方式可以加快Gradle的构建。
Gradle属性
一个加速Gradle构建速度的方法是修改默认的配置。我们在第五章已经提及了并行构建执行,除此之外,你仍然可以调整一些其他的配置。
回顾一下,你可以通过设置项目根目录的gradle.properties
文件打开并行构建。只需要添加如下属性:
org.gradle.parallel=true
另一个简单的方式是打开Gradle守护进程,这会在你第一次构建的时候启动一个后台进程。任何串行构建都会重复利用这个后台进程,这就减少了启动成本。这个进程在你使用Gradle期间会一直保留,直到空闲3个小时后才会杀死。在你短时间内多次使用Gradle的情况下,使用守护进程是很有用的。可以如下打开守护进程:
org.gradle.daemon=true
在Android Studio中,Gradle守护进程是默认打开的。这意味着在IDE第一次构建之后,接下来的构建会稍微快一些。如果你通过命令行进行构建,Gradle守护进程是关闭的,除非你在属性中打开它。
为了加速编译,你可以修改JVM的参数。你可以使用jvmargs
这个Gradle属性来设置JVM内存分配池的值。两个对构建速度有直接影响的参数是Xms
和Xmx
。Xms
参数用来设置初始的内存值,Xmx
参数用来设置最大值。你可以在gradle.properties
文件中手动设置这两个值:
org.gradle.jvmargs=-Xms256m -Xmx1024m
你需要设置一个值和一个单位(k,m,g)。最大内存分配(Xmx)默认是256MB,起始内存分配(Xms)默认没有设置。可设置的值依赖你电脑的内存大小。
最后一个你可以配置的属性是org.gradle.configureondemand
。如果你的工程有几个模块,这会非常有用。因为它可以忽略当前任务不需要的模块,来尝试减少配置阶段的耗时。如果该属性设置为true
,Gradle在运行配置阶段之前,会尝试计算出哪些模块更改了配置,哪些没有。如果你的项目是单app或者library的项目,这个特性将不会有太大的作用。如果你有很多松散耦合的模块,这个特性可以节省你很多构建时间。
系统范围的Gradle属性
如果你想将这些属性应用到所有的基于Gradle的项目中,你可以在home目录的.gradle文件夹创建一个gradle.properties
文件。在home目录设置这些属性是很好的实践。因为通常你想降低在构建服务器上的内存消耗,构建时间相对来说不是那么重要。
Android Studio
Android Studio的设置中同样包含这些可以加速构建时间的配置。打开Settings,导航到Build,Execution,Deployment|Compiler。该页面,你可以找到并行构建、JVM选项,configure on demand等配置。这些配置只在基于Gradle的Android模块中才会出现。
性能分析
如果你想找到哪一部分减慢了构建速度,可以分析整个构建过程。你可以在执行Gradle任务的时候添加--profile
标识。有了这个标识,Gradle会生成一个性能报告,告诉你哪一部分耗时过长。知道了瓶颈,你就可以进行必要的优化。报告以HTML的形式保存在模块的build/reports/profile
目录。
性能报告展示任务执行过程中,每个阶段耗时的概况。Summary展示了Gradle在配置阶段为每一个模块消耗的时间。Dependency Resolution展示了每个模块解决依赖的时间。Task Execution展示了任务执行细节,包含每个单独任务的耗时,排列顺序从耗时高到耗时低排序。
Jack和Jill
如果你想使用实验工具,你可以打开Jack和Jill来加速构建时间。Jack(Java Android Compiler Kit)是一个新的Android构建工具链,可以直接将Java资源代码编译成dex格式。它有自己的.java
库格式,同时接管了打包和压缩。**Jill(Jack Intermediate Library Linker)是一个将.aar
和.jar
文件转换为.jack
库的工具。这些工具仍然是实验性质的,但它们可以用来加速构建时间,简化Android构建过程。虽然不建议在生产版本使用它们,但可以尝试一下。
为了使用Jack和Jill,你需要使用21.1.1或更高版本的build tools,1.0.0或更高版本的Gradle Android插件。在defaultConfig
块打开Jack和Jill:
android {
buildToolsRevision '22.0.1'
defaultConfig {
useJack = true
}
}
你也可以在特定的构建类型或product flavor中打开Jack和Jill。这种情况下,你可以继续使用常规构建工具链,并额外使用实验性质的构建:
android {
productFlavors {
regular {
useJack = false
}
experimental {
useJack = true
}
}
}
一旦设置了useJack=true
,精简和混淆将不再使用ProGuard,但你仍然可以使用ProGuard规则语法来指定特定的规则和例外,同样可以使用proguardFiles
方法。
忽略Lint
在使用Gradle执行release构建时,Lint检查将会执行。Lint是一个静态代码检查工具,会标识出你的布局和代码中潜在的问题。在某些情况下,甚至会阻塞构建过程。如果你的工程之前没有使用过Lint,并且你想迁移到Gradle中,Lint检查可能会有很多错误。为使构建可以进行,你可以通过关闭abortOnError
忽略Lint检查,阻止它中断构建。这只是一个临时解决方案,因为忽略Lint错误可能导致一些问题,比如丢失翻译,这可能引起应用崩溃。代码如下:
android {
lintOptions {
abortOnError false
}
}
临时关闭Lint检查可以很容将Ant构建迁移到Gradle中。另一种平滑过渡方式是在Gradle中执行Ant任务。
在Gradle中使用Ant
如果你已经花费了大量时间来设置Ant构建,那么切换到Gradle可能听起来会很恐怖。在这种情况下,Gradle不仅能够执行Ant任务,还能扩展它们。这意味着你可以用很少的步骤从Ant迁移到Gradle,而不是花费好几天来进行项目转换。
Gradle为Ant集成使用Groovy的AntBuilder。AntBuilder使你可以执行任何标准的Ant任务,你自定义的Ant任务和整个Ant构建。你还可以在Gradle构建配置中定义Ant属性。
在Gradle中运行Ant任务
Gradle可以直接运行Ant任务。你只需要为相应的Ant任务添加ant.
前缀就可以了。比如,创建一个归档:
task archive << {
ant.echo 'Ant is archiving...'
ant.zip(destfile: 'archive.zip') {
fileset(dir: 'zipme')
}
}
这个任务在Gradle中定义,使用了echo
和zip
这两个Ant任务。
你更应该首先考虑Gradle相同功能的任务。上例中,你可以定义相应的Gradle任务:
task gradleArchive(type:Zip) << {
from 'zipme/'
archiveName 'grarchive.zip'
}
相应的Gradle任务更加简洁和易于理解,也更加高效。
引入整个Ant脚本
如果你创建了一个Ant脚本来构建应用,你可以使用ant.importBuild
来引入整个构建配置。所有的Ant targets会自动转换为Gradle任务,你可以通过原始名称访问它们。比如下面这个Ant构建文件:
Hello, Ant
你可以这样引入到Gradle:
ant.importBuild 'build.xml'
hello
任务将会出现在Gradle构建中,你可以像常规Gradle任务一样执行它:
$ gradlew hello
:hello
[ant:echo] Hello, Ant
因为Ant任务被转换为了Gradle任务,所以你可以使用doFirst
和doLast
或者<<
来进行扩展。比如,你可以在控制台打印另一行:
hello << {
println 'Hello, Ant. It\'s me, Gradle'
}
执行结果为:
$ gradlew hello
:hello
[ant:echo] Hello, Ant
Hello, Ant. It's me, Gradle
你也可以依赖这些来自Ant的任务。比如:
task hi(dependsOn: hello) << {
println 'Hi!'
}
结果如下:
$ gradlew intro
:hello
[ant:echo] Hello, Ant
Hello, Ant. It's me, Gradle
:hi
Hi!
如果需要,你甚至可以创建依赖Gradle任务的Ant任务。你需要在build.xml
文件中为该任务添加depends
属性:
Hi
如果你有一个很大的Ant构建文件,并且你想保证任务名称不重复,你可以在引入Ant任务时进行重命名:
ant.importBuild('build.xml') { antTargetName ->
'ant-' + antTargetName
}
如果你决定重命名所有的Ant任务,你需要时刻谨记如果你有Ant任务依赖于一个Gradle任务,那么这个Gradle任务也需要添加前缀。否则,Gradle会找不到它并抛出UnknownTaskException
。
属性
Gradle和Ant不仅可以共享任务,你还可以在Gradle中定义属性,然后在Ant构建文件中使用它们。下面的Ant target打印一个version
的属性:
${version}
你可以在Gradle的构建配置中定义version属性,属性前添加ant.
前缀,就像任务一样。这是定义Ant属性最简短的方式:
ant.version = '1.0'
Groovy隐藏了很多实现细节。属性定义的全写如下:
ant.properties['version'] = '1.0'
运行version任务,输出如下:
$ gradlew appVersion
:appVersion
[ant:echo] 1.0
Gradle的深度Ant集成使你很容易的将基于Ant的构建移植到Gradle中。
高级应用部署
在第四章,我们讲解了几种方式来创建同一个应用的不同版本,即使用构建类型和product flavors。而某些情况下,使用一些特殊技巧将更加方便,比如APK分割。
分割APK
构建变体可以看做单独的实体,它有自己的代码、资源和清单文件。另一方面,APK分割只影响应用的打包。编译、压缩和混淆等仍是共享的。这个机制允许你基于密度或者application binary interface(ABI)来分割APK。
你可以使用android
块中的splits
块来配置分割。配置密度分割,可以在splits
块中创建density
块。同理abi
块用于设置ABI分割。
如果你打开密度分割,Gradle会为每个密度创建单独的APK。你可以手动去除不需要的密度,以加速构建过程。如下代码展示了如何打开密度分割,并去掉较低的密度:
android {
splits {
density {
enable true
exclude 'ldpi', 'mdpi'
compatibleScreens 'normal', 'large', 'xlarge'
}
}
}
如果你只支持几个密度,可以使用include
来定义白名单。使用include
,你首先需要reset()
方法来将密度白名单列表置空。
compatibleScreens
属性是可选的,它会在manifest文件中注入匹配的节点。上例中配置应用支持normal及以上的屏幕,不支持小屏。
基于ABI分割APK也是同样的方式,所有的属性和密度分割一致,除了compatibleScreens
。
执行配置了密度分割的构建,Gradle会创建一个universal APK和几个特定密度的APK。也就是说你会得到几个APK文件:
app-hdpi-release.apk
app-universal-release.apk
app-xhdpi-release.apk
app-xxhdpi-release.apk
app-xxxhdpi-release.apk
使用APK分割有一个警告。如果你想向Google Play推几个APK,你需要确保每个APK有不同的版本号。也就是说每个分割有一个单独的版本号。幸运的是,现在你可以使用applicationVariants
属性。
下面的片段来自Gradle Android插件的文档,展示了如何为每个APK生成不同的版本号:
ext.versionCodes = ['armeabi-v7a':1, mips:2, x86:3]
import com.android.build.OutputFile
android.applicationVariants.all { variant ->
// assign different version code for each output
variant.outputs.each { output ->
output.versionCodeOverride = project.ext.versionCodes.get(output.getFilter(OutputFile.ABI)) * 1000000 + android.defaultConfig.versionCode
}
}
这个小片段会检查每个构建变体使用的ABI,然后将一个乘数应用到版本代码,以确保每个变体都有一个独特的版本号。