深入gradle

gradle api文档:

https://docs.gradle.org/current/javadoc/



1.gradle版本和gradle插件版本:

as中已经安装的gradle在as安装目录下的contents/gradle中

工程中gradle版本在工程的gradle/wrapper/gradle-wrapper.properties里配置

distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

工程中gradle插件版本在project的build.gradle中配置:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.1'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

在project structure中也可以看到gradle版本和gradle插件版本


2.gradle环境变量配置:

在/Users/xxx/(~/)下的.bash_profile文件中,加入环境变量配置:

export GRADLE_HOME=/Applications/Android\ Studio.app/Contents/gradle/gradle-2.14.1(对应as的安装路径)
export PATH=${PATH}:${GRADLE_HOME}/bin


最好再配置一个GRADLE_USER_HOME,它在每个工程的gradle/wrapper/gradle-wrapper.properties文件中要用到,一般默认情况下GRADLE_USER_HOME配置为

/Users/xxx/.gradle(~/.gradle),比如:

export GRADLE_HOME=/Applications/Android\ Studio.app/Contents/gradle/gradle-2.14.1
export GRADLE_USER_HOME=/Users/zhouyi/.gradle
export PATH=${PATH}:${GRADLE_HOME}/bin


3.我们在as中make,clean project,rebuild,run,debug等操作实际上是运行gradle相应project的tasks,这些tasks可以在as右侧的gradle projects面板找到(排列的顺序并不是执行的顺序,执行的顺序可以在event log中观察),在做这些操作的过程中,可以看到底部的event log或gradle console,能看到执行了哪些tasks和正在执行的gradle详细执行操作,比如执行clean project时,event log中:

Executing tasks: [:app:clean, :app:generateDebugSources, :app:generateDebugAndroidTestSources, :app:mockableAndroidJar, :app:prepareDebugUnitTestDependencies, :app:assembleDebug]

这些task在gradle projects中都可以找到


4.gradle执行的是task,我们在as模块的gradle中可以配置插件,比如:

android app模块:

apply plugin: 'com.android.application'
java library模块:

apply plugin: 'java'
android library模块:

apply plugin: 'com.android.library'

配置的这些插件,决定了会执行哪些task(右侧的gradle projects面板可以看到不同类型插件对应的task不一样),这些task其实都是插件里内定好的,我们在as中配置的gradle文件只是配置这些固定task的参数而已,并不是真正执行的task,当然,你也可以自己去定义插件以外需要执行的task


5.gradlew是对gradle的包装,因为直接用gradle命令,可能团队中每个人的版本不一致,造成一些bug,gradlew(gradle wrapper)的命令跟gradle命令一致,但是它会保证整个团队使用一致的gradle版本,如果这个版本不存在,就会自动去下载对应的版本,在新建as工程时,会在每个工程根目录生成gradle脚本文件(mac下是shell,window下是bat),可以直接这个gradlew(如果不考虑版本影响,可以可以直接用gradle命令去执行gradle tasks(先设置好环境变量))

比如可以用 gradle -v获取gradle版本

也可以在工程目录下运行(或直接在as的终端面板下)使用:./gradlew -v


第一次运行gradlew命令,gradlew会从工程的gradle/wrapper/gradle-wrapper.properties配置的distributionUrl中获取gradle,默认是这样的:

distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

如果网络不好,会一直卡住

如果不想下载,可以直接配置离线路径,比如:

distributionUrl=file:///Users/zhouyi/.gradle/gradle-2.14.1-all.zip

也可以把zip放到gradle/wrapper下面,这样就可以直接这样配置:

distributionUrl=gradle-2.14.1-all.zip

这时候,再输入gradlew命令,第一次会解压zip,后面就跟使用gradle命令一样了

如果你想后面新建文件,默认生成的radle-wrapper.properties文件都配置成这个路径,需要修改以下模板:

/plugins/android/lib/templates/gradle/wrapper/gradle/wrapper/gradle-wrapper.properties
这个模板在每次新建工程的时候,都会拷贝到新建的工程里
/plugins/android/lib/templates还有其他一些新建工程时需要的模板,比如build.gradle,如果需要,也可以在此修改

如果gradlew不能运行,可能是没有运行权限,增加运行权限即可:

chmod +x gradlew


5.as中gradle文件执行顺序和tasks的生成:

  • 首先会解析自动生成的xxx.gradle文件,解析顺序

rootproject 的setting.gradle,然后是rootproject的build.gradle,然后是各个module。所以project下的build.gradle会先于app下的build.gradle。

setting.gradle中配置了需要解析那些module底下的build.gradle,比如:

include ':app', ':javatestutils'
模块并不一定要放在专门的目录中,比如可以把默认生成的app模块放在项目的根目录下,这时候setting.gradle类似这样配置:

include ':'
include ':base_framework'
这样as会从根目录去找模块的build.gradle去解析

  • 根据解析的结果,生成gradle tasks

gradle tasks按照as中项目的的project(可以多个)和module(可多个)组织,在右侧的gradle projects可以看到所有的gradle tasks以及组织结构

生成task主要根据gradle里配置的插件来生成的,比如android application插件会有install任务, android library和java就没有

插件也会根据gradle中的其他配置生成不同的task,比如assemble,默认配置一般就分为assemble,assembledebug,assemblerelease,如果配置了多渠道,则会对每个渠道都生成对应的assembledebug和assemblerelease任务,install也类似


在gradle projects面板中对每个module点击右键,都会有一个open gradle config选项,用来打开对应module的build.gradle文件,说明这种对应关系,也说明build.gradle是tasks的配置文件,build.gradle中那些标签都是在tasks里定义好的


6.tasks的执行:

可以单独执行某个task,比如:

./gradlew installBaiduDebug

./gradlew assemble---会组装所有渠道的apk

./gradlew  assemble_360Flavor --组装360渠道的apk,./gradlew assemble会执行它

gradle会预先计算这些task的依赖,这些依赖会形成一个有向无环图,比如assemble肯定要执行build,执行一个task,会从依赖图的最开始一直执行到本task


我们在as上做clean,make,rebuild,run这些操作时,实际上是在执行一些task


有四个基本的 task, Android 继承他们分别进行了自己的实现:

  • assemble:对所有的 buildType 生成 apk 包。
  • clean:移除所有的编译输出文件,比如apk
  • check:执行lint检测编译。
  • build:同时执行assemblecheck命令

这些都是基本的命令,在实际项目中会根据不同的配置,会对这些task 设置不同的依赖。比如 默认的 assmeble 会依赖 assembleDebug 和assembleRelease,如果直接执行assmeble,最后会编译debug,和release 的所有版本出来。如果我们只需要编译debug 版本,我们可以运行assembleDebug

除此之外还有一些常用的新增的其他命令,比如 install命令,会将编译后的apk 安装到连接的设备。

执行这些基本的命令,就跟在as上做clean,make,rebuild,run这些操作类似,会执行一些as设置好的task,比如执行./gradlew install跟as中直接run类似,执行./gradlew clean跟as中clean也类似,当然不是完全一样的,但是原理都是去执行一组task


7.安卓的编译流程非常复杂:



我们需要一种工具帮我们更快更方便更简洁地完成 Android 程序的编译。现在结合Android Studio 我们一般使用的工具都是Gradle, 在 Gradle 出现以前Android 也有对应的编译工具叫 Ant,在Gradle 出现之后,也有新的编译工具出现,就是FaceBook 的Buck工具。这些编译工具在出现的时候几乎都比 Gradle 要快,Gradle 之所以慢是跟它的编译周期有很大关系。

Gradle 的编译周期

在解析 Gradle 的编译过程之前我们需要理解在 Gradle 中非常重要的两个对象。ProjectTask

每个项目的编译至少有一个 Project,一个 build.gradle就代表一个project,每个project里面包含了多个task,task 里面又包含很多actionaction是一个代码块,里面包含了需要被执行的代码。

在编译过程中, Gradle 会根据 build 相关文件,聚合所有的projecttask,执行task 中的 action。因为 build.gradle文件中的task非常多,先执行哪个后执行那个需要一种逻辑来保证。这种逻辑就是依赖逻辑,几乎所有的Task 都需要依赖其他 task 来执行,没有被依赖的task 会首先被执行。所以到最后所有的 Task 会构成一个 有向无环图(DAG Directed Acyclic Graph)的数据结构。

编译过程分为三个阶段:

  • 初始化阶段:创建 Project 对象,如果有多个build.gradle,也会创建多个project.
  • 配置阶段:在这个阶段,会执行所有的编译脚本,同时还会创建project的所有的task,为后一个阶段做准备。
  • 执行阶段:在这个阶段,gradle 会根据传入的参数决定如何执行这些task,真正action的执行代码就在这里.

8.BuildConfig

个类相信大家都不会陌生,我们最常用的用法就是通过BuildConfig.DEBUG来判断当前的版本是否是 debug版本,如果是就会输出一些只有在 debug 环境下才会执行的操作。 这个类就是由gradle 根据 配置文件生成的,路径在:工程目录/build/source/buildConfig,按渠道名,包名,编译选项存放,以下是一个样例:

public final class BuildConfig {
  public static final boolean DEBUG = false;
  public static final String APPLICATION_ID = "com.qf.zhouyi.mutilchannelpkgtest";
  public static final String BUILD_TYPE = "release";
  public static final String FLAVOR = "baidu";
  public static final int VERSION_CODE = 1;
  public static final String VERSION_NAME = "1.0";
}
这个功能非常强大,我们可以通过在这里设置一些key-value对,这些key-value 对在不同编译类型的 apk 下的值不同,比如我们可以为debug 和release 两种环境定义不同的服务器。比如:

// 打包选项
buildTypes {

    
    Type1{
        debuggable true
        shrinkResources false
        minifyEnabled false
        zipAlignEnabled false
        jniDebuggable true
        signingConfig signingConfigs.znSigningConfig
        buildConfigField "String", "LIVE_SHARE_URL", "\"http://aaa/liveShare/index.html\""
        buildConfigField "String", "CONFIG_TAG", "\"type1\""
    }

    //下面是打包时使用的环境
    Type2{
        debuggable true
        shrinkResources false
        minifyEnabled false
        zipAlignEnabled false
        jniDebuggable true
        signingConfig signingConfigs.znSigningConfig
        buildConfigField "String", "LIVE_SHARE_URL", "\"http://bbb/zhiniao_test/liveShare/index.html\""
    
        buildConfigField "String", "CONFIG_TAG", "\"Type2\""
    }
    .....
}

这样就会根据你选择的编译选项,生成不同的buildconfig文件,比如你编译的是Type1版本

// Fields from build type: Dev
public static final String CONFIG_TAG = "Type1";
public static final String LIVE_SHARE_URL = "http://aaa/zhiniao_test/liveShare/index.html";

在代码中可以使用BuildConfig.CONFIG_TAG,BuildConfig.LIVESHARE_URL来引用它们


9.Build Variants

在开发中我们可能会有这样的需求:

  • 我们需要在debug 和 release 两种情况下配置不同的服务器地址;
  • 当打市场渠道包的时候,我们可能需要打免费版、收费版,或者内部版、外部版的程序。
  • 渠道首发包通常需要要求在欢迎页添加渠道的logo。等等
  • 为了让市场版和debug版同时存在于一个手机,我们需要编译的时候自动给debug版本不一样的包名。

这就需要结合sourceSets,buildTypes,productFlavors来实现,通过配置,可以给不同渠道配置不同的编译代码、不同的mainefest、不同的res等等

比如我现在的渠道配置如下:

productFlavors {
    default_channel{}
    wandoujia{}
    _360{}
    yingyongbao{}
    xiaomi{}
    baidu{}
    huawei{}
    jifeng{}
}
我要对baidu渠道配置不一样的启动activity,这就要设置sourceSets了

首先,sourceSets会有一个默认的main配置(不写也会有),默认情况下,所有渠道都会继承这个配置,如果某个渠道需要配置成不一样,就需要在sourceSets增加相应渠道的配置,比如:

sourceSets {
    baidu{
        java.srcDirs = ['src/baidu/java']
        res.srcDirs = ['src/baidu/res']
        manifest.srcFile 'src/baidu/AndroidManifestbaidu.xml'
    }
}
首先,路径都是相对于当前模块所在路径的(即现在配置的这个模块的build.gradle所在路径),我的项目路径如下,所以都是以src打头:

然后,两个渠道相同包下,类名不能一样

除了类名,资源名,maiifest名等其他都可以一样

做了特殊配置的渠道,还是会继承main里面的东西,比如res,如果baidu底下res目录没有某个资源,就会从main里找

如果main和百度里都有一样的文件(.java不能一样,否则会编译出错),这时候,mainifest和string资源是合并,其他文件是替换,就是说比如baidu里定义了一个string资源如下:

<resources><string name="app_name">TypesAndFlavors STAGING</string></resources>
main里定义了如下:

<resources><string name="app_name">TypesAndFlavors</string><string name="hello_world">Hello world!</string></resources>

编译百度版本时,就会合并如下:

<resources><string name="app_name">TypesAndFlavors STAGING</string><string name="hello_world">Hello world!</string></resources>

就是里面内容如果是相同部分就覆盖,如果是没有的,就新增,其实就是取并集

mainifest类似

所以,我们只需要定义baidu和main不一样的地方即可,一致的地方就直接继承main即可

对buildTypes的配置也跟productFlavors一致


10.自定义task

在project的build.gradle最后加入:

task hello {
    println 'hello world'
}
同步后,发现右侧的gradle projects的app模块的other里面多了一个task

可以使用命令执行:

./gradlew hello



11.groovy语法:

gradle基于groovy语言

  • 变量定义:

在groovy 中,没有固定的类型,变量可以通过def关键字引用,比如:

def name = 'Andy'

我们通过单引号引用一串字符串的时候这个字符串只是单纯的字符串,但是如果使用双引号引用,在字符串里面还支持插值操作,

def name = 'Andy'
def greeting = "Hello, $name!"
作为例外,方法参数和循环变量的声明不需要def


  • 方法定义和调用:

defsquare(defnum){ num * num}

调用:square 4 square(4)

  • 类定义和调用:

Groovy 也是通过Groovy 定义一个类:

class MyGroovyClass {
    String greeting
    String getGreeting() {
        return 'Hello!'
    }
}
  • 在Groovy 中,默认所有的类和方法都是pulic的,所有类的字段都是private的;
  • 和java一样,我们通过new关键字得到类的实例,使用def接受对象的引用:def instance = new MyGroovyClass()
  • 而且在类中声明的字段都默认会生成对应的setter,getter方法。所以上面的代码我们可以直接调用instance.setGreeting 'Hello, Groovy!'注意,groovy 的方法调用是可以没有括号的,而且也不需要分号结尾。除此之外,我们甚至也可以直接调用;
  • 我们可以直接通过instance.greeting这样的方式拿到字段值,但其实这也会通过其get方法,而且不是直接拿到这个值。
  • 集合:

在 Groovy 中,定义一个列表是这样的:

List list = [1, 2, 3, 4, 5]

遍历一个列表是这样的:

list.each { element ->

println element

}

定义一个 map 是这样的:

Map pizzaPrices = [margherita:10, pepperoni:12]

获取一个map 值是这样的:

pizzaPrices.get('pepperoni')
pizzaPrices['pepperoni']

  • 闭包:
Groovy的闭包更象是一个“代码块”或者函数指针,代码在某处被定义然后在其后的调用处执行

def square = { num ->
    num * num
}
square 8

如果只有一个参数,我们甚至可以省略这个参数,默认使用it作为参数,最后代码是这样的:

Closure square = {
    it * it
}
square 16
理解闭包的语法后,我们会发现,其实在我们之前的配置文件里,android,dependencies这些后面紧跟的代码块,都是一个闭包而已。

dependencies {
    compile 'com.android.support:appcompat-v7:23.4.0'
    compile 'org.greenrobot:eventbus:3.0.0'
}


这段脚本的作用是往项目引入指定的第三方库
大括号{compile "<groupId>.<artifactId>:<version>"}的部分是一个闭包(标红部分),dependecies是Project里的一个方法,dependecies { }是把这个闭包作为一个参数传入dependencies方法中,在闭包里,执行了2个语句,每个语句执行一个compile方法,传入一个string参数













阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