1. 程式人生 > 實用技巧 >Android 重構 | 持續優化統一管理 Gradle

Android 重構 | 持續優化統一管理 Gradle

LZ-Says



我介意著你的不介意。

前言

藉著韓哥哥要求重構的機會,正好好好回顧下以前遺忘/忽略的知識點。

記錄下有關 Gradle 優化之路:

  • Android|模組化探索抽取 basic 簡化子 module 冗餘

  • Android 重構 | 統一管理 Gradle 依賴版本

大概的方向或者說最終目標精簡後如下:

一次引用,全文(專案)使用,避免團隊協作引入重複依賴;

自帶依賴更新提示;

支援跳轉等常規操作。

最重要的,依然是便於維護。

從最初的建立 config.gradle 到現在的 basic_depend.gradle,雖說今天更比昨天強,但是依然不是很滿意。

ext 方式雖然是 Google 官方目前推薦,並且當前一些主流庫也採用此種方式,實際使用起來,個人還是有部分不方便。比如說不支援跳轉,不支援更新等等,人吶,總想得到更多。

在查閱了多個文件後,再次準備優化/升級一波,繼續讓韓總蒙圈。

一、buildSrc 搞起來

將官方的描述用 Google 翻譯了一遍,如下:

複雜的構建邏輯通常很適合作為自定義任務或二進位制外掛進行封裝。自定義任務和外掛實現不應存在於構建指令碼中。buildSrc 只要不需要在多個獨立專案之間共享程式碼,就可以非常方便地使用該程式碼。

該目錄 buildSrc 被視為包含的構建。發現目錄後,Gradle 會自動編譯並測試此程式碼,並將其放入構建指令碼的類路徑中。對於多專案構建,只能有一個 buildSrc 目錄,該目錄必須位於根專案目錄中。buildSrc 應該比指令碼外掛更可取,因為它更易於維護,重構和測試程式碼。

buildSrc 使用適用於 Java 和 Groovy 專案的相同原始碼約定。它還提供對 Gradle API 的直接訪問。

Google Develop

思索許久,個人簡單總結下:

  • buildSrc 存在於 Gradle 編譯期;

  • 同樣 buildSrc 支援(單獨專案)共享程式碼,例如一個專案中多個 module 都可以直接呼叫。

buildSrc 實踐

描述下操作步驟:

  • 在專案根目錄下建立 buildSrc 目錄,隨後新建 build.gradle.kts 檔案;

  • 建立 src 目錄,以及對應管理版本檔案;

  • 替換直接使用原有依賴

build.gradle.kts 內容如下:

// 匯入 Kotlin 外掛import org.gradle.kotlin.dsl.`kotlin-dsl`
plugins { `kotlin-dsl`}
repositories { jcenter()}/** * 禁用測試報告(Gradle 預設會自動建立測試報告) */tasks.withType<Test> { reports.html.isEnabled = false reports.junitXml.isEnabled = false}/** * isFork:將編譯器作為單獨的程序執行。 * 該過程在構建期間將被重用,因此分叉開銷很小。分叉的好處是,記憶體密集型編譯是在不同的過程中進行的,從而導致主 Gradle 守護程式中的垃圾回收量大大減少。 * 守護程式中較少的垃圾收集意味著 Gradle 的基礎架構可以執行得更快,尤其是在您還使用的情況下 --parallel。 * * isIncremental:增量編譯。Gradle 可以分析直至單個類級別的依賴關係,以便僅重新編譯受更改影響的類。自 Gradle 4.10 起,增量編譯是預設設定。 */tasks.withType<JavaCompile> { options.isFork = true options.isIncremental = true}/** * 禁用關於使用實驗性 Kotlin 編譯器功能的警告 */kotlinDslPluginOptions { experimentalWarning.set(false)}

Dependencies.kt,這是我定義的版本管理的檔案,部分內容如下:

