Android 自定義構建型別 BuildType

揚州慢發表於2019-03-04

最近接觸到自定義構建型別 BuildType,發現這一塊有些地方稍不注意的話會被繞進去浪費點時間,既然我這邊已經花費時間了,如果正好你也需要接觸到 BuildType,也許接下來分享的 tips 可能會幫你節省些時間。

緣起

BuildType 相信許多開發者都不陌生,很常見的一種使用場景是線上、線下的後臺介面 BaseUrl 不同,許多人會選擇在 build.gradle 檔案的 buildTypes 中定義全域性變數來實現線上線下環境的定義(Gradle 2.x 版本),例如:

buildTypes {
    debug {
        buildConfigField "String", "BASE_URL", ""http://debug.api/""
    }
    release {
        buildConfigField "String", "BASE_URL", ""https://release.api/""        
    }
}
複製程式碼

在開發過程中,除了預設的 Debug 和 Release 版本,我們可能還需要為程式自定義一些東西。比如在上線 release 版本前,還需要一個預釋出版本,該版本除了後臺介面的 BaseUrl 與線上版本不同外,其他資源(包括資料庫環境)都與線上相同,該版本用來做釋出前的最後測試,最大程度避免線上環境出問題。如果每次打預發版本都去直接修改程式碼中的 BaseUrl 很明顯不是最優解。有一種解決方案是自定義 BuildType,在 app 模組下的 build.gradle 的 buildTypes 中自定義新的構建版本:

buildTypes {
    debug {
        buildConfigField "String", "BASE_URL", ""http://debug.api/""
    }

    release {
        buildConfigField "String", "BASE_URL", ""https://release.api/""        
    }

    pre.initWith(release) 
    pre {
        buildConfigField "String", "BASE_URL", ""https://pre.api/""  
    }
}

//java 類中呼叫 BuildConfig.BASE_URL 獲取定義的變數

複製程式碼

initWith() 是 BuildType 的一項配置項,我們還可以看到上文中提到的buildConfigField其實也是一項配置項。該配置可以理解成initWith(release) 可以理解成拷貝了 release 這一構建型別的所有變數,因為我們知道,每一個構建型別都有一些預設的變數,例如debuggablezipAlignEnabled等,使用該配置就免去為新增的構建型別定義所有的變數。
定義了新的構建型別後,gradle會自動生成新的task,使用gradle assemblePre即可打包新定義的預發包。這裡需要稍微注意的地方就是,必須在 app 模組下的 build.gradle 中定義新的構建型別,gradle 才會生成新的task。

Moduel 中自定義 BuildType 的問題

隨著現在模組化開發越來越流行,許多專案都會將一些業務無關的模組獨立出去,作為 Moduel 在專案中依賴使用,以此達到複用的效果。很常見的例如網路庫可能就會被獨立出來,那麼上文中的關於就會被定義在子 Module 中。這裡不注意的話就會浪費一些時間。假設你在 app 模組與子模組的 build.gradle 的 buildTypes 中都如上文定義了三種型別 debug
、release、pre 版本的BASE_URL請注意

如果你執行了gradle assemblePre,沒錯是構建了 pre 版本,但是列印出日誌你會發現:

app.BuildConfig.BASE_URL = "https://pre.api/"
module.BuildConfig.BASE_URL = "https://release.api/"
複製程式碼

子模組中如果沒有特別指定構建版本,無論你執行的是gradle assemblePre還是gradle assembleDebug,構建的都是 release 版本。可以使用defaultPublishConfig配置指定需要構建的版本,例如在子模組的 build.gradle 中指定:

android {
    ...
    //指定構建版本
    defaultPublishConfig "pre"
    ...
}
複製程式碼

當然最好設定個變數,否則如果有許多子模組,不可能修改構建版本時一個一個改過去,常用的是在專案最外層的 build.gradle 中設定變數供子模組呼叫:

buildscript {
    ...
    dependencies {
        classpath `com.android.tools.build:gradle:2.3.3`
    }
    ...
}
ext {
    projectBuildType = "debug"
}

//子模組中引用變數
defaultPublishConfig rootProject.ext.projectBuildType
複製程式碼

這裡需要注意,一旦子模組中指定了構建型別,例如 pre 版本,則該模組的 buildTypes 中必須也要有對應的構建型別 pre,否則編譯不通過。並且,一旦指定了構建型別,則該模組的構建型別就只會是指定的型別。舉個例子:子模組中指定了defaultPublishConfig "pre",執行gradle assembleRelease,列印日誌:

