本文會不定期更新,推薦watch下專案。如果喜歡請star,如果覺得有紕漏請提交issue,如果你有更好的點子可以提交pull request。本文意在分享作者在實踐中掌握的關於gradle的一些技巧。
本文固定連線:github.com/tianzhijiex…
本文有部分關於加速配置的內容在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 npm
url "$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 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預設會做嚴格檢查,遇到包錯誤會終止構建過程。你可以用如下開關關掉這個選項,不過最好是重視下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
}
}複製程式碼
引用本地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" // 這裡設定了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中就大量用到了這個技巧,下面是一個例子:
<!-- 騰訊的認證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/""複製程式碼
init.with
如果我們想要新增加一個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
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"複製程式碼
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 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種類
外掛化有可能會要根據環境更改當前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
方式加入的混淆具有以下特性:
- *.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) { // 如果用了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檔案分包也是利用了這個知識點)。
定義多個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 `<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