Gradle:你必須掌握的開發常見技巧~
BATcoder技術 群,讓一部分人先進大廠
大家好,我是劉望舒,騰訊最具價值專家,著有三本業內知名暢銷書,三本書被中國國家圖書館、各大985名校圖書館收藏,連續五年蟬聯電子工業出版社年度優秀作者。
前華為面試官、獨角獸公司技術總監。
想要 加入 BATcoder技術群,公號回覆 BAT
即可。
作者:Peterp
http://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="http://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="http://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="http://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 是你子專案的名字,示例如下 // 官方文件地址:http://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
為了防止失聯,歡迎關注我的小號
微信改了推送機制,真愛請星標本公號 :point_down:
- 厲害了!自己寫個App 啟動任務框架
- 一個解決滑動衝突的新思路,做到檢視之間無縫地巢狀滑動!
- 谷歌官方改了兩次的知識點,你一定要知道!
- Android 最新架構詳解 | MVI = 響應式程式設計 單向資料流 唯一可信資料來源 !
- 說兩件事~
- 最新的動畫布局來了,一文帶你瞭解!
- Gradle:你必須掌握的開發常見技巧~
- Kotlin DSL 實戰:像 Compose 一樣寫程式碼!
- 厲害了,Android自定義樹狀圖控制元件來了!
- 一文帶你全面掌握Android元件化核心!
- 為什麼大廠開始全面轉向Compose?
- 谷歌限制俄羅斯使用Android系統,俄或將轉用 HarmonyOS!
- 鴻蒙OS、安卓、iOS測試對比,結果出乎意料!
- 最詳細的Android圖片壓縮攻略,讓你一次過足癮(建議收藏)
- Android字型漸變效果實戰!
- 攔截控制元件點選 - 巧用ASM處理防抖!
- Android正確的保活方案,拒絕陷入需求死迴圈!
- 再見 MMKV,自己擼一個FastKV,快的一批
- 白嫖一個Android專案的類圖生成工具!(建議收藏)
- 日常需求做的挺好,面試就被底層原理放倒