Gradle配置最佳實踐

本文會不按期更新,推薦watch下項目。若是喜歡請star,若是以爲有紕漏請提交issue,若是你有更好的點子能夠提交pull request。本文意在分享做者在實踐中掌握的關於gradle的一些技巧。javascript

本文固定鏈接:github.com/tianzhijiex…html

本文有部分關於加速配置的內容在Android打包提速實踐已經有所涉及,若是有想了解打包加速的內容,能夠移步去閱讀。java

需求

隨着android的發展,新技術和新概念層出不窮。不一樣的測試環境、不一樣的分發渠道、不一樣的依賴方式,再加上各大廠家「優秀」的插件化方案,這些給咱們的開發工做帶來了新的需求。我但願能夠經過gradle這個使人又愛又恨的東西來解決這些問題。react

實現

調整gradle的編譯參數

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.+' // 不推薦的寫法
}複製代碼

這樣的寫法能夠保證庫每次都是最新的,但也帶來了很多的問題

  • 每次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依賴。

  1. 把aar文件放在某目錄內,好比就放在某個module的libs目錄內
  2. 在這個module的build.gradle文件中添加:
    repositories {
     flatDir {
         dirs 'libs' //this way we can find the .aar file in libs folder
     }
    }複製代碼
  3. 以後在其餘項目中添加下面的代碼後就引用了該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_timebuild_hostbuild_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來作剔除是一個好方法。

  1. 剔除整個組織的庫
    compile ('com.facebook.fresco:animated-webp:0.13.0') {
     exclude group: 'com.android.support' // 僅僅寫組織名稱
    }複製代碼
  2. 剔除某個庫
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時跳過包含AndroidTestTest關鍵字的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視爲敵人和友人,去多多瞭解這個傢伙。

developer-kale@foxmail.com

參考自:

GRADLE構建最佳實踐
Gradle依賴統一管理
深刻理解Android(一):Gradle詳解
生成帶混淆配置的aar
Making Gradle builds faster
Gradle Plugin User Guide

相關文章
相關標籤/搜索