本文会不按期更新,推荐watch下项目。若是喜欢请star,若是以为有纰漏请提交issue,若是你有更好的点子能够提交pull request。本文意在分享做者在实践中掌握的关于gradle的一些技巧。javascript
本文固定链接:github.com/tianzhijiex…html
本文有部分关于加速配置的内容在Android打包提速实践已经有所涉及,若是有想了解打包加速的内容,能够移步去阅读。java
随着android的发展,新技术和新概念层出不穷。不一样的测试环境、不一样的分发渠道、不一样的依赖方式,再加上各大厂家“优秀”的插件化方案,这些给咱们的开发工做带来了新的需求。我但愿能够经过gradle这个使人又爱又恨的东西来解决这些问题。react
gradle.properties中容许咱们进行各类配置:android
配置大内存:git
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8复制代码
守护进程github
org.gradle.daemon=true复制代码
并行编译web
org.gradle.parallel=true复制代码
开启缓存:npm
android.enableBuildCache=true复制代码
开启孵化模式:缓存
org.gradle.configureondemand=true复制代码
以上的配置须要针对自身进行选择,随意配置大内存可能会出现oom。若是想了解这样配置的原理,请移步官方文档。
dependencies {
compile 'com.google.code.gson:gson:2.+' // 不推荐的写法
}复制代码
这样的写法能够保证库每次都是最新的,但也带来了很多的问题:
推荐写成固定的库版本:
dependencies {
compile 'com.google.code.gson:gson:2.2.1'
}复制代码
即便是jar包和aar,我也指望能够写一个固定的版本号,这样每次升级就能够经过git找到历史记录了,而不是简单的看jar包的hash是否变了。
allprojects {
repositories {
jcenter()
}
tasks.withType(JavaCompile){
options.encoding = "UTF-8"
}
}复制代码
在根目录的build.gradle中:
apply plugin: 'groovy'
allprojects {
// ...
}
dependencies {
compile localGroovy()
}复制代码
若是是在某个module中设置,那么就在其build.gradle中配置:
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
}复制代码
若是想要作全局配置,那么就在根目录的build.gradle中配置:
allprojects {
repositories {
jcenter()
}
tasks.withType(JavaCompile) {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
}
}复制代码
当咱们在使用Gradle Retrolambda Plugin的时候,就会用到上述的配置(将来迁jack的时候也或许会用到)。
密码和签名这类的敏感信息能够统一进行存放,不进行硬编码。在gradle.properies
中,咱们能够随意的定义key-value。
格式:
key value复制代码
例子:
STORE_FILE_PATH ../test_key.jks
STORE_PASSWORD test123
KEY_ALIAS kale
KEY_PASSWORD test123
PACKAGE_NAME_SUFFIX .test
TENCENT_AUTHID tencent123456复制代码
配置后,你就能够在build.gradle
中随意使用了。
signingConfigs {
release {
storeFile file(STORE_FILE_PATH)
storePassword STORE_PASSWORD
keyAlias KEY_ALIAS
keyPassword KEY_PASSWORD
}
}复制代码
上述仅仅是应对于密码等信息的存放,其实你能够将这种方式用于插件化(组件化)等场景。
facebook的react native由于更新速度很快,jcenter的仓库已经没法达到实时的程度了(估计是官方懒得提交),因此咱们须要作本地的库依赖。
先将库文件放入一个目录中:
接着配置maven的url为本地地址:
allprojects {
repositories {
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/module_name/libs/android"
}
}
}复制代码
路径都是能够随意指定的,关键在于$rootDir
这个参数。
maven仓库的配置很简单,关键在于url这个参数,下面是一个例子:
allprojects {
repositories {
maven {
url 'http://repo.xxxx.net/nexus/'
name 'maven name'
credentials {
username = 'username'
password = 'password'
}
}
}
}复制代码
其中name和credentials是可选项,视具体状况而定。若是你用jitpack的库的话就须要用到上面的知识点了。
allprojects {
repositories {
jcenter()
maven {
url "https://jitpack.io"
}
}
}复制代码
每次打包后都会有unaligned的apk文件,这个文件对开发来讲没什么意义,因此能够配置一个task来删除它。
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
// ...
}
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
// 删除unaligned apk
if (output.zipAlign != null) {
output.zipAlign.doLast {
output.zipAlign.inputFile.delete()
}
}
}
}复制代码
若是你但愿你库生成的aar文件都放在特定的目录,你能够采用下列配置:
android.libraryVariants.all { variant ->
variant.outputs.each { output ->
if (output.outputFile != null && output.outputFile.name.endsWith('.aar')) {
def name = "${rootDir}/demo/libs/library.aar"
output.outputFile = file(name)
}
}
}复制代码
apk等文件也能够进行相似的处理(这里再次出现了${rootDir}
关键字)。
lint默认会作严格检查,遇到包错误会终止构建过程。你能够用以下开关关掉这个选项,不过最好是重视下lint的输出,有问题及时修复掉。
android {
lintOptions {
disable 'InvalidPackage'
checkReleaseBuilds false
// Or, if you prefer, you can continue to check for errors in release builds,
// but continue the build even when errors are found:
abortOnError false
}
}复制代码
有时候咱们有部分代码须要多个app共用,在不方便上传仓库的时候,能够作一个本地的aar依赖。
repositories {
flatDir {
dirs 'libs' //this way we can find the .aar file in libs folder
}
}复制代码
dependencies {
compile(name:'aar的名字(不用加后缀)', ext:'aar')
}复制代码
若是你但愿把aar放在项目的根目录中,也能够参考上面的配置方案。在根目录的build.gradle
中写上:
allprojects {
repositories {
jcenter()
flatDir {
dirs 'libs'
}
}
}复制代码
工程能够依赖自身的module和jar文件,依赖方式以下:
dependencies {
compile project(':mylibraryModule')
compile files('libs/sdk-1.1.jar')
}复制代码
这种的写法十分经常使用,语法格式不太好记,但必定要掌握。
android {
defaultConfig {
applicationId "com" // 这里设置了com做为默认包名
}
buildTypes {
release {
applicationIdSuffix '.kale.gradle' // 设置release时的包名为com.kale.gradle
}
debug{
applicationIdSuffix '.kale.debug' // 设置debug时的包名为com.kale.debug
}
}复制代码
这对于flavor
也是同理:
android {
productFlavors {
dev {
applicationIdSuffix '.kale.dev'
}
}
}复制代码
这种写法只能改包名后缀,目前没办法彻底更改整个包名。
咱们在manifest中能够有相似{appName}
这样的占位符,在module的build.gradle
中能够将其进行赋值。
android{
defaultConfig{
manifestPlaceholders = [appName:"@string/app_name"]
}
}复制代码
flavors或buildType也是同理:
debug{
manifestPlaceholders = [
appName: "123456",
]
}复制代码
ShareLoginLib中就大量用到了这个技巧,下面是一个例子:
<!-- 腾讯的认证activity -->
<activity android:name="com.tencent.tauth.AuthActivity" android:launchMode="singleTask" android:noHistory="true" >
<intent-filter>
<!-- 这里须要换成:tencent+你的AppId -->
<data android:scheme="${tencentAuthId}" />
</intent-filter>
</activity>复制代码
我如今但愿在build时动态改变tencentAuthId
这个的值:
release {
minifyEnabled false
shrinkResources false // 是否去除无效的资源文件
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
applicationIdSuffix '.liulishuo.release'
manifestPlaceholders = [
// 这里的tencent123456是暂时测试用的appId
"tencentAuthId": "tencent123456",
]
}复制代码
先在project根目录下的build.gradle定义全局变量:
ext {
minSdkVersion = 16
targetSdkVersion = 24
}复制代码
而后在各module的build.gradle中能够经过rootProject.ext
来引用:
android {
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
}
}复制代码
这里添加rootProject
是由于这个变量定义在根目录中,若是是在当前文件中定义的话就不用加了(详见定义局部变量一节)。
假如想把当前的编译时间、编译的机器、最新的commit版本添加到apk中,利用gradle该如何实现呢?此需求中有时间这样的动态参数,不能经过静态的配置文件作,动态化方案以下:
android {
defaultConfig {
resValue "string", "build_time", buildTime()
resValue "string", "build_host", hostName()
resValue "string", "build_revision", revision()
}
}
def buildTime() {
return new Date().format("yyyy-MM-dd HH:mm:ss")
}
def hostName() {
return System.getProperty("user.name") + "@" + InetAddress.localHost.hostName
}
def revision() {
def code = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--short', 'HEAD'
standardOutput = code
}
return code.toString()
}复制代码
上述代码实现了动态添加了3个字符串资源: build_time
、build_host
、build_revision
, 在其余地方可像引用字符串同样使用:
getString(R.string.build_time) // 输出2015-11-07 17:01
getString(R.string.build_host) // 输出jay@deepin,这是个人电脑的用户名和PC名
getString(R.string.build_revision) // 输出3dd5823, 这是最后一次commit的sha值复制代码
上面讲到的是植入资源文件,咱们照样能够在BuildConfig.class
中增长本身的静态变量。
defaultConfig {
applicationId "kale.gradle.demo"
minSdkVersion 14
targetSdkVersion 20
buildConfigField("boolean", "IS_KALE_TEST", "true") // 定义一个bool变量
resValue "string", "build_time", "2016.11.17" // 上面讲到的植入资源文件
}复制代码
在sync后BuildConfig
中就有你定义的这个变量了。
public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "kale.gradle.test";
public static final String BUILD_TYPE = "debug";
public static final String FLAVOR = "";
public static final int VERSION_CODE = 1;
public static final String VERSION_NAME = "1.0.0";
// Fields from default config.
public static final boolean IS_KALE_TEST = true;
}复制代码
若是有带引号的string,要记得转义:
buildConfigField "String", "URL_ENDPOINT", "\"http://your.development.endpoint.com/\""复制代码
若是咱们想要新增长一个buildType,又想要新的buildType继承以前配置好的参数,init.with()
就很适合你了。
buildTypes {
release {
zipAlignEnabled true
minifyEnabled true
shrinkResources true // 是否去除无效的资源文件
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
signingConfig signingConfigs.release
}
rtm.initWith(buildTypes.release) // 继承release的配置
rtm {}
}复制代码
flavor能够定义不一样的产品场景,咱们在以前的文章中已经屡次讲到了这个属性,下面就是一个在dev的时候提高支持的android最低版本的作法。
productFlavors {
// 自定义flavor
dev {
minSdkVersion 21
}
}复制代码
flavor的一大优势是能够经过as来动态的改变这个值,不用硬编码:
若是你定义了不一样的flavor,能够在目录结构上针对不一样的flavor定义不一样的文件资源。
productFlavors{
dev {}
dev2 {}
qihu360{}
yingyongbao{}
}复制代码
有时候一个库会被引用屡次,或者一个库有多个依赖,但这些依赖的版本都是统一的。咱们经过ext来定义一些变量,这样在用到的时候就能够统一使用了。
ext {
leakcanaryVersion = '1.3.1'
scalpelVersion = "1.1.2" // other param
}复制代码
debugCompile "com.squareup.leakcanary:leakcanary-android:$leakcanaryVersion"
releaseCompile "com.squareup.leakcanary:leakcanary-android-no-op:$leakcanaryVersion"复制代码
咱们常常会遇到库冲突的问题,这个在多个部门协做的大公司会更常见到。将冲突的库经过exclude
来作剔除是一个好方法。
compile ('com.facebook.fresco:animated-webp:0.13.0') {
exclude group: 'com.android.support' // 仅仅写组织名称
}复制代码
compile('com.android.support:appcompat-v7:23.2.0') {
exclude group: 'com.android.support', module: 'support-annotations' // 写全称
exclude group: 'com.android.support', module: 'support-compat'
exclude group: 'com.android.support', module: 'support-v4'
exclude group: 'com.android.support', module: 'support-vector-drawable'
}复制代码
有时候一些库是一并依赖的,剔除也是要一并剔除的,咱们能够像下面这样进行统一引入:
compile([
'com.github.tianzhijiexian:logger:2e5da00f0f',
'com.jakewharton.timber:timber:4.1.2'
])复制代码
这样别的开发者就知道哪些库是有相关性的,在下掉库的时候也比较方便。
Gradle每次构建时都执行了许多的task,其中或许有一些task是咱们不须要的,能够把它们都屏蔽掉,方法以下:
tasks.whenTaskAdded { task ->
if (task.name.contains('AndroidTest') || task.name.contains('Test')) {
task.enabled = false
}
}复制代码
这样咱们就会在build时跳过包含AndroidTest
和Test
关键字的task了。
ps:有时候咱们本身也会写一些task或者引入一些gradle插件和task,经过这种方式能够简单的进行选择性的执行(下文会将如何写逻辑判断)。
咱们上面有提到动态得到字段的技巧,但有些东西是在打包发版的时候用,有些则是在调试时用,咱们须要区分不一样的场景,定义不一样的task。我下面以经过“用git的commit号作版本号”这个需求作例子。
def cmd = 'git rev-list HEAD --first-parent --count'
def gitVersion = cmd.execute().text.trim().toInteger()
android {
defaultConfig {
versionCode gitVersion
}
}复制代码
由于上面的操做可能比较慢,或者在debug时不必,因此咱们就作了以下判断:
def gitVersion() {
if (!System.getenv('CI_BUILD')) { // 不经过CI进行build的时候返回01
// don't care
return 1
}
def cmd = 'git rev-list HEAD --first-parent --count'
cmd.execute().text.trim().toInteger()
}
android {
defaultConfig {
versionCode gitVersion()
}
}复制代码
这里用到了System.getenv()
方法,你能够参考java中System
下的getenv()
来理解,就是获得当前的环境。
在根目录中创建一个config.gradle
文件:
ext {
android = [
compileSdkVersion: 23,
applicationId : "com.kale.gradle",
]
dependencies = [
"support-v4": "com.android.support:appcompat-v7:24.2.1",
]
}复制代码
而后在根目录的build.gradle
中引入apply from: "config.gradle"
,即:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply from: "config.gradle" // 引入该文件
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.2'
}
// ...
}复制代码
以后就能够在其他的gradle中读取变量了:
defaultConfig {
applicationId rootProject.ext.android.applicationId // 引用applicationId
minSdkVersion 14
targetSdkVersion 20
}
dependencies {
compile rootProject.ext.dependencide["support-v7"] // 引用dependencide
}复制代码
咱们除了能够经过buildtype来定义不一样的依赖外,咱们还能够经过写逻辑判断来作:
dependencies {
//根据是不一样情形进行判断
if (!needMultidex) {
provided fileTree(dir: 'libs', include: ['*.jar'])
} else {
compile 'com.android.support:multidex:1.0.0'
}
// ...
}复制代码
插件化有可能会要根据环境更改当前module是app仍是lib,gradle的出现让其成为了可能。
if (isDebug.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}复制代码
接下来只须要在gradle.properties
中写上:
isDebug = false复制代码
须要说明的是:根据公司和插件化技术的不一样,此方法因人而异。
有不少库是须要进行混淆配置的,但让使用者配置混淆文件的方式老是不太友好,consumerProguardFiles
的出现可让库做者在库中定义混淆参数,让混淆配置对使用者屏蔽。
ShareLoginLib中的例子:
apply plugin: 'com.android.library'
android {
compileSdkVersion 24
buildToolsVersion '24.0.2'
defaultConfig {
minSdkVersion 9
targetSdkVersion 24
consumerProguardFiles 'consumer-proguard-rules.pro' // 自定义混淆配置
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}复制代码
realm也用到了这样的配置:
打包工具会将*.pro文件打包进入aar中,库混淆时候会自动使用此混淆配置文件。
以consumerProguardFiles
方式加入的混淆具备如下特性:
若是你对于consumerProguardFiles有疑问,能够去ConsumerProGuardFilesTest这个项目了解更多。
android {
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
assets.srcDirs = ['assets']
if (!IS_USE_DATABINDING) { // 若是用了databinding
jniLibs.srcDirs = ['libs']
res.srcDirs = ['res', 'res-vm'] // 多加了databinding的资源目录
} else {
res.srcDirs = ['res']
}
}
test {
java.srcDirs = ['test']
}
androidTest {
java.srcDirs = ['androidTest']
}
}
}复制代码
经过上面的配置,咱们能够自定义java代码和res资源的目录,一个和多个都没有问题,更加灵活(layout文件分包也是利用了这个知识点)。
sourceSets {
main {
if (isDebug.toBoolean()) {
manifest.srcFile 'src/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/release/AndroidManifest.xml'
}
}
}复制代码
根据flavor也能够进行定义:
productFlavors {
hip {
manifest.srcFile 'hip/AndroidManifest.xml'
}
main {
manifest.srcFile '<where you put the other one>/AndroidManifest.xml'
}
}复制代码
Force
force强制设置某个模块的版本。
configurations.all {
resolutionStrategy {
force 'org.hamcrest:hamcrest-core:1.3'
}
}
dependencies {
androidTestCompile('com.android.support.test:runner:0.2')
androidTestCompile('com.android.support.test:rules:0.2')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.1')
}
能够看到,本来对hamcrest-core 1.1的依赖,所有变成了1.3。
Exclude能够设置不编译指定的模块
configurations {
all*.exclude group: 'org.hamcrest', module: 'hamcrest-core'
}
dependencies {
androidTestCompile('com.android.support.test:runner:0.2')
androidTestCompile('com.android.support.test:rules:0.2')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.1')
}
单独使用group或module参数
exclude后的参数有group和module,能够分别单独使用,会排除全部匹配项。例以下面的脚本匹配了全部的group为’com.android.support.test’的模块。
configurations {
all*.exclude group: 'com.android.support.test'
}
dependencies {
androidTestCompile('com.android.support.test:runner:0.2')
androidTestCompile('com.android.support.test:rules:0.2')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.1')
}
gradle的最佳实践是最好写也是至关难写的。好写之处在于都是些约定俗成的配置项,并且写法固定;难写之处在于很难系统性的解释和说明它在实际中的意义。由于它太灵活了,能够作的事情太多了,用法仍是交给开发者来扩展吧。
当年从eclipse切到android studio时,gradle没少给我添麻烦,也正是由于这些麻烦和不断的填坑积累,给我了上述的多个实践经验。
从写demo到正式项目,从正式项目作到开发库,从开发库作到组件化,这一步步的走来都少不了gradle这个魔鬼。今天我将我一年内学到的和真正使用过的东西分享在此,但愿你们除了获益之外,还能真的将gradle视为敌人和友人,去多多了解这个家伙。
参考自:
GRADLE构建最佳实践
Gradle依赖统一管理
深刻理解Android(一):Gradle详解
生成带混淆配置的aar
Making Gradle builds faster
Gradle Plugin User Guide