Android使用Gradle命令動態傳參完成打包,不需要修改代碼


不得不說,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

android Gradle Task結構

  命令行里面可以直接使用gradle taskName(例如: gradle assemble360會將360市場的所有包都打出來,包括debug,release等,當然,這些還得你先在build.gradle里面配置好)。Android Studio 中也可以打開左下角的terminal中使用gradlew 執行命令,gradlew是gradle wrapper的簡寫,和gradle命令功能一樣。

android studio terminal

  好,開始切入正題,假如現在有這樣的需求:

  • 通常我們的應用都會有開發環境(也可以理解為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 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM