Gradle:你必須掌握的開發常見技巧~

語言: CN / TW / HK

 BATcoder技術 群,讓一部分人先進大廠

大家好,我是劉望舒,騰訊最具價值專家,著有三本業內知名暢銷書,三本書被中國國家圖書館、各大985名校圖書館收藏,連續五年蟬聯電子工業出版社年度優秀作者。

前華為面試官、獨角獸公司技術總監。

想要 加入  BATcoder技術群,公號回覆 BAT  即可。

作者:Peterp

https://juejin.cn/post/7053985196906905636

前言

本篇將分享神祕 Gradle 的常見開發技巧,主要包括:

  • Gradle配置

  • Config配置

  • Build配置

  • 依賴管理

  • 簡化BuildConfig配置

  • 管理全域性外掛的依賴

  • 動態調整元件開關

  • 自定義Gradle外掛

技巧1:Gradle配置

對於一個普通 model.gradle ,預設的配置如下:

如果我們每個 model 都這樣寫,步驟太複雜了,下面通過模板提取從而進行優化。

優化步驟

新建一個 gradle 檔案,命名為  xxx.gradle ,複製上述  model 裡的配置,放到你的專案中,可以自定義修改一些通用內容,在其他 model 中依賴即可,如下:

// 這就是剛才新建的預設gradle檔案,
// 注意:如果你的default.gradle是在專案目錄下,請使用../,如果僅在app下,請使用./
apply from: "../default.gradle"
import xxx.*

android {
   // 用於隔離不同model的資原始檔
    resourcePrefix "lc_play_"
}


dependencies {
    compileOnly project(path: ':common')
    api xxx
}

// 上述的 android{} , dependencies{}
// 其內部的內容都會在 `default.gradle` 的基礎上疊加,對於唯一的鍵值對,會進行替換。

技巧2:Config配置

在專案中,你是如何去寫你的版本號等其他預設配置呢?

  • 對於一個新專案,其預設的配置如下所示

  • 若每次新建立  model ,也需要定義其預設引數,如果每次都直接在這裡去改動,那麼如果版本變化,意味著我們需要修改多次,這並不是我們想看到的效果。

新建 「config.gradle」 ,內容如下:

// 一些配置檔案的儲存

// 使用git的commit記錄當做versionCode
static def gitVersionCode() {
    def cmd = 'git rev-list HEAD --count'
    return cmd.execute().text.trim().toInteger()
}

static def releaseBuildTime() {
    return new Date().format("yyyy.MM.dd", TimeZone.getTimeZone("UTC"))
}

ext {
    android = [compileSdkVersion: 30,
               applicationId    : "com.xxx.xxx",
               minSdkVersion    : 21,
               targetSdkVersion : 30,
               buildToolsVersion: "30.0.2",
               buildTime        : releaseBuildTime(),
               versionCode      : gitVersionCode(),
               versionName      : "1.x.x"]
}

使用時:

android {
    def android = rootProject.ext.android
    defaultConfig {
        multiDexEnabled true
        minSdk android.minSdkVersion
        compileSdk android.compileSdkVersion
        targetSdk android.targetSdkVersion
        versionCode android.versionCode
        versionName android.versionName
    }
 }

技巧3:Build配置

配置不同build型別

在開發中,我們一般會有多個環境,比如 「開發環境」「測試環境」「線上環境」

buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }

    dev{
        // initWith代表的是允許從其他build型別進行復制操作,然後配置我們想更改的設定
       // 這裡代表的是從release複製build配置
        initWith release
        // 清單佔位符
        manifestPlaceholders = [hostName:"com.petterp.testgradle.dev"]
        // 會在你原包名後面新增.test
        applicationIdSuffix ".dev"
    }
}

如上所述, dev 是我們新增的  「build型別」 ,當新增之後,我們就可以在命令列使用如下匹配的指令,或者點選As 最右側,gradle圖示,選擇app(根據自己build的配置位置而定,一般預設是app-model),選擇other,即可看到多瞭如下幾個指令:

<img src="https://tva1.sinaimg.cn/large/008i3skNly1gyepjrwhvpj312c0u076r.jpg" alt="image-20220114095904490" style="zoom: 33%;" />

當然你也可以選擇如下命令列執行,以便在 「Jenkins」 或者  「CI」 下  build 時執行:

gradlew buildDev
gradlew assembleDev
// 注:mac下是gradlew開頭,windows下可能是./gradlew

配置變體

對於開發中,我們一般都有多渠道的需求,一般而言,如果僅僅是多渠道我們可以選擇使用第三方 walle 去做,如果我們可能還有更精細的設定,比如針對這個 「build型別」 ,我們很可能對應了不同的預設配置等,比如配置不同的  applicationId ,資源。如下所示:

// 變體風味名,如果只設置一個,則所有變體會自動使用,如果存在兩個及以上,需要在變體中指定,並且變體需要與分組匹配。
// 風味名,類似於風格,分組的意思。
flavorDimensions "channel"
// flavorDimensions ("channel","api")
productFlavors {
    demo1 {
      // 每一個變體都必須存在一個風味,預設使用flavorDimensions(僅限其為單個時)的值,否則如果沒提供,則會報錯。
        dimension "channel"
        // appid字尾,會覆蓋了我們build型別中的applicationIdSuffix
        applicationIdSuffix ".demo"
        // 版本字尾
        versionNameSuffix "-demo"
    }
    demo2 {
        dimension "channel"
        applicationIdSuffix ".demo2"
        versionNameSuffix "-demo2"
    }
}

然後檢視我們的 「build Variants」 :

<img src="https://tva1.sinaimg.cn/large/008i3skNly1gyepjsdbhpj30p20cgjry.jpg" alt="image-20220115110605931" style="zoom:50%;" />

「Gradle」會根據我們的  變體 和  build型別 自動建立多個build變種,按照  變體名-build型別名 方式命名。

在配置變體時,我們也可以替換在 build型別 中設定的所有預設值,具體原因是,在新增  build型別 時,預設的  defaultConfig 配置其實是屬於  ProductFlavors 類,所以我們也可以在任意變體中替換所有預設值。

組合多個變體

在某些場景下,我們可能想將多個產品的變體組合在一起,比如我們想增加一個 「api30」 的變體,並且針對這個變體,我們想 「讓demo1和demo2與分別也能與其組合在一起」 ,即也就是當channel是demo1時api30下對應的包。如下所示,我們更改上面的配置:

  flavorDimensions("channel", "api")
    productFlavors {
        demo1 {
            dimension "channel"
            applicationIdSuffix ".demo"
            versionNameSuffix "-demo"
        }
        demo2 {
            dimension "channel"
            applicationIdSuffix ".demo2"
            versionNameSuffix "-demo2"
        }
        minApi23 {
            dimension "api"
            minSdk 23
            applicationIdSuffix ".minapi23"
            versionNameSuffix "-minapi23"
        }
    }

最終如下所示,左側是 gralde 生成的  「build變種」 ,右側對應其中  demo1MinApi23Debug 打包後的產物具體資訊:

「所以最終可總結為:最終我們在打包時,我們的包名和版本名會根據多個變體混合生成,具體如上圖所示,然後分別使用了兩者都具有的配置,當配置出現重複時,優先以開頭的變體配置作為基準。」

特別需要注意的是:如果我們給demo1變體也配置了最低sdk版本是21,那麼最終打出來的包minSdk也會是21,而不是minApi23中的minSdk配置

過濾變體

Gradle 會為我們配置的  「所有變體」 和  「build型別」 每一種可能組合都建立一個  build變種 。當然有些變種,我們並不需要,所以我們可以在相應模組的  build.gradle 中建立  「變體過濾器」 ,以便移除某些不需要的變體配置。

android{
 ...
 variantFilter { variant ->
        def names = variant.flavors*.name
        if (names.contains("demo2")) {
            setIgnore(true)
        }
    }
 ...
}

效果如下:

<img src="https://tva1.sinaimg.cn/large/008i3skNly1gyepjtb49xj31ho0j4tb7.jpg" alt="image-20220115120754881" style="zoom:50%;" />

針對變體配置依賴項

我們也可以針對上面這些變體,進行不同的依賴。比如:

 demo1Implementation  xxx
 minApi23Implementation xxxx

變體和build型別 該 如何選擇?

如你新增了一個變體 firDev ,那麼預設情況下就會有如下的  「build命令」 生成

firDevDebug
firDevRelase
firDevXXX(xxx是你自定義的build型別)

需要注意的是 debug 和  relase 是預設就會存在的,我們可以選擇覆蓋,否則就算移除,其也會選擇預設設定存在

即也就是最終 gradle 會幫我們每個變體都生成相應的  「build型別」 對應的命令,變體就相當於不同的渠道,而  「build型別」 就相當於針對這個渠道,存在著多種環境,比如  debug , relase ,你自定義的更多build型別。

  • 所以如果你的場景僅僅是想對應幾個不同環境,那麼直接配置  「build型別」 即可;

  • 如果你可能希望區分不同的包下的依賴項或者資源配置,那麼配置  「變體」 即可。

技巧4:依賴管理

對於一些環境下,我們並不想在線上依賴某些庫或者 「model」 ,如果是三方庫,一般都會有  「relase」 下依賴的版本。

