Gradle自动构建化基础

  Gradle可谓是众多学习安卓工作者的一块心病,每次项目基本上只用到引用第三方库和打包用到,其余就再不关心了,遇到bug也是一顿百度解决,但也只能治标不治本,今天就带大家一起走起Gradle,看看Gradle到底是个什么玩意,能够让我们如此头痛。
  因为Gradle的知识点相对来说是比较杂乱的,并且为了针对了解Gradle不同程度的读者来更好找到自己想要的部分,本文分以下俩部分展开

  • Gradle组成

  简单介绍Gradle一般组成作用和用法,初学者学习良药。

  • Gradle扩展

  Gradle自动构建化基础,充分使用Gradle的功能,Gradle提升必备。

  在介绍Gradle的作用之前,首先介绍一下Gradle是什么。

  Gradle是构建工具,基于Groovy语言,用来帮助我们构建app的,构建包括编译、打包等过程。

  • Gradle组成

  1.Project与Task
  在Gradle中,每一个待构建的工程是一个Project,构建一个Project需要执行一系列Task,比如编译、打包这些构建过程的子过程都对应着一个Task。具体来说,一个apk文件的构建包含以下Task:Java源码编译、资源文件编译、Lint检查、打包以生成最终的apk文件等等。
  这样说可能同学们不太直观,下面给张图

Project示意图

  我们时常切换这个选项,却没有把它和Gradle联系到一起,再看下我们的Task
Task示意图

  可以看到android,build,help等Task,大部分是系统给我们定制好的,当然我们也可以添加,有人说Task作用是什么呢,Task就是指一个个任务,包括Java源码编译、资源文件编译、Lint检查、打包以生成最终的apk文件等等,我们可以尝试点击图中的assemble,等一会就会发现会生成我们的apk,所以说我们打包apk的过程实际上就是执行一个个Task。

  2.Gradle配置文件
  每个项目和module都会配置一个Gradle,如下图


Gradle示意图

  一个负责Project,一个负责Module。

  • gradle.properties
      从它的名字可以看出,这个文件中定义了一系列“属性”,实际上,这个文件中定义了一系列供build.gradle使用的常量,比如keystore的存储路径、keyalias等等,如下:
    gradle.properties配置图

      填好之后,同步刷新一下Gradle(右上角Sync Now),然后在我们Module中就可以使用了,如下:
  signingConfigs {
        release {
            keyAlias KEY_ALIAS
            keyPassword KEY_PASSWORD
            storeFile rootProject.file(STORE_FILE)
            storePassword STORE_PASSWORD
        }
    }

  如果使用到了数字怎么办呢,我们在使用的时候可以这样

    defaultConfig {
        applicationId "cn.isimpo.distinguish"
        minSdkVersion 15
        targetSdkVersion 29
        versionCode Integer.parseInt(VERSION_CODE)
        versionName "1.0"
    }

  VERSION_CODE我们事先在gradle.properties中赋值为1或者其他数字即可。

  • gradlew与gradlew.bat
      gradlew为Linux下的shell脚本,gradlew.bat是Windows下的批处理文件。gradlew是gradle wrapper的缩写,也就是说它对gradle的命令进行了包装,比如我们进入到指定Module目录并执行“./gradlew assemble”即可完成对当前Module的构建,和我们直接点击上图展示的Task中的assemble效果一致。

  • local.properties
      local.properties用于定义本地sdk和ndk的路径,当然,也可以定义一些我们想要的字段,比如

# Location of the SDK. This is only used by Gradle.
# header note.
sdk.dir=/Users/simpo/kaifa/android/sdk

KEY_ALIAS=key_alias
KEY_PASSWORD=key_password
STORE_FILE=key/dianqu.keystore
STORE_PASSWORD=store_password

  然后看一下在Module中的使用

    signingConfigs {
        //加载资源
        Properties properties = new Properties()
        InputStream inputStream = project.rootProject.file('local.properties').newDataInputStream() ;
        properties.load( inputStream )

        //读取字段
        def key_keyAlias = properties.getProperty( 'KEY_ALIAS' )
        def key_keyPassword = properties.getProperty( 'KEY_PASSWORD' )
        def key_storePassword = properties.getProperty( 'STORE_PASSWORD' )
        //读取文件
        def sdkDir = properties.getProperty('STORE_FILE')
        release {
            keyAlias key_keyAlias
            keyPassword key_keyPassword
            storeFile rootProject.file(sdkDir)
            storePassword key_storePassword
        }
    }

  相对于直接把变量放gradle.properties里,这个操作还是复杂了许多。

  • settings.gradle

  这个我们应该算是比较熟悉的,它的作用就是需要编译哪些Module,正常我们的项目只有一个Module,所以长这样