@file:Suppress("SpellCheckingInspection")
/** * @author HLQ_Struggle * @date 2020/7/27 * @desc 統一管理類*/
// 統一管理專案中的版本資訊objectVersions{ // Build Config const val compileSDK = 29 // 編譯 SDK 版本 const val buildTools = "29.0.3" // Gradle 編譯專案工具版本 const val minSDK = 23 // 最低相容 Android 版本constvaltargetSDK=29//最高相容Android版本
// App Version const val appVersionCode = 1 // 當前版本編號constvalappVersionName="1.0"//當前版本資訊
// 。。。}
// 統一管理專案中使用的依賴庫objectDeps{
// GradleconstvalandroidGradle="com.android.tools.build:gradle:${Versions.androidGradlePlugin}"
// Kotlin const val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}" const val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}" const val kotlinxCoroutines ="org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.kotlinxCoroutines}" // ...}

舉個兩個栗子,如何使用:

  • 根目錄下 build 如何使用:

直接通過在 Dependencies 檔案中定義的分組名去獲取對應的屬性即可,如下所示:

buildscript {    // ...    dependencies {classpathDeps.androidGradle
classpathDeps.kotlinGradlePlugin
// NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files }}//...

  • 其它 module 目錄下 build 如何使用:

同理,當然也可以採用直接倒入整個對應分組方式,直接使用對應屬性,例如:

// 這裡採用直接倒入定義的 Deps 以及 Versions 分組方式import static Deps.*import static Versions.*
apply plugin: 'com.android.library'apply plugin: 'kotlin-android'apply plugin: 'kotlin-android-extensions'
android {
// 這裡就可以直接使用對應的屬性 compileSdkVersion compileSDK buildToolsVersion buildTools
defaultConfig { minSdkVersion minSDK targetSdkVersion targetSDK versionCode appVersionCode versionName appVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-rules.pro' } // ...}
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) // 同理,這裡也是一樣,直接使用對應的屬性名即可 implementation kotlinStdLib implementation appcompat implementation coreKtx api 'com.google.android.material:material:1.2.0' testImplementation junit androidTestImplementation extJunit androidTestImplementation espresso
api mmkv api 'com.airbnb.android:lottie:3.4.1'}

這種方式比較有好的幾個特點如下:

  • 支援跳轉;

  • 支援智慧提示;

  • Gradle 編譯時介入,感腳很溼高大上

但是關鍵的更新提示呢?

ummm,不開森。

加個 gif 配圖吧~

手動編寫 buildSrc 需要注意:

  • 目錄結構:例如:buildSrc/src/main/kotlin(java)

  • 在 build.gradle.kts 中新增 jcenter(),否則 kotlin-dsl 載入失敗

二、refreshVersions 使用(2020/09/15)

網上搜到關於 refreshVersions 的描述,覺得蠻合適,嘗試一波。

大概的優勢在於以下幾點:

  • 集中管理依賴

  • 以最小成本提示依賴升級

操作步驟如下:

Step 1:修改 settings.gradle 檔案

// settings.gradle.ktsimport de.fayard.refreshVersions.RefreshVersionsSetup
// Here you might have some pluginManagement block:pluginManagement { //...}
buildscript { repositories { gradlePluginPortal() } dependencies.classpath("de.fayard.refreshVersions:refreshVersions:0.9.5")}
rootProject.name = 'Your Android Project Name'include ':app'include ':helper'include ':weight'// include other module
RefreshVersionsSetup.bootstrap(settings)

Step 2:同步後執行命令

./gradlewmigrateToRefreshVersionsDependenciesConstants--console=plain

根據提示進行依賴替換:

隨後生成 versions.properties 檔案:

## suppress inspection "SpellCheckingInspection" for whole file## suppress inspection "UnusedProperty" for whole file#### Dependencies and Plugin versions with their available updates## Generated by $ ./gradlew refreshVersions## Please, don't put extra comments in that file yet, keeping them is not supported yet.
version.androidx.appcompat=1.1.0## # available=1.2.0-alpha01## # available=1.2.0-alpha02## # available=1.2.0-alpha03## # available=1.2.0-beta01## # available=1.2.0-rc01## # available=1.2.0-rc02## # available=1.2.0## # available=1.3.0-alpha01## # available=1.3.0-alpha02
##。。。

有一點覺得不舒服的地方是,它內建了 Android 一部分的依賴,而對於我們實際開發中使用其它依賴,則顯示不太友好了,如下圖:

研究好一段時間,各種蒙圈,實際的效果還是不是太滿意,如果能在 buildSrc 的基礎上新增版本更新就更好了。

三、buildSrc 結合 task(2020/09/17)

不得不說,掘金大佬很多,很友善,這不,沉璧浮光cbfg 大佬教我一招~

文末已附上鍊接,感興趣的小夥伴可以直接拉到底自行學習~

我簡單總結下大佬的實踐思路:

  • 新建 versions.gradle 用於存放依賴/外掛配置,在這裡支援依賴更新/提示;

  • 新建 updateDependencies.gradle task,用於將更新後的依賴/外掛同步 groovy;

  • 使用直接呼叫 groovy 即可。

Step 1:在專案根目錄下建立 buildSrc 目錄

Step 2:新建 version.gradle 依賴/外掛管理

大佬在日誌中以及寫的很明確了,這裡我單獨說下我期間遇到的坑,或者是重點吧,讓看到此文的小夥伴更快的上手。

  • version 之間是一些版本的配置 , 解析後會放到 Dependencies.kt 的 object Versions 中,必須存在,如下:

/*<version>*/<---必須存在// 對應的版本資訊def compileSDK = 29       // 編譯 SDK 版本def buildTools = "29.0.3" // Gradle 編譯專案工具版本def minSDK = 23           // 最低相容 Android 版本deftargetSDK=29//最高相容Android版本/*</version>*/<---必須存在

  • dep 之間是外掛/依賴庫引用路徑,解析後會放到 Dependencies.kt 的 object Deps 中,同樣必須存在,如下:

    /*<dep>*/<---必須存在
    // gradlePluginimplementation "com.android.tools.build:gradle:4.0.1"
    // permissionsDispatcher:Android 6.0 動態許可權管理implementation "org.permissionsdispatcher:permissionsdispatcher:4.7.0"
    /*</dep>*/<---必須存在

這裡需要注意下,// 後第一位代表你在使用中呼叫的名稱,:後代表對當前依賴的描述。

完整的 version.gradle 內容如下(篇幅有限,移除部分專案中使用依賴):

dependencies {    /* readme *     *     * 為了統一管理外掛/依賴庫版本,避免版本衝突,統一將外掛/依賴庫資訊配置在此檔案中,     * 通過gradlew updateDependencies task     * 解析此檔案生成對應內容到Dependencies.kt中進行統一引用     *     * <version> </version> 之間是一些版本的配置 , 解析後會放到Dependencies.kt的object Versions中     *     * <dep> </dep> 之間是外掛/依賴庫引用路徑 , 解析後會放到Dependencies.kt的object Deps中     *     * 配置外掛/依賴庫引用說明:     * 0、版本配置格式:def <name> = <value>     * 1、配置外掛/依賴庫引用路徑前備註格式://<外掛/依賴庫名> : <備註>,這個部分會被解析確定外掛/依賴庫引用名稱     * 2、配置外掛/依賴庫引用路徑時以 implementation 作為開頭     * 3、更新配置後執行 updateDependencies.gradle 的 updateDependencies task 同步更新到Dependencies.kt     *     *  Extra:     *  [Google's Maven Repository] (https://dl.google.com/dl/android/maven2/index.html)     */
/* * Version 部分 */
/*<version>*/
def compileSDK = 29 // 編譯 SDK 版本 def buildTools = "29.0.3" // Gradle 編譯專案工具版本 def minSDK = 23 // 最低相容 Android 版本 def targetSDK = 29 // 最高相容 Android 版本
/*</version>*/
/* * 外掛/依賴庫路徑部分 */
/*<dep>*/
// gradlePlugin implementation "com.android.tools.build:gradle:4.0.1"
/** * Third Lib */
// indicator:指示器 implementation "com.hlq-struggle:indicator:1.0.0"
// permissionsDispatcher:Android 6.0 動態許可權管理 implementation "org.permissionsdispatcher:permissionsdispatcher:4.7.0" // permissionsDispatcherProcessor implementation "org.permissionsdispatcher:permissionsdispatcher-processor:4.7.0"
// mmkv:基於 mmap 的高效能通用 key-value 元件 implementation "com.tencent:mmkv-static:1.1.0"
/*</dep>*/}

Step 3:新建(拷貝)大佬提供的 updateDependencies.gradle

相關日誌寫的很清楚了,大家仔細閱讀即可。

以下內容主要是將 version 中按照規則寫好的依賴/外掛進行同步 groovy 中。

/** * 將versions.gradle/xVersion.gradle中配置的版本資訊生成到src/main/groovy/Dependencies.groovy中 * 執行該task方法: * 方法1: * New: 在gradle task列表面板點選'Execute Gradle Task'(類似大象的)按鈕,在輸入框輸入'-p buildSrc updateDependencies'然後點回車鍵; * Deprecated: 在gradle task列表面板點選'Run Gradle Task'(類似大象的)按鈕,在'Gradle Project'欄選中buildSrc模組,在'Command line'欄輸入'updateDependencies'然後點選'OK'; * 方法2: * New: 在Terminal輸入'gradlew -p buildSrc updateDependencies'然後執行 * Deprecated: 在Terminal輸入'gradlew updateDependencies'然後執行 * 方法3: * AS->Edit Configurations->Gradle,點選左上角'+'新增配置: * Name: $projectName:buildSrc [updateDependencies] * Gradle project: $projectName/buildSrc * Tasks: updateDependencies * 點選'Apply'儲存此配置,後續在專案的 gradle task 列表中就可以找到此 task 雙擊執行了 */task("updateDependencies") {    String inputFilePath = "versions.gradle"    String outputFilePath = "src/main/groovy/Dependencies.groovy"
// 將inputFilePath宣告為該Task的inputs inputs.file(inputFilePath) // 將outputFilePath宣告為outputs outputs.file(outputFilePath)
doLast { File inputFile = file(inputFilePath) if (!inputFile.exists()) { return }
String inputTxt = inputFile.text StringBuilder builder = new StringBuilder()
/* * 解析拼接版本object */ builder.append("/**\n") .append(" * 版本資訊\n") .append(" */\n") .append("interface Versions {\n")
String startFlag = "/*<version>*/" String endFlag = "/*</version>*/"
int start = inputTxt.indexOf(startFlag) int end = inputTxt.indexOf(endFlag)
builder.append(" ") .append(inputTxt.substring(start + startFlag.length(), end).trim()) .append("\n}\n\n")
/* * 解析拼接依賴object */ builder.append("/**\n") .append(" * 依賴庫路徑\n") .append(" */\n") .append("interface Deps {\n")
startFlag = "/*<dep>*/" endFlag = "/*</dep>*/"
start = inputTxt.indexOf(startFlag) end = inputTxt.indexOf(endFlag)
String depsTxt = inputTxt.substring(start + startFlag.length(), end).trim()
int implementationIndex int doubleSlashIndex
while (true) { implementationIndex = depsTxt.indexOf("implementation") if (implementationIndex == -1) { break } doubleSlashIndex = depsTxt.lastIndexOf("//", implementationIndex) String namePart String name while (true) { namePart = depsTxt.substring(doubleSlashIndex + 2, implementationIndex) name = namePart.split(":")[0].trim() if (!name.contains("/")) { break } doubleSlashIndex = depsTxt.lastIndexOf("//", doubleSlashIndex - 2) } depsTxt = depsTxt.replaceFirst("implementation", String.format("def %s =", name)) }
builder.append(" ") .append(depsTxt) .append("\n}\n")
String resultTxt = builder.toString()
file(outputFilePath).withPrintWriter("utf-8", { writer -> writer.print(resultTxt) writer.flush() }) }}

Step 4:新建最後的build.gradle

apply plugin: 'groovy'applyfrom:'updateDependencies.gradle'

Step 5:執行同步命令

當然大佬提供了三種方案,挑選個人習慣的一種即可。

在Step 3中拷貝如下命令:

-p buildSrc updateDependencies

注意我畫紅線的地方,這是AS提供的一個類似歷史記錄的操作,很方便的記錄下我們上次使用的task,省的每次都輸入。

執行速度還是蠻快的,隨後變生成了我們的groovy檔案:

大概擷取此檔案內容,其實就是和我們的versions.gradle一樣,不信你看:

Step 6:如何使用?

到這裡其實就很easy了,簡單舉例。

Versions使用:

Deps使用:

如何更新以及同步?

圖片沒抓到,檢視原文吧~

感謝掘金大佬~

四、基於basic繼續封裝抽取build

基本完善之後,默默的趕緊還是有點不舒服的地方,例如:

現在的架構是一個app下對應其它module,而每個module都會有一些相同卻不相同的內容,如果後期調整,難倒我要一個個去修改嗎?豈不是又讓雞老大一通鄙視麼。

想想,再好好想想。

之前曾經做過一個basic抽取,同樣將共有引數/資訊提取到basic.gradle中,每個module apply,這樣就是減少不少程式碼量。

請看我封裝好的basic.gradle:

apply plugin: 'kotlin-android'apply plugin: 'kotlin-android-extensions'apply plugin: 'kotlin-kapt'
android {
// 指定用於編譯專案的 API 級別 compileSdkVersion Versions.compileSDK // 指定在生成專案時要使用的 SDK 工具的版本,Android Studio 3.0 後不需要手動配置。 buildToolsVersion Versions.buildTools
// 指定 Android 外掛適用於所有構建版本的版本屬性的預設值 defaultConfig { minSdkVersion Versions.minSDK targetSdkVersion Versions.targetSDK versionCode 1 versionName "1.0"
// 僅保留中文資源 resConfigs "zh" // 啟用多 dex 檔案 multiDexEnabled true
ndk { // 設定支援的SO庫架構 abiFilters "armeabi", "x86" }
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" }
// 配置 Java 編譯(編碼格式、編譯級別、生成位元組碼版本) compileOptions { encoding = 'utf-8' sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 }
kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() }
// 開啟檢視繫結 相容 Gradle 4.x 及以上版本 buildFeatures { dataBinding = true viewBinding = true // gradle 5.x + }
lintOptions { // lint 異常後繼續執行 abortOnError false }}
/** * implementation:不會向下傳遞,僅在當前 module 生效; api:向下傳遞,所依賴的 module 均可使用 */dependencies { implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation Deps.kotlinStdlibJdk7 implementation Deps.appcompat implementation Deps.coreKtx
testImplementation Deps.junit androidTestImplementation Deps.extJunit androidTestImplementation Deps.espressoCore}

我的app build.gradle:

apply plugin: 'com.android.application'
apply from: "../basic.gradle"
android {
// 指定 Android 外掛適用於所有構建版本的版本屬性的預設值 defaultConfig { applicationId "com.pwccn.fadvisor" }
// 封裝專案的所有構建型別配置 buildTypes { debug { // Log 控制器 - 輸出日誌 buildConfigField "boolean", "LOG_DEBUG", "true" // 對除錯 build 停用 Crashlytics ext.enableCrashlytics = false // 禁止自動生成 build ID ext.alwaysUpdateBuildId = false // 關閉資源縮減 shrinkResources false // 關閉程式碼縮減 minifyEnabled false // 關閉 zipAlign 優化 zipAlignEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } release { // Log 控制器 - 禁止輸出日誌 buildConfigField "boolean", "LOG_DEBUG", "false" // 啟用資源縮減 shrinkResources true // 啟動程式碼縮減 minifyEnabled true // 開啟 zipAlign 優化 zipAlignEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } }
}
/** * implementation:不會向下傳遞,僅在當前 module 生效;api:向下傳遞,所依賴的 module 均可使用 */dependencies { implementation Deps.legacySupportV4 implementation Deps.lifecycleExtensions implementation Deps.lifecycleViewModelKtx
// 模組化部分匯入部分
// helper implementation project(path: ':helper') // weight implementation project(path: ':weight')
// 常用三方依賴匯入部分 // 。。。
}

我的helper module:

apply plugin: 'com.android.library'apply from:"../basic.gradle"
dependencies { api Deps.constraintLayout api Deps.xPopup
// helper implementation project(path: ':helper')}

ummm,basic配合buildSrc,果然覺得巴適許多。

強烈推薦第三種以及第四種結合使用,那感覺,真的是feel倍兒爽~

參考資料

  • 配置專案全域性屬性

  • Use buildSrc to abstract imperative logic

  • refreshVersions

  • 掘金之路(一)統一管理外掛和依賴庫資訊->buildSrc

  • maven.google.com

  • BuildSrcDemo

  • 推薦:江蘇啟東屬於哪個市