app.BuildConfig.BASE_URL = "https://release.api/"
module.BuildConfig.BASE_URL = "https://pre.api/"
複製程式碼

所以這裡就會比較煩人,每次打不同的包都需要去修改projectBuildType的值,還是需要手動修改。

分支名決定構建的版本型別

想要隔離手動修改,網上看到過一種解決方案:
子模組的 build.gradle 中設定:

android {
    publishNonDefault true
}
複製程式碼

然後在模組的依賴處,例如 app 模組的 build.gradle 中改進依賴的寫法:

releaseCompile project(path: `:module`, configuration: `release`)
debugCompile project(path: `:module`, configuration: `debug`)
複製程式碼

這樣就可以實現構建時子模組與 app 模組的型別一致。但這種寫法太煩了,如果你有十來個依賴,還得一個一個寫過去,又如果你還自定義了不止一種構建型別,且沒新增一個新的 buildType 都得修改所有的依賴,我認為也不是最優解。

這裡提供一個方案僅供參考。先介紹一下我們的開發流程,例如新開發1.0版本。首先從 master 切出一條 devel1.0 分支用於前期開發階段,開發完畢達到上線標準後,切出一條 pre1.0 預發分支,打預發包做最後測試並做最後的 bug 修復,最後測試通過,合併程式碼到 master 分支,打線上包釋出至應用商店。不同階段對應不同的分支,所以不同的構建版本可以通過分支名來決定,git 肯定可以獲取當前分支名,而 grale 中則可以執行 cmd 命令,二者結合即可達到想要的效果。有這個思路後實現起來就很簡單:

ext {
    projectBuildType = "debug"

    def gitBranchName = "git rev-parse --abbrev-ref HEAD".execute().text.trim()

    if(gitBranchName.contains("master")) {
        projectBuildType =  "release"
    } else if(gitBranchName.contains("pre")) {
        projectBuildType =  "pre"
    }
}
複製程式碼

Gradle 3.0.0 帶來的問題

Android Studio 3.0 + Gradle 3.0 相信許多人都躍躍欲試。升級到 Gradle 3.0 可能需要做一些改動,詳情可見Migrate to Android Plugin for Gradle 3.0.0
Gradle3.0 中自定義 BuildType 有需要注意的地方

Cause of build error

Your app includes a build type that a library dependency does not.

在 Gradle 2.x 時代,如果 app 中定義了 pre 型別,而子模組中沒有定義,是不會報錯的。但在 Gradle 3.0 下,如果你的 app 包含了新的自定義的 buildType,而依賴庫中卻沒有相應的自定義 buildType,則編譯階段就會報錯。

一種解決方案是在子模組裡也定義 app 中的所有 buildType,當然,專案裡依賴多的同學肯定要吐槽了:我懶!不想修改辣麼多東西!
Gradle 提供了比 2.x 時代更智慧的相容方案:matchingFallbacks

在 buildTypes 中定義 matchingFallbacks,可以在子模組沒找到當前構建型別時指定要載入哪個型別

//app 模組下的 build.gradle 中
buildTypes {
    debug {
        buildConfigField "String", "BASE_URL", ""http://debug.api/""
    }

    release {
        buildConfigField "String", "BASE_URL", ""https://release.api/""        
    }

    pre.initWith(release) 
    pre {
        buildConfigField "String", "BASE_URL", ""https://pre.api/""  
        matchingFallbacks = [`pre`, `debug`, `release`]
    }
}
複製程式碼

matchingFallbacks 可以定義多個構建型別,當執行gradle assemblePre 構建 Pre 版本時,而恰巧某個子模組又沒有定義 pre 版本,則會按照你指定的 matchingFallbacks 從前往後依次尋找,直到型別匹配。這種匹配方案比 Gradle 2.x 時代預設為你構建 release 版本要智慧的多,至少我們還可以根據變數來在構建不同型別時定義不同的匹配順序。並且 Gradle 3.x 中前文提到的defaultPublishConfig配置已經不再生效了,構建時子模組的構建型別與 app 構建型別保持一致(前提是子模組中也定義了該型別),變得更加靈活。

技術終歸是在向前發展的。關於自定義 BuildType 的一些使用小貼士就是這些了,至於更多的關於構建型別相關的知識,例如 buildTypes 結合 productFlavors,或是定義 sourceSets 屬性指定不同的程式碼目錄、資原始檔目錄等知識以後有機會再開一篇聊吧(又挖坑2333)。

囉嗦了一堆,權且算是拋磚引玉。吼啦,下篇部落格見~

相關文章