include ':app'

  如果我们添加了很多Module,并且需要编译的时候,就可以再后面接着添加,比如我们新增的Module名称为other,可以更改为:

include ':app', ':other'

  如果一个Module中代码量过多,或者有过多的Module参与编译,会导致编译时间增常,往往会耽误很多时间,这就让我们学会如何代码模块化,高聚合,低耦合,从而每次编译只需要编译必须的模块,避免徒增多余的编译时间。

  • build.gradle
      正常情况下,我们的项目中只有俩个这样的文件,一个在项目目录下,一个在Module目录下。
    先看project下的build.gradle
buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:*'
    }
}
allprojects {
    repositories {
        google()
        jcenter()
    }
}
task clean(type: Delete) {
    delete rootProject.buildDir
}

1.buildscript:配置存储库和Gradle本身的依赖,一般此处无需我们修改。
  repositories:配置Gradle的存储库,一般有jcenter(),maven();
  dependencies:配置了Gradle需要使用的依赖;
  classpath:buildscript所需的插件和依赖;
2.allprojects:配置存储库和项目中所有模块(如第三方插件)使用的依赖项或库;
3.task:执行的工作单元,此处是项目clean的时候删除buildDir文件夹。
  再看app这个Module下的build.gralde。

apply plugin: 'com.android.application'    
android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.example.show"
        minSdkVersion 19
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
}

  1.apply:标明此module的类型。

apply plugin: 'com.android.application' // app应用程序
apply plugin: 'com.android.library' // 库
apply from: 'config.gradle’'//用于给本地文件系统提供路径或到远程位置的URL的脚本插件。

  再介绍一下其余参数的含义

   android           android的配置
   compileSdkVersion 编译应用使用的sdk版本
   apllicationId     包名
   minSdkVersion     支持的最小SDK版本,即在小于此版本的系统上不可使用
   targertSdkVersion 目标版本,应用已兼容从minSdkVersion至tartgetSdkVersion之间所有api的变化
   versionCode       版本号;只能为整型
   versionName       版本名;字符串类型
   testInstrumentationRunner 测试框架
   buildType         指定生成安装文件的相关配置
   release           正式版本配置
   minifyEnabled     设置是否开启混淆模式,如果开启,则使用         
   proguardFiles     属性指定的混淆文件
   proguardFiles     添加混淆规则的文件,用于提高反编译阅读难度和降低apk大小
   dependencies      依赖的包、模块等;项目需要使用的库、包都要在此处进行关联
   implementation    远程依赖的库
   testImplementation 测试依赖的库
   androidTestImplementation:测试用例库
   dependencies      用于引用三方库

  好了,到这里Gradle的组成就算结束了。

  • Gradle扩展

  当我们熟悉了Gradle的一些基本功能配置之后,我们就可以尝试用Gradle做一些更加智能的事情,首先我们要知道Gradle和我们的程序是一样的,是由上往下执行的,所以被引用的对象一定要写在引用对象的前面,比如buildTypes要引用signingConfigs中的对象,buildTypes必须要写在signingConfigs的下面,好,下面看看我们的Gradle还有哪些有趣的东西。

  • def
      def可以用来声明我们的对象,我们使用的时候写在Module中任何一个地方都可以,看看有哪些用法。
//声明数字
def version = 1
//声明纯字符串
def s1 = 'example'
//声明的字符串中可携带参数,比如引用上面的version,形式为 ${}
def s2 = "gradle version is ${version}"
//可换行的字符串
def s3 = '''my name
            is imooc'''

  还可以创造数组

声明集合对象
def list = new ArrayList()
或
def list = [1, 2, 3, 4, 5]
//添加集合对象
list.add(1)
//移除集合对象
list.remove(1)

  事实上我们def定义后,刷新Gradle,再使用list的时候,可调用的方法都会提示出来,有需要的可以自己选择,如下图


