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阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • 在 Android Studio 构建的项目中,基于 Gradle 进行项目的构建,同时使用 Android DS...
    Ant_way阅读 7,241评论 0 16
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,459评论 2 59
  • https://www.jianshu.com/p/7c288a17cda8 总的来说,Android的系统体系结...
    燕京博士阅读 1,147评论 0 6
  • 如果让我用两个字形容英语课 一定是无聊 这节课英语老师又迟到了4分钟 这个年轻的女老师总是迟到 but她长得好看 ...
    也行趣阅读 145评论 0 0