如果是本地model,目前已經引用到了,所以就需要對於線上環境做null包處理,只留有相應的包名與入口,具體的實現都為null.

限制依賴條件為build型別

debugImplementation project(":dev")
releaseImplementation project(":dev_noop")

有一點需要注意,當我們使用預設的 debugImplementation 和  releaseImplementation 進行依賴時,最終打包時是否會依賴其中,取決於我們  「使用的build命令中build型別是不是debug或者relase」 ,如果使用的是自定義的  dev ,那麼上述的兩個 model 也都不會依賴,很好理解。

限制依賴條件為變體

相應的,如果我們希望當前的依賴的庫或者model 不受 「build型別」 限制,僅受  「變體」 限制,我們也可以使用我們的  變體-Implementation 進行依賴,如下所示:

demo1Implementation project(":dev")

這個意思是, 「如果我們打包時使用demo1相應的gradle命令,比如assembleDemo1Debug,那麼無論當前build型別是debug還是release或者其他,其都會參與依賴。」

排除傳遞的依賴項

開發中,我們經常會遇見依賴衝突,對於第三方庫導致的依賴衝突,比較好解決,我們只需要使用 exclude 解決即可,如下所示:

dependencies {
    implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") {
        exclude group: 'androidx.lifecycle', module: 'lifecycle-process'
    }
}

統一全域性的依賴版本

有時候,某些庫會存在好多個版本,雖然 Gradle 會預設選用最高的版本,但是依然不免有時候還是會提示報錯,此時我們就可以通過配置全域性統一的版本限制:

android{
 defaultConfig {
        configurations.all {
            resolutionStrategy {
                force AndroidX.Core
                force AndroidX.Ktx.Core
                force AndroidX.Work_Runtime
            }
        }
     }
}

技巧5:簡化BuildConfig配置

開發中,我們常見的都會將一些配置資訊,寫入到 BuildConfig 中,以便我們在開發中使用,這也是最常用的手段之一了。

配置方式1

最簡單的方式就是,我們可以在執行 「applicationVariants」 task任務時,將我們的  config 寫入配置中,示例如下:

「app/ build.gradle」

android.applicationVariants.all { variant ->
    if ("release" == variant.buildType.getName()) {
        variant.buildConfigField "String", "baseUrl", "\"xxx\""
    } else if ("preReleaseDebug" == variant.buildType.getName()) {
        variant.buildConfigField "String", "baseUrl", "\"xxx\""
    } else {
        variant.buildConfigField "String", "baseUrl", "\"xxx\""
    }
    variant.buildConfigField "String", "buglyAppId", "\"xx\""
    variant.buildConfigField "String", "xiaomiAppId", "\"xx\""
   ...
}

在寫入時,我們也可以通過判斷當前的 「build型別」 從而決定到底寫入哪些。

優化配置

如果配置很少的話,上述方式寫還可以接收,那如果配置引數很多,成百呢?此時就需要我們將其抽離出來了。

所以我們可以新建一個 build_config.gradle ,將上述程式碼複製到其中。

然後在需要的 「模組」 裡,依賴一下即可。

apply from: "build_config.gradle"

這樣做的好處就是,可以減少我們 app-build.gradle 裡的邏輯,通過增加統一的入口,來提高效率和可讀性。

配置方式2

當然也有另一種方式,相當於我們自己定義兩個方法,在 buildType 裡自行呼叫,相應的我們將  「config配置」 按照規則寫入一個檔案中去管理。

示例程式碼:

「app/ build.gradle」

buildTypes {
    // 讀取 ./build_extras 下的所有配置
    def configBuildExtras = { com.android.build.gradle.internal.dsl.BuildType type ->
        // This closure reads lines from "build_extras" file and feeds its content to BuildConfig
        // Nothing but a better way of storing magic numbers
        def buildExtras = new FileInputStream(file("./build_extras"))
        buildExtras.eachLine {
            def keyValue = it == null ? null : it.split(" -> ")
            if (keyValue != null && keyValue.length == 2) {
                type.buildConfigField("String", keyValue[0].toUpperCase(), "\"${keyValue[1]}\"")
            }
        }
    }
   release {
    ...
    configBuildExtras(delegate)
    ...
   }
   debug{
    ...
    configBuildExtras(delegate)
    ...
   }
 }

「build_extras」

...
baseUrl -> xxx
buglyId -> xxx
...

上述兩種配置方式,我們可以根據需要自行決定,我個人是比較喜歡方式1,畢竟看著更簡單,但其實兩者的實現方式也是大差不大,具體看個人習慣吧。

技巧6:管理全域性外掛的依賴

某些時候,我們所有的model,可能都需要整合一個外掛,此時我們就可以通過在 專案build.gradle 裡全域性統一管理,而避免到每一個 「Gradle」 下去整合:

// 管理全域性外掛的依賴
subprojects { subproject ->
    // 預設應用所有子專案中
    apply plugin: xxx
    // 如果想應用到某個子專案中,可以通過 subproject.name 來判斷應用在哪個子專案中
    // subproject.name 是你子專案的名字,示例如下
    // 官方文件地址:https://guides.gradle.org/creating-multi-project-builds/#add_documentation
//    if (subproject.name == "app") {
//        apply plugin: 'com.android.application'
//        apply plugin: 'kotlin-android'
//        apply plugin: 'kotlin-android-extensions'
//    }
}

技巧7:動態調整元件開關

對於一些元件,在 debug 開發時如果依賴,對我們的編譯時間可能會有影響,那麼此時,如果我們增加相應的開關控制,就會比較好:

buildscript {
 ext.enableBooster = flase
 ext.enableBugly = flase

 if (enableBooster)
    classpath "com.didiglobal.booster:booster-gradle-plugin:$booster_version"
 }

如果每次都是靜態控制,那麼當我們使用 CI 來打包時,就會沒法操作。所以相應的,我們可以更改一下邏輯:

我們建立一個資料夾,裡面放的是相應的忽略檔案,如下所示:

然後我們更改一下相應的 buildscript 邏輯:

buildscript {
 ext.enableBooster = !file("ignore/.boosterignore").exists()
 ext.enableBugly = !file("ignore/.buglyignore").exists()

 if (enableBooster)
    classpath "com.didiglobal.booster:booster-gradle-plugin:$booster_version"
 }

通過判斷相應的外掛對應的檔案是否存在,來決定外掛在CI打包時的啟用狀態。在CI打包時,我們只需要通過shell刪除相應的配置ignore檔案或者通過gradle執行相應命令即可。因為本篇是講gradle的一些操作,所以我們就主要演示一下gradle的命令示例。

技巧8:自定義Gradle外掛

我們先簡單寫一個最入門的外掛,用來移除相應的檔案,來達到開關外掛的目的。

task checkIgnore {
    println "-------checkIgnore--------開始->"
    removeIgnore("enableBugly", ".buglyignore")
    removeIgnore("enableGms", ".gmsignore")
    removeIgnore("enableByteTrack", ".bytedancetrackerignore")
    removeIgnore("enableSatrack", ".satrackerignore")
    removeIgnore("enableBooster", ".boosterignore")
    removeIgnore("enableHms", ".hmsignore")
    removeIgnore("enablePrivacy", ".privacyignore")
    println "-------checkIgnore--------結束->"
}

def removeIgnore(String name, ignoreName) {
    if (project.hasProperty(name)) {
        delete "../ignore/$ignoreName"
        def sdkName = name.replaceAll("enable", "")
        println "--------已開啟$sdkName" + "元件"
    }
}

這個外掛的作用很簡單,就是通過我們 Gradle 命令 「攜帶的引數」 來移除相應的外掛檔案。

gradlew app:assembleRoyalFinalDebug  -PenableBugly=true

具體如圖所示:在 「CI-build」 時,我們就可以通過傳遞相應的值,來動態決定是否啟用某外掛。

上述方式雖然方便,但是看著依然很麻煩,那麼有沒有更簡單,單純利用 Gradle 即可。其實如果稍微懂一點 Gradle 生命週期,這個問題就能輕鬆解決。

我們可以在 settings.gradle 裡監聽一下 Gradle 的  「生命週期」 ,然後在專案結構載入完成時,也就是  projectsLoaded 執行時,去判斷一下,如果存在某個引數,那麼就開啟相應的元件,否則關閉。示例如下:

// settings.gradle

gradle.projectsLoaded { proj ->
    println 'projectsLoaded()->專案結構載入完成(初始化階段結束)'
    def rootProject = proj.gradle.rootProject
    rootProject.ext.enableBugly = rootProject.findProperty("enableBugly") ?: false
    rootProject.ext.enableBooster = rootProject.findProperty("enableBooster") ?: false
    rootProject.ext.enableGms = rootProject.findProperty("enableGms") ?: false
    rootProject.ext.enableBytedance = rootProject.findProperty("enableBytedance") ?: false
    rootProject.ext.enableSadance = rootProject.findProperty("enableSadance") ?: false
    rootProject.ext.enableHms = rootProject.findProperty("enableHms") ?: false
    rootProject.ext.enablePrivacy = rootProject.findProperty("enablePrivacy") ?: false
}

執行build命令時攜帶相應引數即可:

gradlew assembleDebug -PenablePrivacy=true