您当前的位置: 首页 >  ui
  • 0浏览

    0关注

    674博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

build.gradle配置的本质

沙漠一只雕得儿得儿 发布时间:2021-12-01 11:17:30 ,浏览量:0

android plugin文档:http://google.github.io/android-gradle-dsl/current/

Google官网的gradle手册:Gradle Plugin User Guide - Android Studio Project Site

gradle官方文档:Gradle User Manual: Version 7.3   (分为userguide、dls reference和javadoc)

groovy官方文档:The Apache Groovy programming language - Documentation

这个文档不是讲解build.gradle中各个配置字段的含义,具体的含义可以参加上面的文档(主要是android plugin文档)

这个文档主要是以编程的角度来看待build.gradle文件。gradle做了很多的工作使得本来是代码的文件看起来像是一个配置文件。以下的内容就是把gradle的工作逆向一下,不过由于gradle很庞大,所以只能点到为止,不会展开。

注:以下关于android plugin和gradle的知识都是基于plugin2.2.2版本和gradle 2.14.1版本

1、闭包

        gradle和groovy的基础知识不做介绍,可以参数上面的官方文档以及网上查一些资料。这里只介绍闭包(Closure)的相关知识,这个是核心概念。

   1.1 什么是闭包

        闭包就是使用一对花括号({})包起来的一段代码,groovy有个对应的类:groovy.lang.Closure,可以定义如下:

        def closure = { println "Hello,World!"}  //def类似js中的var,也可以直接指定为Closure。花括号中可以通过回车来包含多个语句

        Closure closure2 = { param1, param2 -> println "${param1},${param2}"} //->之前的为参数,可以带类型,如果留空表示无参数,如果没有->,则包含一个默认的it参数。${}是取{}中表达式的值。

        闭包可以像函数一样调用,上面两个闭包可以这样调用:closure()和closure2("Hello", "World!")。但是闭包和函数还是有区别的,但是我们可以不关心,详见groovy官方文档。

    1.2 this、owner与delegate

        this、owner和delegate是Clousure的三个属性,官方定义如下:

  • this corresponds to the enclosing class where the closure is defined

  • owner corresponds to the enclosing object where the closure is defined, which may be either a class or a closure

  • delegate corresponds to a third party object where methods calls or properties are resolved whenever the receiver of the message is not defined

        通俗点说(不一定非常准确)就是如下:

        this:离定义closure最近的那个类。例如,当Closure定义在一个内部类中时,这个this就是指这个内部类的对象。

        owner:和this很像,不过this是指向class的,而owner除了可以指向class,还可以指向Closure。例如,一个Closure定义在另一个Closure内部,那么这两个Closure的this应该是指向同一个class对象的,而里面那个Closure的owner是指向外面那个Closure的。

        delegate:默认和owner是一样的,但是可以指定为其他对象。这个delegate非常重要,是理解闭包的一个关键点。

        关于这三者的详细介绍,可以参考官方文档:The Apache Groovy programming language - Closures

    1.3 属性和方法查找

        闭包是一个代码块,里面会有一些方法的调用,比如上面闭包中的调用的println函数(groovy中函数调用时圆括号可以省略,上面正常的写法应该是println("Hello,World!"))。闭包中这些方法是怎么查找到的呢?默认情况下,方法是从三个地方查找:

        1、Closure及其父类        

        2、闭包的owner对象

        3、闭包的delegate对象

        示例代码如下(文件名为Test.groovy):

class Foo {

 void cc() {
        println "Hello,cc"
  }

    Closure innerClosure = { 
       cc()
    }
}

def outerClosure = {
    cc()
}

def foo = new Foo()
outerClosure.delegate = foo
foo.innerClosure.delegate = this
outerClosure()
foo.innerClosure()

        这段代码执行的结果就打印出两行“Hello,cc”。这段代码的内容就不说了,大家连蒙带猜的也能看懂。先看一个问题:这个类中,innerClosure和outerClosure的owner和delegate分别是什么?

        1.根据前面介绍可知,innerClosure本来的owner和delegate都是Foo对象,但是在倒数第三行时delegate被赋值成了this,所以innerClosure的owner和delegate分别是Foo对象和this。这个this是啥呢?其实就是Test对象,因为groovy文件会被编译成对应的class文件,本例中即是Test.class,这个class文件的主类就是Test,所以这个this就是Test对象。这个可以通过在innerClosure闭包中打印delegate属性查看。

        2.outerClosure是定义在Foo之外,根据1中说明的groovy文件的编译方式可知,刚开始outerClosure的owner和delegate都是Test对象,但是在倒数第四行时,delegate被赋值成了Foo对象。

        前面我们说过,闭包可以像函数一样调用,这段代码的最后两行就是这样用法。首先看倒数第二行,outerClosure调用就会执行闭包中的代码,即调用cc函数。按照之前的查找规则,首先会到Closure及其父类中查找是否有cc函数,由于cc是自定义函数,所以一般不会有;其次会到outerClosure的owner中去查找,前面说过,这个owner是Test对象,而Test中很明显也没有定义cc函数,因此会接着到delegate对象中去查找,而outerClosure的delegate就是Foo对象,而Foo类中定义了cc函数,因此可以调用成功,如果这个时候还找不到就会抛出MissingMethodException异常。

        最后一行也和倒数第二行一样的查找过程,只不过在innerClosure的owner中就找到了cc函数的定义。

        上面说的是默认的查找方式,闭包的查找方法可以通过resolveStrategy属性来设置,详见闭包官方文档:The Apache Groovy programming language - Closures

        闭包中属性的查找与方法是一样的。

        闭包的属性和方法查找是一个递归的过程,例如,当一个闭包在delegate查找不不到时就会在这个delegate上执行上述方法的查找(如果这个delegate是闭包的话)

        Tips:

        1.groovy代码如何运行

            命令行中没有试过,在AS中,在groovy文件上右键,然后选择Run ‘类名'即可。可以在build目录下查看编译出来的class文件,可以解决某些疑惑,比如groovy文件编译成class文件是自动添加main函数,所以可以执行;在文件最外层定义的变量,在方法中是无法访问等。

    1.4 强制转换

        闭包可以被隐式的转换成一个SAM( single abstract method)类型,所谓SAM就是只有一个abstract方法的interface或abstract class。

2、gradle配置     2.1 生命周期

        gradle的生命周期分为三个阶段:初始化、配置以及执行,官方定义(http://www.gradle.org/docs/current/userguide/build_lifecycle.html)如下:

        Initialization

            Gradle supports single and multi-project builds. During the initialization phase, Gradle determines which projects are going to take part in the build, and creates a Project instance for each of these projects.

         Configuration

            During this phase the project objects are configured. The build scripts of all projects which are part of the build are executed. Gradle 1.4 introduced an incubating opt-in feature called configuration on demand. In this mode, Gradle configures only relevant projects (see the section called “Configuration on demand”).

        Execution

            Gradle determines the subset of the tasks, created and configured during the configuration phase, to be executed. The subset is determined by the task name arguments passed to the gradle command and the current directory. Gradle then executes each of the selected tasks.

        理解gradle生命周期的一个实际意义是知道你的代码什么时候执行,否则愿望和结果会变得风马牛不相及,尤其是配置和执行阶段的理解。

        初始化阶段:就是解析setting.gradle来判断当前project是single project还是multi-project,并且创建Project实例。这个阶段一般我们不太需要关注

        配置阶段:配置就是执行build.gradle中的一些代码(build script)来给project对象设置一些信息(配置),这些信息之后有可能在之后的执行阶段会使用。

        执行阶段:这个阶段就是执行一些task。具体执行哪些task是由运行gradle命令时传入的task名称决定的。task之间是有依赖关系的,gradle会计算出task的依赖关系,当gradle执行一个task时会首先执行它依赖的task。

        不论执行阶段要执行的task是什么,初始化和配置阶段的代码都会被执行,就像不管你出门做什么都会穿上衣服、拿上钥匙、带着钱包、揣着手机一样。

如果想了解各个Project和task的执行顺序,可以在项目的setting.gradle文件中添加以下代码自行查看。

gradle.addProjectEvaluationListener(new ProjectEvaluationListener() {      @Override      void beforeEvaluate(Project project) {           println "emit by gradle : ${project.name} beforeEvaluate"      }

     @Override      void afterEvaluate(Project project, ProjectState state) {           println "emit by gradle : ${project.name} afterEvaluate , state : $state"      } })

gradle.taskGraph.addTaskExecutionListener(new TaskExecutionListener() {      @Override      void beforeExecute(Task task) {           println "emit by gradle : ${task.name} beforeExecute"      }

     @Override      void afterExecute(Task task, TaskState state) {           println "emit by gradle : ${task.name} afterExecute"      } })

gradle.addBuildListener(new BuildListener() {      @Override      void buildStarted(Gradle gradle) {           println "emit by gradle : buildStarted"      }

     @Override      void settingsEvaluated(Settings settings) {           println "emit by gradle : settingsEvaluated"      }

     @Override      void projectsLoaded(Gradle gradle) {           println "emit by gradle : projectsLoaded"      }

     @Override      void projectsEvaluated(Gradle gradle) {           println "emit by gradle : projectsEvaluated"      }

     @Override      void buildFinished(BuildResult result) {           println "emit by gradle : buildFinished"      } })

    2.2 配置的本质

        既然叫配置,配置谁呢?从定义中可以看出是project对象。用谁配置呢?定义里面并没有说,不过既然是配置project,我们可以看一下project的官方文档(Project - Gradle DSL Version 7.3),里面有如下一段:

        Lifecycle

            There is a one-to-one relationship between a Project and a build.gradle file. During build initialisation, Gradle assembles a Project object for each project which is to participate in the build, as follows:

  • Create a Settings instance for the build.
  • Evaluate the settings.gradle script, if present, against the Settings object to configure it.
  • Use the configured Settings object to create the hierarchy of Project instances.
  • Finally, evaluate each Project by executing its build.gradle file, if present, against the project. The projects are evaluated in breadth-wise order, such that a project is evaluated before its child projects. This order can be overridden by calling Project.evaluationDependsOnChildren() or by adding an explicit evaluation dependency using Project.evaluationDependsOn(java.lang.String).

            从这段内容可以看出project对象是使用build.gradle文件来配置的,两者之间是一一对应的关系。那是如何使用build.gradle配置project呢?就是执行build script。那build script是什么呢?官方文档(Gradle DSL Version 7.3)中有一段说明,如下:

A build script is made up of zero or more statements and script blocks. Statements can include method calls, property assignments, and local variable definitions. A script block is a method call which takes a closure as a parameter. The closure is treated as a configuration closure which configures some delegate object as it executes

        一个build script是有一系列语句和script blocks组成。语句就是方法调用,属性赋值和局部变量定义。script block其实也是一个方法调用,只不过这个方法的参数是一个closure。

        所以,配置就是通过调用一系列方法来设置给project一些信息,这些信息会在之后执行时使用。那些build.gradle文件中看着像配置的东东其实就是方法调用。

      2.3 build.gradle文件分析

            前面说了那么一堆理论性的东西,接下来看看几个实际的应用。在此之前,在强调一下之前红色文字的内容:1、groovy(gradle)中函数的调用可以省略圆括号;2、调用闭包的方法时会到它的delegate中查找

        2.3.1 apply

            gradle应该算是一个框架,很多事情都是需要插件来实现的。对android工程(产出为apk的),就会看到下面这句:

 apply plugin: 'com.android.application'

           上面这一句是应用Google提供编译android工程的插件。该怎么理解这句代码呢?前面说过,配置实际就是在调用一些方法,所以这个应该是在调用一个方法。但是这个看着不像啊!嗯,参加上面第一条,我们把圆括号加上,给它还原一下,如下:

apply(plugin: 'com.android.application')

            这样看着就比之前像了,但是里面的参数看着还是奇怪。不过看到这个冒号(:)我们会马上想到key-value,是的,这个就是一个map,这个apply方法接收一个map类型作为参数。虽然看着奇怪,在groovy中奇怪的写法很多,从现在开始慢慢习惯把。

        2.3.2 buildscript

            接下来看一下前面说过的script blok:

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

            这个稍微有点复杂,不过我们把这个分析完之后,其他的基本也就能理解了。再说一遍:配置都是方法调用;函数调用时圆括号可以省略。所以buildscript应该是一个方法名,后面花括号应该是一个参数,根据前面介绍可知,这个参数是一个闭包类型。repositories和dependencies也是和buildscript差不多,jcenter就不说了,和Java中函数调用一样,classpath也是一个函数,与上面说的apply类似。我们把上面的代码写成我们熟悉的样子(这个只是为了解释而做的转换,实际执行时和上面似乎有一些区别,原因不详):

def closure = {
def repoClosure = {
        jcenter()
    }
    repositories(repoClosure)
def dependClosure = {
        classpath('com.android.tools.build:gradle:2.2.2')
    }
    dependencies(dependClosure)
}
buildscript(closure)

            上面举例说明了配置实际上是一些函数调用,虽然可能看着还是奇怪,但也能勉强接受和理解。不过这些函数是定义在哪里呢?回答这个问题之前,先看一个官网上关于project的说明(Project - Gradle DSL Version 7.3):

Dynamic Methods

A project has 5 method 'scopes', which it searches for methods:

  • The Project object itself.
  • The build file. The project searches for a matching method declared in the build file.
  • The extensions added to the project by the plugins. Each extension is available as a method which takes a closure or Action as a parameter.
  • The convention methods added to the project by the plugins. A plugin can add properties and method to a project through the project's Convention object.
  • The tasks of the project. A method is added for each task, using the name of the task as the method name and taking a single closure or Action parameter. The method calls the Task.configure(groovy.lang.Closure) method for the associated task with the provided closure. For example, if the project has a task called compile, then a method is added with the following signature: void compile(Closure configureClosure).
  • The methods of the parent project, recursively up to the root project.
  • A property of the project whose value is a closure. The closure is treated as a method and called with the provided parameters. The property is located as described above.

            这个是说哪些方法会被认为是project对象中的方法,这里简单说明一下(按照上面官方定义的顺序):

    • Project对象自己,这个没啥好解释的
    • build file是指gradle中定义的方法
    • extensions是project的一个属性,它其实是一个容器。在开发插件的时候一般都会定义一个Extension类,在Plugin类中会调用extensions的create方法,这个时候create的第一个参数就会被当作一个方法名动态添加到project中。下面会再说明一下。
    • convention这个不是很熟悉,暂时没有使用,之后有人了解的话可以补充一下。
    • gradle文件中定义的task。gradle中定义的task可以把task名称当作一个方法名来直接调用。定义task时,gradle其实会根据task的名字动态创建一个方法。
    • 父工程中的方法。这个对multi-project中有用。
    • 当把属性定义成一个closure时,可以把这个属性当作一个方法来调用,这个之前也说过:闭包可以当作一个方法来调用。

            之所以要说明project对象方法的查找范围,是因为我们可以把build.gradle文件整个看成一个闭包(不一定是闭包,没有深究),而这个闭包的delegate就是project。每个build.gradle文件中都有一个project,我们调用方法时可以加上“project.”,不过一般情况下都是省略的,就像调用同一个类中的方法时我们一般不再方法前加this.一样。project中的属性和方法类似,可以看一下官方文档。

            现在我们来回答一下之前的问题:上面例子中的方法定义在哪里?答案是:部分是定义在Project中(广义范围的project,包括Project的父类及上面Dynamic Methods中说明的)。为什么是部分呢?我们对上面的例子做进一步分析:

  1. 之前说过apply是一个方法,这个方法是定义在PluginAware类中的,而Project实现了该类,因此apply也算是project中的方法。在上面的例子上我们说过,apply的参数是一个map,既然如此,这个map的key和value可以随便定义吗?从API看key是不行的,这个key的取值只有几个:from、plugin和to。其实这几个key也是函数,那它们定义在哪里呢?是在ObjectConfigurationAction类中。至于map的key和value如何映射到ObjectConfigurationAction对象的,我没有更进一步分析,有兴趣的可以自己看源码分析。
  2. buildscript也是project的方法,可以直接在API文档中找到,是一个接受Closure作为参数的方法。
  3. repositories和dependencies方法调用的不是Project中的方而是ScriptHandler类中的,虽然Project中也有这两个方法。可是,我怎么知道这两个方法调用的不是Project中的呢?回到这一节的开始,看一下我们使用黑体字强调内容的第二条。这两个方法是在repoClosure的delegate中查找的。but,根据之前的定义,repoClosure变量的delegate应该是变量closure啊?刚开始确实是,但是,delegate也是可以修改的,在buildscript函数中,作为参数的那个闭包的delegate就被修改成ScriptHandler对象了,不过这个过程也挺复杂的,有兴趣的可以自己看。由此可以得出另外一个推论:闭包中方法的查找是先在owner、delegate等对象中查找,如果没有,但是有project对象则在project对象中查找。作为验证,可以在传递给buildscript函数的那个闭包中调用一个ScriptHandler类中没有,但是project中有的方法,这个时候也能编译通过。 
  4. jcenter函数可以按照上面的分析方法来看一下是在哪里定义的(在RepositoryHandler类中,gradle中的类可以到gradle官网的javadoc中查找)。
  5. classpath这个按照上面的方法来分析的话是找不到一个叫classpath的函数的。哪里出问题了呢?分析的方法没有错,只不过这个classpath方法是一个运行时的方法,就是说这个方法是在运行时动态生成的。关于动态方法可以多说一点:根据上面的方法,可以分析到classpath函数应该在DependencyHandler类中,但是实际情况是这个类中是没有的,但是这个类的实现DefaultDependencyHandler类中有个叫methodMissing的方法,当调用的方法不存在是就会调用这个方法,这个方法中就会动态的添加那个不存在的方法,具体实现有兴趣的可以自己去看。对于DependencyHandler来说,并不是所有的方法找不到 都会添加,比如把上面的dependencies改成下面的代码就会报错:
dependencies {
        abc 'com.android.tools.build:gradle:2.2.2'
}

          为什么呢?以下纯属猜测,如果错误请更正:上面的代码只有一个方法名叫abc,然后一个字符串参数,但是并没有实现啊,所以你这么胡乱写一个方法名肯定是不行的,因为根本不知道这个方法是干嘛的,因此报错也是合情合理的。使用classpath为什么可以呢,是因为这个是配置编译和执行脚本本身所需要的classpath,因为这个含义很明确,所以gradle内部就可以实现了,只需要你设置一个参数,告诉这个classpath是谁即可。

        2.3.3 android

            android工程的build.gradle文件中都有如下一段代码:

android {
  compileSdkVersion 21
 buildToolsVersion '21.1.2'
  //其他配置
}

            由前面的分析可知,android应该是一个函数,可是我们却找不到。莫非这也是一个动态方法?答案是:是的。前面说project方法查找范围的时候说过extensions,它是project的一个属性,类型是ExtensionContainer,看一下这类的API文档可知,这个类有个add方法,这个方法会根据第一个参数动态创建一个同名的属性和方法,这个动态创建的方法接收一个闭包作为参数。那么这个叫android的方法是什么时候添加的呢?作为它参数的那个闭包的delegate又是什么呢?这就要先回到之前说的apply方法。当调用apply方法应用一个插件时,会调用这个插件的apply方法,而id为com.android.application的插件对应的类就是AppPlugin,它的apply方法是直接调用父类的apply方法,即BasePlugin的apply方法,这个方法会调用一个叫createExtension的方法,这个方法的内容如下:

private void createExtension() {
  NamedDomainObjectContainer buildTypeContainer = this.project.container(BuildType.class, new BuildTypeFactory(this.instantiator, this.project, this.project.getLogger()));
 NamedDomainObjectContainer productFlavorContainer = this.project.container(ProductFlavor.class, new ProductFlavorFactory(this.instantiator, this.project, this.project.getLogger(), this.extraModelInfo));
 final NamedDomainObjectContainer signingConfigContainer = this.project.container(SigningConfig.class, new SigningConfigFactory(this.instantiator));
 this.extension = (BaseExtension)this.project.getExtensions().create("android", this.getExtensionClass(), new Object[]{this.project, this.instantiator, this.androidBuilder, this.sdkHandler, buildTypeContainer, productFlavorContainer, signingConfigContainer, this.extraModelInfo, Boolean.valueOf(this.isLibrary())});
 //删除无用代码
}

          project.getExtensions()返回的就是extensions,然后调用extensions的create方法,这个create方法的实现在DefaultConvention类中:

public  T create(String name, Class type, Object... constructionArguments) {
T instance = getInstantiator().newInstance(type, constructionArguments);
 add(name, instance);
 return instance;
}

            可以看到,在create方法中会调用add方法,这个name就是android,type是AppExtension.class,因此那个instance是一个AppExtension对象。这个AppExtension对象是干嘛用的呢?其实它最后会被设置为andorid方法接收的那个closure的delegate,可以大胆猜测,android方法接收的那个closure中的方法很多都是AppExtension中的,实际情况也就是如此。例子中的compileSdkVersion和buildToolsVersion方法(希望这个时候你已经能接受它们是方法来)可以在AppExtension的父类BaseExtension中找到。

            所以,想了解android方法的闭包中各个配置的含义以及有哪些配置,可以查看前面说的android plugin文档。

            这里有个技巧,当我们想查看build.gradle文件中的某个方法的定义时,可以使用快捷键(AS默认为Ctrl-b或Ctrl+鼠标),不过查看android的定义时,会直接跳到AppExtension这个类,前面说过这个类是android方法参数的delegate,这个应该是IDE做的吧。

        2.4 配置阶段与执行阶段

            这两个阶段容易弄混,主要是会把配置阶段执行的代码认为是在执行阶段执行的。要弄清这个问题,其实只要搞清执行阶段会执行那些就可以了,剩下的就是配置和初始化阶段。而执行阶段就是在执行task,而一般task都是预先定义好的,我们都看不到,所以那些build.gradle文件中的代码都是在配置阶段就执行了。但是我们也可以自定义一个task,如下:

task abc {
    println "configuration phase"
 doLast {
        println "excution phase:abc task"
 }
}

task exe {
    doLast {
        println "excution phase:exe task"
 }
}

            当我们执行exe时(gradle exe)时,结果是什么呢?结果就是会打印:configuration phase和excution phase:exe task,虽然我们执行的是exe task,但是abc task的第一个println也会执行,因为它属于配置阶段的代码,第二个println没有执行是因为它是执行阶段的代码,而abc没有被执行,所以不会打印。那怎么知道abc task的第二个println是在执行阶段执行呢?根据前面的介绍,现在应该很容易知道,这个doLast其实是个方法,定义在Task类中,而它的实现就是把作为参数的闭包放到一个list中,在task执行的时候会遍历这个list执行里面的闭包。所以从微观来说,想知道一个闭包中的代码在哪个阶段执行可以去看文档和源码。

关注
打赏
1657159701
查看更多评论
立即登录/注册

微信扫码登录

0.0399s