Android 自定義構建類型 BuildType

最近接觸到自定義構建類型 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 版本要智能的多,至少我們還可以根據變量來在構建不同類型時定義不同的匹配順序。並且我們也可以拋棄掉前文提到的defaultPublishConfig配置了,因爲 Gradle 3.x 中,只要 gradle 構建時,子模塊的構建類型變成了與 app 構建類型一致了(前提是子模塊中也定義了該類型),變得更加靈活。

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章