Gradle配置最佳實踐
本文會不定期更新,推薦watch下項目。如果喜歡請star,如果覺得有紕漏請提交issue,如果你有更好的點子可以提交pull request。本文意在分享作者在實踐中掌握的關于gradle的一些技巧。
本文固定連接:https://github.com/tianzhijiexian/Android-Best-Practices
本文有部分關于加速配置的內容在Android打包提速實踐已經有所涉及,如果有想了解打包加速的內容,可以移步去閱讀。
需求
隨著android的發展,新技術和新概念層出不窮。不同的測試環境、不同的分發渠道、不同的依賴方式,再加上各大廠家“優秀”的插件化方案,這些給我們的開發工作帶來了新的需求。我希望可以通過gradle這個令人又愛又恨的東西來解決這些問題。
實現
調整gradle的編譯參數
gradle.properties中允許我們進行各種配置:
配置大內存:
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
守護進程
org.gradle.daemon=true
并行編譯
org.gradle.parallel=true
開啟緩存:
android.enableBuildCache=true
開啟孵化模式:
org.gradle.configureondemand=true
以上的配置需要針對自身進行選擇,隨意配置大內存可能會出現oom。如果想了解這樣配置的原理,請移步官方文檔。
寫死庫的版本
dependencies {? ? compile'com.google.code.gson:gson:2.+'// 不推薦的寫法}
這樣的寫法可以保證庫每次都是最新的,但也帶來了不少的問題:
每次build時會向網絡進行檢查,國內訪問倉庫速度很慢
庫更新后可能會更改庫的內部邏輯和帶來bug,這樣就無法通過git的diff來規避此問題
每個開發者可能會得到不同的最新版本,帶來潛在的隱患
推薦寫成固定的庫版本:
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"}}
支持groovy
在根目錄的build.gradle中:
apply plugin:'groovy'allprojects {// ...}dependencies {? ? compile localGroovy()}
設置java版本
如果是在某個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 npmurl"$rootDir/module_name/libs/android"}? ? }}
路徑都是可以隨意指定的,關鍵在于$rootDir這個參數。
設置第三方maven倉庫
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
每次打包后都會有unaligned的apk文件,這個文件對開發來說沒什么意義,所以可以配置一個task來刪除它。
dependencies {? ? compile fileTree(include: ['*.jar'],dir:'libs')// ...}android.applicationVariants.all { variant ->? ? variant.outputs.each { output ->// 刪除unaligned apkif(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默認會做嚴格檢查,遇到包錯誤會終止構建過程。你可以用如下開關關掉這個選項,不過最好是重視下lint的輸出,有問題及時修復掉。
android {? ? lintOptions {? ? ? ? disable'InvalidPackage'checkReleaseBuildsfalse// Or, if you prefer, you can continue to check for errors in release builds,// but continue the build even when errors are found:abortOnErrorfalse}}
引用本地aar
有時候我們有部分代碼需要多個app共用,在不方便上傳倉庫的時候,可以做一個本地的aar依賴。
把aar文件放在某目錄內,比如就放在某個module的libs目錄內
在這個module的build.gradle文件中添加:
repositories { flatDir {? ? dirs'libs'//this way we can find the .aar file in libs folder}}
之后在其他項目中添加下面的代碼后就引用了該aar
dependencies { compile(name:'aar的名字(不用加后綴)',ext:'aar')}
如果你希望把aar放在項目的根目錄中,也可以參考上面的配置方案。在根目錄的build.gradle中寫上:
allprojects {? repositories {? ? ? jcenter()? ? ? flatDir {? ? ? ? dirs'libs'}? }}
依賴項目中的module和jar
工程可以依賴自身的module和jar文件,依賴方式如下:
dependencies {? ? compile project(':mylibraryModule')? ? compile files('libs/sdk-1.1.jar')}
這種的寫法十分常用,語法格式不太好記,但一定要掌握。
根據buildType設置包名
android {? ? defaultConfig {? ? ? ? applicationId"com"http:// 這里設置了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'}? ? }}
這種寫法只能改包名后綴,目前沒辦法完全更改整個包名。
替換AndroidManifest中的占位符
我們在manifest中可以有類似{appName}這樣的占位符,在module的build.gradle中可以將其進行賦值。
android{? ? defaultConfig{? ? ? ? manifestPlaceholders = [appName:"@string/app_name"]? ? }}
flavors或buildType也是同理:
debug{? ? manifestPlaceholders = [? ? ? ? appName:"123456",? ? ]}
ShareLoginLib中就大量用到了這個技巧,下面是一個例子:
我現在希望在build時動態改變tencentAuthId這個的值:
release {? ? minifyEnabledfalseshrinkResourcesfalse// 是否去除無效的資源文件proguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'signingConfig signingConfigs.release? ? applicationIdSuffix'.liulishuo.release'manifestPlaceholders = [// 這里的tencent123456是暫時測試用的appId"tencentAuthId":"tencent123456",? ? ]}
定義全局變量
先在project根目錄下的build.gradle定義全局變量:
ext {? ? minSdkVersion =16targetSdkVersion =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() {returnnewDate().format("yyyy-MM-dd HH:mm:ss")}def hostName() {returnSystem.getProperty("user.name") +"@"+ InetAddress.localHost.hostName}def revision() {? ? def code =newByteArrayOutputStream()? ? exec {? ? ? ? commandLine'git','rev-parse','--short','HEAD'standardOutput = code? ? }returncode.toString()}
上述代碼實現了動態添加了3個字符串資源:build_time、build_host、build_revision, 在其他地方可像引用字符串一樣使用:
getString(R.string.build_time)// 輸出2015-11-07 17:01getString(R.string.build_host)// 輸出jay@deepin,這是我的電腦的用戶名和PC名getString(R.string.build_revision)// 輸出3dd5823, 這是最后一次commit的sha值
上面講到的是植入資源文件,我們照樣可以在BuildConfig.class中增加自己的靜態變量。
defaultConfig {? ? applicationId"kale.gradle.demo"minSdkVersion14targetSdkVersion20buildConfigField("boolean","IS_KALE_TEST","true")// 定義一個bool變量resValue"string","build_time","2016.11.17"http:// 上面講到的植入資源文件}
在sync后BuildConfig中就有你定義的這個變量了。
public finalclassBuildConfig{? publicstaticfinal boolean DEBUG =Boolean.parseBoolean("true");? publicstaticfinalStringAPPLICATION_ID ="kale.gradle.test";? publicstaticfinalStringBUILD_TYPE ="debug";? publicstaticfinalStringFLAVOR ="";? publicstaticfinal int VERSION_CODE =1;? publicstaticfinalStringVERSION_NAME ="1.0.0";// Fields from default config.publicstaticfinal boolean IS_KALE_TEST =true;}
如果有帶引號的string,要記得轉義:
buildConfigField"String","URL_ENDPOINT","\"http://your.development.endpoint.com/\""
init.with
如果我們想要新增加一個buildType,又想要新的buildType繼承之前配置好的參數,init.with()就很適合你了。
buildTypes {? ? ? ? release {? ? ? ? ? ? zipAlignEnabledtrueminifyEnabledtrueshrinkResourcestrue// 是否去除無效的資源文件proguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.txt'signingConfig signingConfigs.release? ? ? ? }? ? ? ? rtm.initWith(buildTypes.release)// 繼承release的配置rtm {}? ? }
多個flavor
flavor可以定義不同的產品場景,我們在之前的文章中已經多次講到了這個屬性,下面就是一個在dev的時候提升支持的android最低版本的做法。
productFlavors {// 自定義flavordev {? ? ? ? minSdkVersion21}}
flavor的一大優點是可以通過as來動態的改變這個值,不用硬編碼:
如果你定義了不同的flavor,可以在目錄結構上針對不同的flavor定義不同的文件資源。
productFlavors{
dev {}
dev2 {}
qihu360{}
yingyongbao{}
}
定義局部變量
有時候一個庫會被引用多次,或者一個庫有多個依賴,但這些依賴的版本都是統一的。我們通過ext來定義一些變量,這樣在用到的時候就可以統一使用了。
ext {? ? leakcanaryVersion ='1.3.1'scalpelVersion ="1.1.2"http:// other param}
debugCompile"com.squareup.leakcanary:leakcanary-android:$leakcanaryVersion"releaseCompile"com.squareup.leakcanary:leakcanary-android-no-op:$leakcanaryVersion"
exlude關鍵字
我們經常會遇到庫沖突的問題,這個在多個部門協作的大公司會更常見到。將沖突的庫通過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'])
這樣別的開發者就知道哪些庫是有相關性的,在下掉庫的時候也比較方便。
剔除task
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
我們上面有提到動態獲得字段的技巧,但有些東西是在打包發版的時候用,有些則是在調試時用,我們需要區分不同的場景,定義不同的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 carereturn1}? 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.applyfrom:"config.gradle"http:// 引入該文件buildscript {? ? repositories {? ? ? ? jcenter()? ? }? ? dependencies {? ? ? ? classpath'com.android.tools.build:gradle:2.2.2'}// ...}
之后就可以在其余的gradle中讀取變量了:
defaultConfig {? ? applicationId rootProject.ext.android.applicationId// 引用applicationIdminSdkVersion14targetSdkVersion20}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種類
插件化有可能會要根據環境更改當前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 {? ? compileSdkVersion24buildToolsVersion'24.0.2'defaultConfig {? ? ? ? minSdkVersion9targetSdkVersion24consumerProguardFiles'consumer-proguard-rules.pro'// 自定義混淆配置}? ? buildTypes {? ? ? ? release {? ? ? ? ? ? minifyEnabledfalseproguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'}? ? }}
realm也用到了這樣的配置:
打包工具會將*.pro文件打包進入aar中,庫混淆時候會自動使用此混淆配置文件。
以consumerProguardFiles方式加入的混淆具有以下特性:
*.pro文件會包含在aar文件中
這些pro配置會在混淆的時候被使用
此配置針對此aar進行混淆配置
此配置只對庫文件有效,對應用程序無效
如果你對于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) {// 如果用了databindingjniLibs.srcDirs = ['libs']? ? ? ? ? ? ? ? res.srcDirs = ['res','res-vm']// 多加了databinding的資源目錄}else{? ? ? ? ? ? ? ? res.srcDirs = ['res']? ? ? ? ? ? }? ? ? ? }? ? ? ? test {? ? ? ? ? ? java.srcDirs = ['test']? ? ? ? }? ? ? ? androidTest {? ? ? ? ? ? java.srcDirs = ['androidTest']? ? ? ? }? ? }}
通過上面的配置,我們可以自定義java代碼和res資源的目錄,一個和多個都沒有問題,更加靈活(layout文件分包也是利用了這個知識點)。
定義多個Manifest
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'/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視為敵人和友人,去多多了解這個家伙