Android Studio使用Gradle構建示例

前言

最近遇到了問題,大概是 APPT2 ERROR 錯誤,這個錯誤很常見,說的是 .9圖片 有問題,但是網上的回答都非常的零散和不夠系統。編譯的時候從 LOG終端 中也看不了太多信息。網上的建議是加編譯參數 --stacktrace --debug ,所以就想著把 Gradle 構建系統詳細了解下。
以下說的大多是學習總結,一些概念可能描述的不準確,更多信息請參考文中和文末的資料鏈接。

問題

  1. Android Studio, Gradle, Groovy 的關系
  2. Android Studio 生成的項目中 build.gradle , settings.gradle , gradle.properties , gradle-wrapper.properties 這些文件的作用
  3. 如何構建不同渠道的多個包
  4. 如何構建服務器地址不同的包,甚至邏輯不同的版本,而不是新開分支

回答

Android Studio, Gradle, Groovy 的關系
  • Groovy 是基于JVM的一門語言,類似于其他基于JVM的語言一樣,如 JAVA , KOTLIN 等,最終生成JAVA字節碼運行在JVM虛擬機上。 我們從 Groovy官網 中下載安裝,并了解 語言特性

      println ("Groovy Demo")
      def num1 = 6
      num2 = 3
      
      ppp "你好","Groovy"
      println "$num1 的平方是 ${sqrt(num1)}"
      println "$num1 + $num2 = ${ add(num1,num2)}"
      println "平方求和 ${sqrtSum(num1, num2, {var1, var2->var1 + var2})}"
      
      
      def ppp(m1, m2) {
         println "$m1 $m2"
      }
      def sqrtSum(var1, var2, action) {
          return sqrt(action(var2, var2) )
      }
      def add(x, y) {
          x + y
      }
      def sqrt(x) {
          x * x
      }
      def mul(var1, var2) {
          return var1 - var2
      }
      def div = {
          int var1, int var2->
          var1 / var2
      }
    

    Groovy 了解個大概,有個概念差不多了。具體的可以參考官網。

  • Android Studio 編譯時就是把整個構造任務委托給 Gradle 來處理,可以構建Android項目還有 Ant , Buck 等。 Gradle 可以說是腳本,或者是語言,又或者是一個平臺。 Groovy 基于JVM,Gradle 基于 GroovyGradle 可以實現自動化打包,自動化測試,項目依賴管理等功能。 Gradle 基于 約定優先配置 原則,能夠非常智能的知道該如果加載庫,編譯源碼,加載資源,以及生成目標產物的最終位置。而配置的各種的插件,就是定義了一系列的規則。比如,java插件,android插件。

      apply plugin: 'com.android.application'
    

    Gradle 編譯的時候是執行一系列的task,可以使用下列命令查看所有的task

      gradle task --all
    

    常用的有 assemble , assembleDebug , assembleRelease , build , clean
    下面的代碼直觀感受下,如何編寫 build.gradle , 以及如何執行的。我們先新建文件夾 GradleDemo ,然后新建文件 build.gradle , 基本結構就好了。在 build.gradle 寫上下面的代碼:

      println "Gradle demo"
      
      
      task sayHello {
          println "hello from task 'sayHello'"
      }
    

    如果我們執行 gradle task --all 將會看到 sayHello 這個 task , 我們執行下

     gradle sayHello
    

    將會看到如下輸出

    Gradle demo
    hello from task 'sayHello'
    :sayHello UP-TO-DATE
    BUILD SUCCESSFUL
    Total time: 2.401 secs

    同理,我們執行 gradle -q assemble 的時候也就是執行 plugin 事先定義的復雜的 task assemble 進行構建。 顯然加載不同的插件, Gradle 可以構建 java , ANDROID , C/C++ 等不同的項目。

Android Studio 生成的項目中 build.gradle , settings.gradle , gradle.properties , gradle-wrapper.properties 這些文件的作用
  • 我們先看 gradle-wrapper.properties 文件, 位于 project/app/gradle/wrapper 中,這個是用于 gradle 版本管理的,我們的不同項目現在都可以方便的指定 gradle版本

    distributionBase=GRADLE_USER_HOME
    distributionPath=wrapper/dists
    zipStoreBase=GRADLE_USER_HOME
    zipStorePath=wrapper/dists
    distributionUrl=https://services.gradle.org/distributions/gradle-3.3-all.zip

    如果在指定的目錄中存在 3.3 的版本,就直接使用,如果不存在就去指定的服務器中下載。如果是下載的 gradle ,可以使用 gradle wrapper 命令來生成 wrapper ,它將會生成 .gradle , gradle , gradlew.bat , gradlew 等文件和文件夾。現在我們可以使用

      gradlew.bat --version
    

    查看下運行環境。它將顯示 gradlegroovy , jvm 等使用版本。

  • 我們接著看 gradle.properties 文件,顧名思義這個是 gradle 配置屬性值的地方。比如 jdk (org.gradle.java.home)位置,編譯參數(org.gradle.jvmargs)等。具體可以查看 官網說明 build_environment。另外一種使用情況是定義一下統一的配置變量,比如 compileSdkVersionbuildToolsVersionsupportLibrary 等變量。

    舉個例子:
    gradle.properties 中添加如下代碼:

      greetingWord=Hi, groovy
    

    build.gradle 中添加一個 task

      task sayHi {
          println rootProject.buildDir
          println "sayHi word from file $greetingWord"
      }
    

    它將會打印出 Hi, groovy ,這是我們定義的值。

  • 我們接著看 settings.gradle 文件,這個文件是針對多項目的配置文件,比如這個 app 項目引入來了其它庫,對于 gradle 來說,就是引入了 project , 這個文件使用 include 函數來說明了構建項目的 mul-project

  • 我們最后看app模塊中的 build.gradle 文件,這個文件是單獨對該 模塊/PROJECT 進行配置的。

      apply plugin: 'com.android.application' //加載android 應用插件
      
      android {   //dsl android 配置部分
          compileSdkVersion 25            //這兩行必須要有
          buildToolsVersion "26.0.2"      //這兩行必須要有
          defaultConfig {
              applicationId "com.bbg.gradledemo"
              minSdkVersion 19
              targetSdkVersion 25
              versionCode 1
              versionName "1.0"
              testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
          }
          buildTypes {
              release {
                  minifyEnabled false
                  proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
              }
          }
      }
      
      dependencies {  //依賴
          compile fileTree(dir: 'libs', include: ['*.jar'])
          androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
              exclude group: 'com.android.support', module: 'support-annotations'
          })
          compile 'com.android.support:appcompat-v7:25.3.1'
          compile 'com.android.support.constraint:constraint-layout:1.0.2'
          testCompile 'junit:junit:4.12'
      }
    

    里面的代碼塊是什么意思,更具體的可以參考 官網說明

如何構建不同渠道的多個包

這個問題和接下來的問題,將涉及到兩個主要概念,build typesproduct flavors ,這兩個概念弄明白了,我們的問題也就差不多解決了。 build types 是什么? build types 是站在程序員的角度來說的,比如 debug , release 版本,它并不為終端用戶所感知。而 product flavors 則可以理解為面向終端用戶的,比如 免費版付費版 ,又比如各大應用市場的不同版本。

我們看個復雜的實例,構建不同渠道的免費和收費的DEBUG、RELEASE兩種版本。這個例子分開來看的話,一共是3個階段。第一階段,構建 releasedebug 版本;第二階段,構建 channel1channel2 兩個不同渠道的版本;第三階段,構建 freepay 收費模式版本。組合起來的話,一共 2*2*2 最終會構建成 8 版本。

buildTypes 構建apk類型,即 debugrelease ; productFlavors 構建用戶感知的app變體,即不同的app; flavorDimensions 構建多維度的 flavor,可以理解為多維度的用戶使用情形。比如該例子說的是,既要區分不同渠道,又要對不同渠道做 freepay 兩種產品的區分。具體的寫法參考下面的 build.gradle 中的代碼。下面的代碼也展示如何在 AndroidManifest.xml 中插入我們配置的值,以及如何在 build.gradle 中配置運行時變量。 在項目代碼中還展示了如何讀取 meta-data 的值和在 build.gradle 中配置的值。

當我們 sync 我們的項目時,IDE中的 Build Variants 選項卡也會出現對應的各種APP變體。更官方的 Build Variants 可以參考這里,而 build.gradleblock代碼 塊中的屬性值或方法具體是什么意思或該怎么用,可以參考這里

下面是我們編譯出來的共 8 個APP變體。 8 種不同的 flavors

app-channe2-free-debug.apk
app-channe2-free-release.apk
app-channe2-pay-debug.apk
app-channe2-pay-release.apk
app-channel-free-debug.apk
app-channel-free-release.apk
app-channel-pay-debug.apk
app-channel-pay-release.apk

//默認配置
defaultConfig {

    //應用包名,注意和 AndroidManifest.xml 中的 packageName 區別,
    //后者影響資源的R類的生成; 前者是唯一包名
    applicationId "com.bbg.gradledemo"          
                        
    minSdkVersion 16
    targetSdkVersion 25
    versionCode 1
    versionName "1.0"
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    //manifest 占位符, map類型
    //buildConfigField 生成 BuildConfig 類的靜態值,可以代碼訪問
    //
    manifestPlaceholders = ["KEY_TYPE": "KEY-default"]      
    buildConfigField("String", "var1", "\"var-default\"")   
                                                            
}

//簽名配置
signingConfigs {
    config {
        keyAlias 'key0'
        keyPassword '123456'
        storeFile file("$rootProject.rootDir/keystore.jks")
        storePassword '123456'
    }
}

//build types
buildTypes {
    release {
        minifyEnabled true
        debuggable true
        shrinkResources true
        zipAlignEnabled true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        signingConfig signingConfigs.config
        applicationIdSuffix ".release"
    }
    debug {
        minifyEnabled true
        debuggable true
        shrinkResources true
        zipAlignEnabled true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        signingConfig signingConfigs.config
        applicationIdSuffix ".debug"
    }
}

//多維度flavor解決方法,前后順序影響生成的 variants 類型
//然后在product flavors 指定 deminsion 

flavorDimensions "channel", "mode"

productFlavors {
    channel {
        dimension "channel"
        manifestPlaceholders = ["KEY_TYPE": "KEY-channel"]
        buildConfigField("String", "var1", "\"var-channel\"")
    }
    channe2 {
        dimension "channel"
        manifestPlaceholders = ["KEY_TYPE": "KEY-channe2"]
        buildConfigField("String", "var1", "\"var-channe2\"")
    }

    free {
        dimension "mode"
        buildConfigField("String", "mode", "\"mode-free\"")
    }

    pay {
        dimension "mode"
        buildConfigField("String", "mode", "\"mode-pay\"")
    }
}
為不同的Flavor變體加入不同的資源和邏輯

從上面中的回答,我們可以知道,這種方式已經可以滿足我們提出的要求了。一些不同的 Build Variants , 可以根據變量值,在代碼中進行設置,進行不同的邏輯處理。 假如我們不同的 Variants 需要不一樣的啟動圖標,甚至邏輯部分不同,有沒有另外的解決方案。答案是有的,當我們在 build.gradle 中配置了 buildTypsproductFlavors 的時候,Android Studio 就邏輯關系將每個源代碼和資源分組為 源集

Android Studio 按邏輯關系將每個模塊的源代碼和資源分組為源集。模塊的 main/ 源集包括其所有構建變體共用的代碼和資源。其他源集目錄為可選項,在您配置新的構建變體時,Android Studio 不會自動為您創建這些目錄。不過,創建類似于 main/ 的源集有助于讓 Gradle 只應在構建特定應用版本時使用的文件和資源井然有序

如果不同的 源集 包含同一文件的不同版本,Gradle 將按以下優先順序決定使用哪一個:

構建變體 > 構建類型 > 產品風味 > 主源集 > 庫依賴項

我們實踐下,我們把上面的 channe2 變體的應用圖標修改,把第一次進入時候的是否 pay 的邏輯改成 free 。具體的代碼請參考Github。我們創建文件時,可以借助 IDE 來選擇不同的 源集。這樣我們的 app 目錄結構大概是這樣:

```
app
|
└─src
    ├─androidTest
    │  └─java
    │      └─com
    │          └─bbg
    │              └─gradledemo
    ├─channe2
    │  └─res
    │      ├─mipmap-xhdpi
    │      └─values
    ├─free
    │  ├─java
    │  │  └─com
    │  │      └─bbg
    │  │          └─gradledemo
    │  └─res
    │      └─values
    ├─main
    │  ├─java
    │  │  └─com
    │  │      └─bbg
    │  │          └─gradledemo
    │  └─res
    │      ├─drawable
    │      ├─layout
    │      ├─mipmap-hdpi
    │      ├─mipmap-mdpi
    │      ├─mipmap-xhdpi
    │      ├─mipmap-xxhdpi
    │      ├─mipmap-xxxhdpi
    │      └─values
    ├─pay
    │  ├─java
    │  │  └─gradledemo
    │  └─res
    │      └─values
    └─test
        └─java
            └─com
                └─bbg
                    └─gradledemo


```

我們在 chane2 變體中引入 ic_launcher,在 payfree 變體中處理是否收費邏輯。編譯的時候,可以把我們想要的都一起編譯出來,實現自動化,簡直完美。顯然, gradle 知道該如何合并。我們可以參考這里,了解是 gradle 是如何處理合并的。

結尾

到這里,我們順利的解決了提出的問題。發現 GRADLE 構建系統太出色了。這么好的工具,當然要用起來。期間查閱學習了較多資料,寫成此文,希望對讀者有幫助。文中有較多參考鏈接可以擴展查閱,最后末尾附上一些相關的參考補充資料。

如果文章對你有幫助,請點小心心或贊賞支持。歡迎留言交流。

Gradle 完整指南(Android)
深入理解Android(一):Gradle詳解
構建配置
Groovy File IO Document
gradle 編譯不過,Build可以問題解決方法
gradle實戰
Android SDK構建視頻
Android Gradle
Build Type, Flavour and Build Variant

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容

  • 在 Android Studio 構建的項目中,基于 Gradle 進行項目的構建,同時使用 Android DS...
    Ant_way閱讀 7,391評論 0 16
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,596評論 25 707
  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 12,765評論 2 59
  • http://www.lxweimin.com/p/7c288a17cda8 總的來說,Android的系統體系結...
    燕京博士閱讀 1,216評論 0 6
  • 如果讓我用兩個字形容英語課 一定是無聊 這節課英語老師又遲到了4分鐘 這個年輕的女老師總是遲到 but她長得好看 ...
    也行趣閱讀 150評論 0 0