list

  基本和在java中的集合对象没有区别,这是不是更让大家有一种对Gradle的亲近的感觉,其实和我们的java代码没啥子区别嘛。
  还可以创造集合

def number = [one: '1',two: '2']

  上面展示了2个key和2个value,看下怎么调用

colors[one]
或
colors.one

  更多关于def定义集合和Map的操作可以参考:Gradle集合操作
  看下def还可以做什么

//无参闭包
def o = {
    println "hello world"
}
//多参闭包
def count = {
    x,y->
    println "hello world ${x} + ${y}"
}
//定义方法,包含闭包类型的参数
def methodone(Closure closure){
}
//定义方法,包含字符串类型的参数
def methodtwo(String type){
}

  Gradle里面的闭包我们可以理解为一个代码块,或理解成一个匿名函数,但是可以传参进去,我们可以把闭包当成参数传给方法,比如上面的闭包和方法可以组合,如下

def count = {
    x,y->
    println "hello world ${x} + ${y}"
}
//定义方法,包含闭包类型的参数
def methodone(Closure closure){
    closure("我是X", "我是Y")
}

methodone(count)

  如果count中没有定义参数,会有一个默认值it代表当前的count,闭包对我们来说算是比较新奇的一种用法了,当然我们的def定义的方法也是可以有返回值的并且被其他变量使用,同学们可以自行尝试。
  想知道更多闭包的操作,请移步:更多闭包操作
  需要注意def的声明中不能引用def的变量,需要知道详情的移步:
  def中不能引用def

  • ext
    ext和def是很像的,但是这里说下区别

  ext属性可以伴随对应的ExtensionAware对象在构建的过程中被其他对象访问,例如你在rootProject中声明的ext中添加的内容,就可以在任何能获取到rootProject的地方访问这些属性,而如果只在rootProject/build.gradle中用def来声明这些变量,那么这些变量除了在这个文件里面访问之外,其他任何地方都没办法访问。
  这是因为在build.gradle中直接定义的属性,只是作为gradle构建的生命周期中的Configuration阶段的局部变量而已(参见Gradle的生命周期),而往ext属性中写入变量,则可以在整个构建的生命周期都访问到那些变量。
  此外要注意,ext属性是属于拥有他的相应的对象的,比如Project对象,因此只要能访问到对应的Project对象,就能访问到对应的ext属性

  再看下ext的用法

//定义代码块,在android的rootProject的build.gradle中
ext {
    compileSdkVersion = 25
    buildToolsVersion = "26.0.0"
}
//然后引用
rootProject.ext.compileSdkVersion
rootProject.ext.buildToolsVersion

我们还可以自己建一个Gradle文件,比如叫config.gradle,放我们的ext定义的参数,如下

ext {
    //创建了一个名为android的,类型为map的变量,groovy中可以用[]来创建map类型。那么就是一个map下面又创建了一个map,且名字叫做android。
    android = [
            compileSdkVersion: 23,
            buildToolsVersion: "23.0.2",
            minSdkVersion    : 14,
            targetSdkVersion : 22,
    ]
    dependencies = [
            appcompatV7': 'com.android.support:appcompat-v7:23.2.1',
            design      : 'com.android.support:design:23.2.1'
    ]
}

  想使用这个Gradle里面的ext需要添加引用

apply from: "config.gradle"

  然后使用

android {
    compileSdkVersion rootProject.ext.android.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion
    defaultConfig {
        ...
        minSdkVersion rootProject.ext.android.minSdkVersion
        targetSdkVersion rootProject.ext.android.targetSdkVersion
        ...
    }
  
...
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile rootProject.ext.dependencies.appcompatV7
    compile rootProject.ext.dependencies.design
}

  同样的,ext也可以创建闭包

ext.method1 = {
}
ext.method2 = { x, y ->
}

  调用起来也很方便

rootProject.ext.method1()
rootProject.ext.method2("1","2")
  • manifestPlaceholders
      manifestPlaceholders的作用是替换manifest中的资源,我们可以在Gradle中声明变量,然后在manifest中使用,比如多渠道打包的时候,我们显示不同的app名称和图标,可以通过下面实现
apply plugin: 'com.android.application'

android {
        ...
    defaultConfig {
        ...
    }

    productFlavors {
        other {
            buildTypes {
                release {
                    minifyEnabled false
                    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
                    manifestPlaceholders = [app_name: "其他正式包",app_icon:"@mipmap/release"]
                }
                debug{
                    manifestPlaceholders = [app_name: "其他测试包",app_icon:"@mipmap/debug"]
                }
            }
        }
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
}

  我们在android里添加里productFlavors,并且可以分别指定release和debug打包,这里我只写了app_name和app_icon,如果想要更多,可以接着在括号里面追加,写好后,刷新Gradle,然后我们看下manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="cn.isimpo.distinguish">

    <application
        android:icon="${app_icon}"
        android:label="${app_name}"
        android:roundIcon="${app_icon}">
        ...
    </application>

</manifest>

  只需要用${app_icon}和${app_name}就可以引用我们刚刚Gradle中的资源,等我们打包的时候会在build/output/apk下找到我们渠道名为other的渠道包,一个是debug,一个是release,当然如果想要添加更多的渠道包也是可以的,如下

apply plugin: 'com.android.application'

android {
        ...
    defaultConfig {
        ...
    }

    productFlavors {
        other {
            buildTypes {
                release {
                    minifyEnabled false
                    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
                    manifestPlaceholders = [app_name: "其他正式包",app_icon:"@mipmap/release"]
                }
                debug{
                    manifestPlaceholders = [app_name: "其他测试包",app_icon:"@mipmap/debug"]
                }
            }
        }
        othertwo{
            
        }
        otherthree{
            
        }
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
}

  这里我只写了俩个空白设置的渠道包,如果想要增加配置,模仿第一个即可,当然我们也可以指定不同渠道的更多定制信息,如下

apply plugin: 'com.android.application'

android {
        ...
    defaultConfig {
        ...
    }

    productFlavors {
        other {
            applicationId "cn.example.show"
            minSdkVersion 15
            targetSdkVersion 29
            versionCode 1
            versionName "1.0"
            buildTypes {
                release {
                    minifyEnabled false
                    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
                    manifestPlaceholders = [app_name: "其他正式包",app_icon:"@mipmap/release"]
                }
                debug{
                    manifestPlaceholders = [app_name: "其他测试包",app_icon:"@mipmap/debug"]
                }
            }
        }
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
}

  • buildConfigField
      buildConfigField可以用于生成我们需要的属性,分三个参数
buildConfigField(数据类型,字段名称,字段值)

  数据类型就是我们的String,int等,比如我们想在Module中建一个名叫BASE_URL的字段,就可以这么使用

apply plugin: 'com.android.application'

android {
    ...
    defaultConfig {
        ...
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            buildConfigField("String", "BASE_URL", "\"release\"")
        }
        debug {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            buildConfigField("String", "BASE_URL", "\"debug\"")
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
}

  可以看到正式包和测试包我们分别打了一个服务器地址(这里用debug和release代替了),写好后,项目rebuild一下,然后我们就可以在我们的java代码中使用,如下

public class BaseConstant {

    public static final String BASE_URL = BuildConfig.BASE_URL;

}

  那么当我们打不同的包时,这个生成的BuildConfig.BASE_URL也是不一样的,解决了测试和正式服务器的区分。当然这种方式生成的变量也可以给build.gradle自己使用,但是使用之前我们一般会添加一个判断,如下

ext {
    base_url = "adress"
}
def getUrl() {
    return hasProperty("BASE_URL") ? BASE_URL : ext.base_url
}

  hasProperty是Gradle提供给我们判断本地配置的方法,我们需要使用hasProperty判断是否已经存在BASE_URL这个字段,如果没有的就通过其他方式获取,细心的同学可能注意的release和debug两边用还添加了转义字符和",就是因为是字符串,如果是数字或布尔值则不需要。

  • buildDir
      buildDir这个其实是Gradle提供的一个方法,我们点击过去会发现是getBuildDir()这个方法,这个方法返回的就是我们build的路径,通常我们会利用这个参数来修改我们生成渠道包的一些设置。
      未完待续...

推荐阅读更多精彩内容