不得不說,Gradle很強大,有人會問Gradle是什么?這里也不細講,在我認為他就是一個構建神器。Gradle 提供了:
- 一個像 Ant 一樣的非常靈活的通用構建工具
- 一種可切換的, 像 Maven 一樣的基於合約構建的框架
- 支持強大的多工程構建
- 支持強大的依賴管理(基於 ApacheIvy )
- 支持已有的 Maven 和 ivy 倉庫
- 支持傳遞性依賴管理, 而不需要遠程倉庫或者 pom.xml 或者 ivy 配置文件
- 基於 Groovy 的構建腳本
- 有豐富的領域模型來描述你的構建
build.gradle文件
首先先來說說這個文件,大家都知道Android 項目默認目錄下就有兩個build.gradle文件,其實也類似Maven中的pom.xml文件,一個是Project范圍的,另一個是Module范圍的,由於一個Project可以有多個Module,所以每個Module下都會對應一個build.gradle。
這兩個文件是有區別的,Project下的build.gradle是基於整個Project的配置,而Module下的build.gradle是每個模塊自己的配置。這里我主要講一下module下的build.gradle文件,也就是通常說的默認Module app下的。
講到這個配置,還需要引入Gradle中的Task概念。Gradle的Project從本質上說只是含有多個Task的容器,一個Task與Ant的Target相似,表示一個邏輯上的執行單元。我們可以通過很多種方式定義Task,所有的Task都存放在Project的TaskContainer中,Task是Gradle的第一公民。
Task可以自定義,但如果沒有什么太大需求,其實幾乎都用不到,因為Gradle會根據你的build.gradle自動創建Task,你只需要在配置文件里面配置一些需要的就可以了,在構建的時候會自動生成Task,用Android Studio的點擊同步也會自動生成。
可以打開Android Studio右側的Gradle面板,雙擊就可以執行Task
命令行里面可以直接使用gradle taskName(例如: gradle assemble360會將360市場的所有包都打出來,包括debug,release等,當然,這些還得你先在build.gradle里面配置好)。Android Studio 中也可以打開左下角的terminal中使用gradlew 執行命令,gradlew是gradle wrapper的簡寫,和gradle命令功能一樣。
好,開始切入正題,假如現在有這樣的需求:
- 通常我們的應用都會有開發環境(也可以理解為debug環境)、測試環境、預發環境、正式環境區分,我想要不改代碼就可以打出我想要環境的包。比如我現在分別想要一個測試環境的包和一個線上環境的包,但是我又不想改代碼
- 發布的時候發現版本號和版本名忘記改了
- 我想要隨時指定一個目錄,將打包好的文件放在這里面
- 我想要在打包時可以自定義安裝包的文件名
很簡單,如果你不想改代碼又想要得到不同環境的包,那當然是使用Gradle的命令,前面說過Gradle命令后面可以加上Task的name直接執行Task,那我們可以自己定義我們需要的Task,讓不同的Task去做我們想要做的事不就解決問題了嗎。
可是下面又說要動態指定版本號版本名文件名和文件輸出路徑,那怎么辦?
也不難,傳參,需要什么就傳入什么,這樣就解決了動態指定的問題了。
思路講到這里,我們來看看具體要怎么配置這個文件:
第一個問題:怎么去配置不同環境的Task?
原先網絡請求路徑可能很多人都會寫在代碼里面,如下圖所示
/** * 存放一些全局常量 */ public class Constants { //外網測試環境 public static final String BASEHTTP = "http://test.api.cn"; //線上地址 // public static final String BASEHTTP = "http://release.api.cn"; //預發環境 // public static final String BASEHTTP = "http://pre.api.cn"; //本地測試 // public static final String BASEHTTP = "http://dev.api.cn"; //登錄 public static final String LOGIN_URL = BASEHTTP + "/api/user/login"; }
在需要更換環境的時候就換一個BASEHTTP的值,這樣可以解決問題,但是每一次編譯打包都需要重新去改一下代碼。一兩個包還好,如果多了就會覺得很麻煩,不方便。
所以就想到了可不可以將這些信息都寫在build.gradle配置文件里面,這樣好像就可以跟Gradle有點掛鈎了
//正式環境 def API_RELEASE_HOST = "\"http://release.api.cn\"" //預發環境 def API_PRE_RELEASE_HOST = "\"http://pre.api.cn\"" //測試環境 def API_TEST_HOST = "\"http://test.api.cn\"" //開發環境 def API_DEV_HOST = "\"http://dev.api.cn\""
Gradle腳本是用Groovy語言來寫的,Groovy語言這里不細講,大家可以網上搜Groovy語法,資料還是蠻多的,使用Groovy可以感受到到以下兩個特點:
- Groovy繼承了Java的所有東西,就是你突然忘了Groovy的語法可以寫成Java代碼,也就是Groovy和Java混在一起也能執行。
- Groovy和Java一樣運行在JVM,源碼都是先編譯為class字節碼。
這里我用def定義了幾個常量,分別用來表示不同的環境的請求地址,然后在defaultConfig里面自定義了一個常量名,作為代碼與配置文件的橋梁,建立了連接。注意:這里的字符串需要在里面加入引號,用轉義符轉義,因為Groovy會直接把最外層引號內的值賦值給生成的自定義變量,如果不加,賦值后的String字符串就會沒有引號,導致編譯出錯。
defaultConfig { applicationId "com.test" minSdkVersion 15 targetSdkVersion 23 versionCode 5 versionName 1.1.0 buildConfigField("String", "API_HOST", "${API_DEV_HOST}") }
這里的buildConfigField就是自定義一個常量,第一個參數表示類型,第二參數表示常量名,第三個參數傳入的是值。
點擊同步后在代碼中就可以直接調用BuildConfig.API_HOST來使用了,因為當點擊同步后,Gradle就會在BuildConfig這個類中加入常量API_HOST
public final class BuildConfig { public static final boolean DEBUG = Boolean.parseBoolean("true"); public static final String APPLICATION_ID = "com.test"; public static final String BUILD_TYPE = "debug"; public static final String FLAVOR = "360"; public static final int VERSION_CODE = 6; public static final String VERSION_NAME = "1.1.1"; // Fields from default config. public static final String API_HOST = "http://test.api.cn"; }
可以看到BuildConfig這個類中的最后一行已經有了API_HOST這個常量了,還有一些其他的常量也是根據配置自動生成的,這里可以先不用管。
現在可以通過代碼請求到配置文件里面的配置了。
public static final String LOGIN_URL = BuildConfig.API_HOST + "/api/user/login";
接下來要做的就是怎么執行不同的task就會引用不同的配置。
build.gradle文件中有一個buildTypes,里面放的是你在build的時候需要選擇的類型,默認有一個debug,也可以自己自定義,我在這里加了四種類型,debug(開發)、beta(測試)、preRelease(預發)、release(正式發布)
buildTypes { /* 線上環境 */ release { // 不顯示Log buildConfigField "boolean", "LOG_DEBUG", "false" buildConfigField "String", "API_HOST", "${API_RELEASE_HOST}"//API Host minifyEnabled true //是否混淆 //是否設置zip對齊優化 zipAlignEnabled true // 移除無用的resource文件 shrinkResources true //簽名 signingConfig signingConfigs.release proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } /* 預發環境 */ preRelease { // 不顯示Log buildConfigField "boolean", "LOG_DEBUG", "false" buildConfigField "String", "API_HOST", "${API_PRE_RELEASE_HOST}"//API Host minifyEnabled true //是否混淆 //是否設置zip對齊優化 zipAlignEnabled true // 移除無用的resource文件 shrinkResources true //簽名 signingConfig signingConfigs.release proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } /* 本地開發環境 */ debug { minifyEnabled false } /* 測試環境 */ beta { // 顯示Log buildConfigField "boolean", "LOG_DEBUG", "true" buildConfigField "String", "API_HOST", "${API_TEST_HOST}"//API Host minifyEnabled true //是否混淆 //是否設置zip對齊優化 zipAlignEnabled true // 移除無用的resource文件 shrinkResources true //簽名 signingConfig signingConfigs.release proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } }
可以看到,在每個buildType里面都有相應的配置,你打哪個類型的包,就會去讀取哪個類型的配置,如果沒有,默認會去讀取defaultConfig里面的配置,defaultConfig里面相當於初始值,這樣就做到了不同環境有了不同的配置,同步一下,再看一下Android Studio右側的Gradle面板,可以發現多了一些Task,自動生成了一些Task,比如原先是assembleDebug,現在就多了assembleBeta、assemblePreRelease、assembleRelease,想要執行哪個環境就執行哪個任務就ok了。
但是很多第三方的外部包配置不止在build.gradle文件,還會在AndroidManifest.xml做一些正式環境和測試環境的區分,做一些不同的配置,這里的配置怎么處理?
第二個問題:怎么將AndroidManifest.xml里面的配置在build.gradle里面進行配置?
舉個例子,我這里拿talkingData的配置來說,需要在AndroidManifest.xml里面指定APP_ID
<!--TalkingData 配置--> <meta-data android:name="TD_APP_ID" android:value="7E5389EAD0C2324FB7B379701F6D2BA0" />
包括百度地圖、個推等其實很多第三方庫都需要配置這些,在AndroidManifest.xml里面可以直接引用build.gradle文件里面的配置,build.gradle里面怎么配置我們一會再講,先看看引用配置后代碼:
<!--TalkingData 配置--> <meta-data android:name="TD_APP_ID" android:value="${TALKING_DATA_APP_ID}" />
這里使用了引用了build.gradle里面的TALKING_DATA_APP_ID的值,我們再來看看build.gradle文件里面怎么配置。
def TEST_TALKING_DATA_APP_ID = "6E5389EAD0C2C2CFB7B379701F6D2BA8" defaultConfig { applicationId "com.test" minSdkVersion 15 targetSdkVersion 23 versionCode 5 versionName 1.1.0 buildConfigField("String", "API_HOST", "${API_DEV_HOST}") manifestPlaceholders = [ /* talkingData 測試環境 */ TALKING_DATA_APP_ID: "${TEST_TALKING_DATA_APP_ID}" ] }
我在defaultConfig里面指定了一個manifestPlaceholders屬性,也是gradle默認就提供的一個屬性,從形式可以看出是一個數組的形式,里面可以寫多個鍵值對,用逗號隔開,AndroidManifest.xml會從manifestPlaceholders數組里面去尋找匹配的鍵,找到了就會引用這個鍵所對應的值。
這樣問題就迎刃而解了,所有的AndroidManifest.xml里面的配置都可以寫在build.gradle里面統一處理了。
而上面說過,defaultConfig是默認的配置,不同的buildType可以指定不同的配置,所以在不同的buildType,也可以理解為不同的環境里面配置不同的manifestPlaceholders就可以了。代碼如下:
buildTypes { /* 線上環境 */ release { // 不顯示Log buildConfigField "boolean", "LOG_DEBUG", "false" buildConfigField "String", "API_HOST", "${API_RELEASE_HOST}"//API Host manifestPlaceholders = [ /* release 環境 */ GETUI_APP_ID : "${RELEASE_GETUI_APP_ID}", GETUI_APP_KEY : "${RELEASE_GETUI_APP_KEY}", GETUI_APP_SECRET : "${RELEASE_GETUI_APP_SECRET}", /* talkingData release 環境 */ TALKING_DATA_APP_ID: "${RELEASE_TALKING_DATA_APP_ID}", PACKAGE_NAME : defaultConfig.applicationId ] minifyEnabled true //是否混淆 //是否設置zip對齊優化 zipAlignEnabled true // 移除無用的resource文件 shrinkResources true //簽名 signingConfig signingConfigs.release proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } /* 預發環境 */ preRelease { // 不顯示Log buildConfigField "boolean", "LOG_DEBUG", "false" buildConfigField "String", "API_HOST", "${API_PRE_RELEASE_HOST}"//API Host manifestPlaceholders = [ /* release 環境 */ GETUI_APP_ID : "${RELEASE_GETUI_APP_ID}", GETUI_APP_KEY : "${RELEASE_GETUI_APP_KEY}", GETUI_APP_SECRET : "${RELEASE_GETUI_APP_SECRET}", /* talkingData release 環境 */ TALKING_DATA_APP_ID: "${RELEASE_TALKING_DATA_APP_ID}", PACKAGE_NAME : defaultConfig.applicationId ] minifyEnabled true //是否混淆 //是否設置zip對齊優化 zipAlignEnabled true // 移除無用的resource文件 shrinkResources true //簽名 signingConfig signingConfigs.release proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } /* 本地開發環境 */ debug { minifyEnabled false } /* 測試環境 */ beta { // 顯示Log buildConfigField "boolean", "LOG_DEBUG", "true" buildConfigField "String", "API_HOST", "${API_TEST_HOST}"//API Host manifestPlaceholders = [ /* 個推測試環境 */ GETUI_APP_ID : "${TEST_GETUI_APP_ID}", GETUI_APP_KEY : "${TEST_GETUI_APP_KEY}", GETUI_APP_SECRET : "${TEST_GETUI_APP_SECRET}", /* talkingData 測試環境 */ TALKING_DATA_APP_ID: "${TEST_TALKING_DATA_APP_ID}", PACKAGE_NAME : defaultConfig.applicationId ] minifyEnabled true //是否混淆 //是否設置zip對齊優化 zipAlignEnabled true // 移除無用的resource文件 shrinkResources true //簽名 signingConfig signingConfigs.release proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } }
我們已經可以執行不同的Task去打不同環境的包了。
命令行里面可以使用gradle assembleBeta(assembleBeta表示測試環境,其他環境可以替換后面的名字,如assemblePreRelease、assembleRelease等,如果配置了渠道,還會在assemble后面拼上渠道名,例如我的渠道名是360,我要打release的包,那就是assemble360Release)。
ide里面可以點擊菜單上的build,再點擊Generate Signed APK…,填完keystore密碼后選擇buildType進行打包操作。
提醒:在打包前最好先做一下clean操作,否則會出現有些代碼打包不進去,不知道其他人是不是這樣的。
第三個問題:怎么動態傳參滿足需求?
很簡單,直接上代碼:
defaultConfig { applicationId "com.ixwork" minSdkVersion 15 targetSdkVersion 23 //關鍵看這兩行 versionCode project.hasProperty('VERSION_CODE') ? Integer.parseInt(VERSION_CODE) : DEF_VERSION_CODE versionName project.hasProperty('VERSION_NAME') ? VERSION_NAME : "${DEF_VERSION_NAME}" buildConfigField("String", "API_HOST", "${API_DEV_HOST}") }
關鍵看versionCode 和versionName這兩行,原先默認是直接在后邊寫上版本號和版本名,這里用了三目運算符,可以用project.hasProperty(‘KEY’)來判斷是否有KEY這個參數傳入,如果有的話就就返回true,就會使用傳入的值作為實際值,這里用了強轉,將傳入的String類型轉為int類型的,如果沒有就會返回false,使用默認的值。
同理,傳入文件名和文件輸出路徑也一樣。
//修改生成的最終文件名 applicationVariants.all { variant -> variant.outputs.each { output -> def outputFile = output.outputFile if (outputFile != null && outputFile.name.endsWith('.apk')) { //判斷是否有這個OUT_PUT_DIR參數傳入 File outputDirectory = new File(project.hasProperty('OUT_PUT_DIR') ? OUT_PUT_DIR : outputFile.parent); def fileName if(!project.hasProperty('FILE_NAME')){ if (variant.buildType.name == "release" || variant.buildType.name == "preRelease") { // 輸出apk名稱為app_v1.0.0_2015-06-15_playStore.apk fileName = "app_v${defaultConfig.versionName}_${releaseTime()}_${variant.productFlavors[0].name}_${variant.buildType.name}.apk" } else if (variant.buildType.name == "beta") { fileName = "app_v${defaultConfig.versionName}_${releaseTime()}_${variant.buildType.name}.apk" } else { fileName = outputFile.name } }else{ fileName = FILE_NAME } // println("輸出apk ---> " + outputDirectory.absolutePath + File.separator + outputFile.name) output.outputFile = new File(outputDirectory, fileName) } } }
因為我這里配置了多個渠道,所以使用variant.outputs.each循環輸出文件,在里面分別處理每一個包。
在代碼里面分別用了project.hasProperty(‘OUT_PUT_DIR’)和project.hasProperty(‘FILE_NAME’)來判斷是否有這個參數,有無參數分別做了不同的處理。
fileName = “app_v{defaultConfig.versionName}_{defaultConfig.versionName}_{releaseTime()}_{variant.productFlavors[0].name}_{variant.productFlavors[0].name}_{variant.buildType.name}.apk”
這里對文件名進行了比較人性化的處理,加上了各種信息,通過包名就可以看出一些基本信息,如果不指定名字傳入,就會使用這個默認的名字。
萬事俱備,只欠東風了。到這里,基本所有都說完了,最后還有一個問題,哈哈,如何傳參?
第四個問題:如何傳參?
gradle clean assembleBeta -PVERSION_CODE=5 -PVERSION_NAME=1.1.1 -POUT_PUT_DIR=/home/user/Desktop -PFILE_NAME=test.apk
在命令行里面執行這個命令就可以打出所有的Beta包了(前提是已經安裝好Gradle,並配置好Gradle的環境變量,或者使用IDE里面的terminal,在項項目目錄下使用gradlew命令),其中assembleBeta 可以根據自己需求替換成其他的task名字。
傳參就是在后面加上 -P參數,-P后面再加上要傳入的鍵值對,中間用=號連接,需要什么參數就傳什么參數,如果有其他需要也可以自定義加入。
最后附上build.gradle源文件,可以點擊此處下載 build.gradle
另外還有一篇關於Android使用Gradle配合Jenkins自動構建打包的文章,有興趣的可以去看看。
http://blog.csdn.net/u014637428/article/details/52248589
以上是我的配置過程,如有問題歡迎留言,互相學習。
--------------------- 本文來自 githing 的CSDN 博客 ,全文地址請點擊:https://blog.csdn.net/u014637428/article/details/52249423?utm_source=copy