Gradle build process

1. Gradel build lifecycle

Any Gradle build process is divided into three parts: initialization, configuration, and execution.

Initialization phase

The task of the initialization phase is to create a hierarchy of projects and create a Project instance for each Project. The script file associated with the initialization phase is settings.gradle (including all. Gradle script files in

/.gradle/init.d, which apply to all native build processes). A settings.gradle script corresponds to a Settings object. The most common way to declare a project’s hierarchy is to include a method in the Settings class. When gradle initializes, it constructs a Settings instance object, which contains the methods shown in the following figure. These methods can be accessed directly from settings.gradle.

For example, you can add listeners to the Gradle build process with the following code:

gradle.addBuildListener(new BuildListener() {
  void buildStarted(Gradle var1) {
    println 'Start building'
  }
  void settingsEvaluated(Settings var1) {
    println 'Settings evaluation completed (code in settins.gradle completed)'
    // var1.gradle.rootProject: error when you access the Project object, the initialization of the Project is not complete
  }
  void projectsLoaded(Gradle var1) {
    println 'Project structure loading completed (initialization phase completed)'
    println 'Initialization is complete, and the root project is accessible:' + var1.gradle.rootProject
  }
  void projectsEvaluated(Gradle var1) {
    println 'All project evaluations completed (end of configuration phase)'
  }
  void buildFinished(BuildResult var1) {
    println 'Build over'}})Copy the code

After writing the corresponding Gradle lifecycle listening code, we should see the following information in the Build output interface:

(Settings. Gradle code execution is complete) The project structure is loaded (initialization phase is complete) initialization is complete, and the root project is accessible: The root project 'GradleDemo' evaluation of all projects completed phase (configuration) > Task: prepareKotlinBuildScriptModel the UP - TO - DATE end of the buildingCopy the code
The include extension

We can use the include + project method to refer to projects in any location, as follows:

include ':test'
project(':test').projectDir = file('Absolute path of current project')
Copy the code

I usually use this way to reference their own library for debugging, very convenient

However, sometimes we encounter the situation that both AAR and source code are introduced at the same time. We can use include + project, combined with some other configurations, to achieve a quick switch between AAR and source code. The specific steps are as follows:

// Step 1: Introduce the source project in settings.gradle
include ':test'
project(':test').projectDir = file('Absolute path of current project')

// Step 2: Perform the following configuration under the root build.gradle
allprojects {
    configurations.all {
        resolutionStrategy {
            dependencySubstitution {
                substitute module("com.dream:test") with project(':test')}}}}Copy the code

Combined with the official website to provide the document portal to view the effect of the bar

The configuration phase

The tasks in the configuration phase are to execute the build.gradle scripts under each item, complete the configuration of the Project, and construct the Task dependency diagram to execute the Task according to the dependency in the execution phase. This is also the phase of build that we are most often involved in, such as external build plugin apply Plugin: ‘com.android.application’, configure plugin property Android {compileSdkVersion 30… }, etc. Each build.gralde script file corresponds to a Project object that is created during the initialization phase, the interface document of the Project. The code to execute in the configuration phase includes various statements in build.gralde, closures, and configuration section statements in the Task. Add the following code in the root directory of build.gradle:

println 'Build. gradle configuration phase'

// Invoke Project dependencies(Closure c) to declare Project dependencies
dependencies {
    // The code executed in the closure
    println 'Code executed in Dependencies'
}

// Create a Task
task test() {
  println 'Configuration code in Task'
  // Define a closure
  def a = {
    println 'Configuration code 2 in Task'
  }
  // Execute closures
  a()
  doFirst {
    println 'This code will not be executed during configuration.'
  }
}

println 'I do it sequentially.'
Copy the code

Call Gradle build and get the following result:

Configure project: root project 'GradleDemo' > Configure project: 2 I am in order to execute all project evaluation completed (configuration phase end) > Task End: prepareKotlinBuildScriptModel UP - TO - DATE buildCopy the code

It is important to note that the configuration phase not only executes the statements in build.gradle, but also the configuration statements in the Task. As you can see from the above execution result, after executing the closure of dependencies, the configuration code in Task test is executed directly. (All the code in Task except Action is executed in the configuration phase.) On the other hand, whenever you run Gradle commands, the initialization and configuration code is executed. The same Gradle script above, we execute Gradle Help task, still will print the above execution results.

Execution phase

At the end of the configuration phase, Gradle creates a directed acyclic graph based on the dependencies of tasks. The getTaskGraph method of Gradle objects is used to access the TaskExecutionGraph class. When the directed acyclic graph is built, Before all tasks are executed, We can through the whenReady (groovy. Lang. Closure) or addTaskExecutionGraphListener (TaskExecutionGraphListener) to receive the corresponding notification, its code is shown below:

gradle.getTaskGraph().addTaskExecutionGraphListener(new
TaskExecutionGraphListener() {
    @Override
    void graphPopulated(TaskExecutionGraph graph) {
    }
})
Copy the code

Then execute the task by calling Gradle < Task name >.

2. The Hook

Gradle provides a number of hooks for developers to modify behavior during the build process. To illustrate this, look at the following diagram.Pay attention toGradle creates an instance of the script file when it is executed. There are three types of objects:

Gradle object: This object is built when the Project is initialized. The global singleton exists and only this object exists. Gradle is converted to a Seetings object

Gradle provides lifecycle callbacks in all phases. When adding listeners, note that listeners must be added before the lifecycle callback. Otherwise, some callbacks will not be received

Gradle initialization phase
  • After settings.gradle is executed, the settingsEvaluated method of the Gradle object is called back
  • After building all Project objects corresponding to build.gradle, that is, the initialization phase is finished, the gradle object’s projectsLoaded method will be called back
Gradle configuration phase:
  • Gradle loops through the build. Gradle script file for each project

  • Before executing the current Project build.gradle, calls back the gradle object’s beforeProject method and the current Project object’s beforeEvaluate method

  • After executing the current Project build.gradle, the afterProject method of the Gradle object and the afterEvaluate method of the current Project object are called

  • The projectsEvaluated method of the Gradle object is called after the build.gradle of all projects is completed

  • When the configuration phase is complete, the TaskExecutionGraph object’s whenReady method is called after building the Task dependent directed acyclic graph

Note: The callback timing of Gradle objects’ beforeProject and afterProject methods is the same as that of Project objects’ beforeEvaluate and afterEvaluate methods. The differences are:

Gradle objects beforeProject and afterProject methods are called before and after the project builds. Gradle objects beforeProject and afterProject methods are called before and after the project builds

The Project object’s beforeEvaluate and afterEvaluate methods receive callbacks before and after execution of the Project’s build.gradle

3. Execution phase
  • Gradle loops through tasks and their dependent tasks

  • The beforeTask method of the TaskExecutionGraph object is called before the current Task executes

  • After the current Task executes, the afterTask method of the TaskExecutionGraph object is called back

  1. When all tasks are finished, the buildFinish method of the Gradle object is called back

Now that we know the Gradle lifecycle, we can add hooks according to our needs. For example, we can print the time taken for each stage and Task in the Gradle build process

Get the time consumption of each phase of the build

Add the following code to settings.gradle:

// Start time of initialization
long beginOfSetting = System.currentTimeMillis()
// Start time of the configuration phase
def beginOfConfig
// Whether the configuration phase has started is executed only once
def configHasBegin = false
// Store the time before each build.gradle is executed
def beginOfProjectConfig = new HashMap()
// Start time of execution
def beginOfTaskExecute
// The initialization phase is complete
gradle.projectsLoaded {
    println ${system.currentTimemillis () -beginofSetting} ms"
}

/ / build. Gradle before execution
gradle.beforeProject {Project project ->
    if(! configHasBegin){ configHasBegin =true
        beginOfConfig = System.currentTimeMillis()
    }
    beginOfProjectConfig.put(project,System.currentTimeMillis())
}

/ / build. Gradle executed
gradle.afterProject {Project project ->
    def begin = beginOfProjectConfig.get(project)
    println ${system.currentTimemillis () -begin} ms ${system.currentTimemillis () -begin} ms
}

// The configuration phase is complete
gradle.taskGraph.whenReady {
    println ${system.currentTimemillis () -beginofConfig} ms"
    beginOfTaskExecute = System.currentTimeMillis()
}

// Execution phasegradle.taskGraph.beforeTask {Task task -> task.doFirst { task.ext.beginOfTask = System.currentTimeMillis() } task.doLast  { println${system.currentTimemillis () -task.ext.beginofTask} ms"}}// The execution phase is complete
gradle.buildFinished {
    println ${system.currentTimemillis () -BeginOfTaskExecute}"
}

// Run the Gradle command
./gradlew clean

// The output is as follows:Total initialization time140 ms

> Configure project :Configuration phase, root project'GradleDemo'Time:1181 ms

> Configure project :App configuration phase, project':app'Time:1122Total ms configuration time:2735 ms

> Task :Clean Execution phase, task':clean'Time:0 ms

> Task :app:Clean Execution phase, task':app:clean'Time:1Total ms execution time:325
Copy the code

Second, the Project

Each build.gradle has a corresponding Project instance, and in build.gradle, we usually configure a series of Project dependencies, such as this one:

implementation 'com. Making. Bumptech. Glide: glide: 4.8.0'
Copy the code

Dependent keywords like implementation and API are essentially a method call. Above, we use the implementation() method to pass in a map parameter with three pairs of key-values. The complete method is written as follows:

implementation group: 'com.github.bumptech.glide' name:'glide' version:'4.8.0'
Copy the code

When we use an AAR file for implementation or API dependencies, Gradle will find the corresponding aar file in the Repository. Your repository may include jCenter, Maven, etc. Each repository is actually a collection server that depends on many files, and they are categorized and stored according to the above group, name and version.

1. Decomposition of Project core API

There are many apis in Project, but according to theirProperties and usesWe can decompose it intoSix parts, as shown in the figure below:For the role of each part in a Project, we can first have a general understanding of it in order to establish an overall perception of the Project API system, as follows:

  • 1) Project API: Enables the current Project to operate its parent Project and manage its child projects.

  • 2) Task-related API: Provides the ability to add new tasks and manage existing tasks for the current Project.

  • 3) Project property-related Api: Gradle provides us with some Project properties in advance. The property-related Api gives us the ability to add additional properties to a Project.

  • 4) File-related Api: The Project File-related Api is mainly used to handle some files under our current Project.

  • Gradle Lifecycle API: This is the lifecycle API we explained above.

  • 6) Other apis: adding dependencies, adding configurations, importing external files, and so on.

Note: The API demo is performed in the app build.gradle file unless otherwise specified

2, Project API

The Project API documentation, we mainly introduce some commonly used apis

1, getRootProject method

Get the root Project object

println getRootProject()
// Run result
> Configure project :app
root project 'GradleDemo'
Copy the code

2. GetRootDir method

Obtain the path of the root directory folder

println getRootDir()
// Run result
> Configure project :app
/Users/ex-shenkai001/AndroidStudioProjects/Demo
Copy the code

3, getBuildDir method

Gets the build folder path of the current Project

println getBuildDir()
// Run result
> Configure project :app
/Users/ex-shenkai001/AndroidStudioProjects/Demo/app/build
Copy the code

4, getParent method

Gets the current parent Project object

println getParent()
// Run result
> Configure project :app
root project 'GradleDemo'
Copy the code

5, the getAllprojects method

Gets the current Project and its child Project objects. The return value is a Set

// Currently in the root project build.gradle file
println getAllprojects()

// Print the result
> Configure project :
[root project 'GradleDemo', project ':app']
Copy the code

We can also use its closure form

allprojects {
    println it
}

// Run result
> Configure project :
root project 'GradleDemo'
project ':app'

// We usually use the closure syntax in the root build.gradle configuration, as follows:
allprojects {
    repositories {
        google()
        jcenter()
    }
}
Copy the code

Note: The root Project and its subprojects form a tree, but the tree is limited to two levels

6. The getSubprojects method

Gets all child Project objects under the current Project. The return value is a Set

// Currently in the root project build.gradle file
println getSubprojects()
// Run result
> Configure project :
[project ':app']
Copy the code

We can also use its closure form

subprojects { 
    println it 
}
// Run result
> Configure project :
project ':app'
Copy the code

7, apply series method

The plug-in

// Reference third-party plug-ins
apply plugin: 'com.android.application'

// Reference the script file plug-in
apply from: 'config.gradle'
Copy the code

8, Configurations

Write some configuration related to Project, such as removing a dependency globally

configurations {
    all*.exclude group: 'group name'.module: 'Module name'
}
Copy the code

9, project series method

Specify the project instance, and then configure it in the closure

project("app") {
    apply plugin: 'com.android.application'
}
Copy the code

3. Extended attributes

Extended properties: make it easier to use one of our global properties. Similar to Java, static methods are defined in utility classes

1. Extend attribute definitions

We can define extended attributes in two ways:

1. Define extended attributes using the ext keyword

2. Define the extension properties in gradle.properties

1. Define extended attributes using the ext keyword

There are two syntax options for defining extended attributes via Ext:

// Currently under the root build.gradle
// Method 1: ext. attribute name
ext.test = 'erdai666'

// Option 2: ext followed by a closure
ext{
  test1 = 'erdai777'
}
Copy the code
2. Define the extension properties in gradle.properties

Use gradle.properties to define extended properties. Use the form key=value directly:

/ / in gradle. The properties
mCompileVersion = 27

// In build.gradle under app Moudle
compileSdkVersion mCompileVersion.toInteger()
Copy the code

2. Extended attribute invocation

Extended properties defined by ext can be called without the ext prefix

2. Extended properties defined by ext are also available through the Project object in which the extended properties are currently defined. Property name

3. Extension properties defined by Gradle. properties can be called directly by the property name.

/** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** *
println test
println test1
println test2

//2. Extended properties defined by ext are also available through the Project object in which the extended properties are currently defined. Attribute name call
println rootProject.ext.test
println rootProject.ext.test1
println test2

// The output of the above two methods is
> Configure project :app
erdai666
erdai777
erdai888
Copy the code

Note: Subprojects are inherited from the root Project, so attributes and methods defined in the root Project can be accessed by subprojects

3. Application of extended attributes

Generally, we will use extended properties to optimize build.gradle script files. For example, let’s optimize build.gradle in app as an example:

First take a look at what the app’s build.gradle looks like before the optimization

apply plugin: 'com.android.application'

android {
    compileSdkVersion 30

    defaultConfig {
        applicationId "com.dream.gradledemo"
        minSdkVersion 19
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation 'androidx. Appcompat: appcompat: 1.3.0'
    implementation 'com. Google. Android. Material: material: 1.4.0'
    implementation 'androidx. Constraintlayout: constraintlayout: 2.0.4'
    testImplementation 'the junit: junit: 4.13.2'
    androidTestImplementation 'androidx. Test. Ext: junit: 1.1.3'
    androidTestImplementation 'androidx. Test. Espresso: espresso - core: 3.4.0'
}
Copy the code

So let’s do this

Step 1: Create a script file config.gradle in the root directory to store the extended properties

ext{

    androidConfig = [
            compileSdkVersion : 30.applicationId : 'com.dream.gradledemo'.minSdkVersion : 19.targetSdkVersion : 30.versionCode : 1.versionName : '1.0'
    ]


    implementationLib = [
            appcompat : 'androidx. Appcompat: appcompat: 1.3.0'.material : 'com. Google. Android. Material: material: 1.4.0'.constraintlayout : 'androidx. Constraintlayout: constraintlayout: 2.0.4'
    ]

    testImplementationLib = [
            junit : 'the junit: junit: 4.13.2'
    ]


    androidTestImplementationLib = [
            junit : 'androidx. Test. Ext: junit: 1.1.3'.'espresso-core' : 'androidx. Test. Espresso: espresso - core: 3.4.0']}Copy the code

Step 2: Reference config.gradle at the root build.gradle

apply from: 'config.gradle'
Copy the code

Step 3: Call the extension properties in the app build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion androidConfig.compileSdkVersion

    defaultConfig {
        applicationId androidConfig.applicationId
        minSdkVersion androidConfig.minSdkVersion
        targetSdkVersion androidConfig.targetSdkVersion
        versionCode androidConfig.versionCode
        versionName androidConfig.versionName

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}}}def implementationLibMap = implementationLib
def testImplementationLibMap = testImplementationLib
def androidTestImplementationLibMap = androidTestImplementationLib

dependencies {
    implementationLibMap.each{k,v ->
        implementation v
    }

    testImplementationLibMap.each{k,v ->
        testImplementation v
    }

    androidTestImplementationLibMap.each{k,v ->
        androidTestImplementation v
    }
}
Copy the code

3) File operation API

// In build.gradle under rootProject
/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 1, the file method = = = = = = = = = = = = = = = = = = = = = = = = = = = = / / by the method of the file into a relative path, the return value is a file object
println file("config.gradle").text

// Pass in an absolute path via new File
def file = new File("/Users/ex-shenkai001/AndroidStudioProjects/Demo/config.gradle")
println file.text
// The print results are the same, as shown in the screenshot below
/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 2, files method = = = = = = = = = = = = = = = = = = = = = = = = = = = = / / by the method of files into multiple relative path, The return value is a ConfigurableFileCollection namely file collection
files("config.gradle"."build.gradle").each {
    println it.name
}
// Run result
> Configure project :
config.gradle
build.gradle
Copy the code

2. Copy Copies files

1. The Project object provides the copy method, which makes it easy to copy a file or folder

The copy method accepts a closure as a parameter CopySpec. CopySpec provides a variety of apis for file manipulation. See document Portal for details

CopySpec’s from and into methods are used below

Note: The arguments from and into are of type Object, so we can pass in a path or file. For example, we implement: copy the config.gradle file from the root directory to the app directory. As follows:

//1, pass the path
copy {
    from getRootDir().path + "/config.gradle"
    into getProjectDir().path
}

//2. Pass in the file
copy {
    from file('.. /config.gradle')
    into getProjectDir()
}

// The end result is that both methods can be copied successfully
Copy the code

2. Folder copy

For example, we implement: copy all files and folders in the gradle folder under the root directory to the Gradle folder under the app directory

copy {
    // Copy both files and folders
    // Here is to copy the apk directory generated under app moudle to
    // Build directory under the root project
    from file("build/outputs/apk")
    into getRootProject().getBuildDir().path + "/apk/"
    exclude {
        // Exclude files that do not need to be copied
    }
    rename {
        // Rename the copy file}}Copy the code

3. FileTree fileTree mapping

The Project object provides a fileTree method to convert a directory into a file tree, and then to do the related logical processing on the file tree. It takes a relative path similar to file/files

For example, we implement: traversing the gradle folder in the root directory, and printing the name of the file and folder

fileTree('.. /gradle/'){ FileTree fileTree ->
    fileTree.visit { FileTreeElement fileTreeElement ->
        println fileTreeElement.name
    }
}

// Run result
> Configure project :app
wrapper
gradle-wrapper.jar
gradle-wrapper.properties
Copy the code

We usually see a configuration statement like this in app build.gradle:

implementation fileTree(include: ['*.jar'].dir: 'libs')
Copy the code

He actually calls the overloaded method of fileTree that receives the Map argument:

ConfigurableFileTree fileTree(Map<String, ? > var1);Copy the code

Import all jar packages in the libs folder in the current project directory

4) Other apis

1. Buildscript interpretation

When we create an Android project, we usually see this configuration at the root of build.gradle:

buildscript {
    // Plugin repository address
    repositories {
        google()
        mavenCentral()
    }
  
    // Plug-in dependencies
    dependencies {
        classpath "Com. Android. Tools. Build: gradle: 2"}}Copy the code

In fact, the complete code above is written as follows:

buildscript { ScriptHandler scriptHandler ->
    scriptHandler.repositories { RepositoryHandler repositoryHandler ->
        repositoryHandler.google()
        repositoryHandler.mavenCentral()
    }
  
    scriptHandler.dependencies { DependencyHandler dependencyHandler ->
        dependencyHandler.classpath "Com. Android. Tools. Build: gradle: 2"}}Copy the code

Do you wonder why these parameters can be removed and simplified to the above?

To understand this, we need to understand closures:

First, there are three objects in the closure, owenr this delegate. These objects have properties and methods that we can call without writing them

The order in which these three objects are called depends on the closure’s delegate policy. Normally, we operate on the delegate and modify its delegate policy

In fact, Gradle modifydelegate_first (const DELEGATE_FIRST) to DELEGATE_FIRST (const DELEGATE_FIRST), so that the DELEGATE_FIRST (const DELEGATE_FIRST) is removed from the DELEGATE_FIRST (const DELEGATE_FIRST). I won’t analyze the source code here

2. Select dependencies under app Moudle

Unlike dependencies in buildScript, which configur our Gradle project’s plug-in dependencies, dependencies in app Moudle add third party dependencies to the application. The example code for using the dependencies of exclude and transitive is as follows:

implementation(rootProject.ext.dependencies.glide) {
        // Exclude dependencies: this is generally used to resolve problems related to resource and code conflicts
        exclude module: 'support-v4' 
        // Pass the dependency: A => B => C, B uses the dependency of C,
        // If A is dependent on B, then A can use B
        // by default, all dependencies in C are disabled (false)
        transitive false 
}
Copy the code

5) exec external command execution

The Project object provides the exec method to facilitate the execution of external commands

To move a folder in Linux, run the following command:

Mv-f Source file path Destination file pathCopy the code

Now let’s do this in Gradle

For example, we implement: using an external command, move our apk directory to the root of the project, as follows:

task taskMove() {
    doLast {
        // Execute in gradle's execution phase
        def sourcePath = buildDir.path + "/outputs/apk"
        def destinationPath = getRootDir().path
        def command = "mv -f $sourcePath $destinationPath"
        exec {
            try {
                executable "bash"
                args "-c", command
                println "The command execute is success"
            } catch (GradleException e) {
                e.printStackTrace()
                println "The command execute is failed"}}}}Copy the code

3. Task Introduction

Only tasks can be executed during Gradle’s execution phase (which is essentially a series of actions in the executing Task).

1. Introduction to doFirst and doLast

An Action is essentially an execution Action that is executed only when we execute the current Task. Gradle execution is essentially a series of actions that are executed in each Task

DoFirst and doLast are the two actions that Task gives us

DoFirst indicates the Action that was called at the beginning of the Task execution

DoLast indicates the Action that is called when the task finishes executing

Note that doFirst and doLast can be added multiple times for execution.

task shenkai{
    println 'task start... '

    doFirst {
        println 'doFirst1'
    }

    doLast {
        println 'doLast1'
    }

    doLast {
        println 'doLast2'
    }

    println 'task end... '
}

// Execute the current task
./gradlew shenkai

// Print the result as follows
> Configure project :app
task start...
task end...

> Task :app:shenkai
doFirst1
doLast1
doLast2
Copy the code

From the above printed results we can find that

1, println ‘task start… ‘, println ‘task end… ‘These two sentences are executed during Gradle configuration

DoFirst, doLast code is executed in Gradle execution phase, the erdai task is executed

This also verifies the conclusion I said at the beginning: Gradle configuration phase, except for the Action of the Task written in the code will be executed

2. Define and configure tasks

Since tasks and projects are related, projects provide a number of methods for creating tasks. Here are some common ones:

// create a Task named task1
task task1

//2, create a Task named task2 and configure it accordingly with the closure
task task2{
    // Specify the group of tasks
    group 'erdai666'
  
    doFirst{
    
    }
}

//3, create a Task named task3, which inherits from Copy Task and depends on task2
task task3(type: Copy){
    dependsOn "task2"
    doLast{
    
    }
}

//4, create a Task named task4 and specify the group and description
task task4(group: "erdai666".description: "task4") {
    doFirst {
        
    }
    
    doLast {
        
    }
}

//5. Create a Task named task5 in the TaskContainer of the Project object
tasks.create("task5"){

}

//6. Create a Task named task6 in the TaskContainer of the Project object
// A different overloaded method is called than 5
tasks.create(name: "task6"){

}
Copy the code

The attribute of the Task

In “()” we can configure a set of attributes for each task, as follows:

project.task('JsonChao3'.group: "JsonChao".description: "my tasks".dependsOn: ["JsonChao1"."JsonChao2"] ).doLast {
    println "execute JsonChao3 Task"
}
Copy the code

Currently officially supported attributes can be summarized in the following table:

The selection describe The default value
“name” The task name None, must be specified
“type” Task Class to be created DefaultTask
“action” Closure or Action that needs to be executed when task executes null
“overwrite” Replace an existing task false
“dependsOn” The collection of tasks on which the task depends []
“group” Group to which the task belongs null
“description” Description of a task null
“constructorArgs” Parameter passed to the Task Class constructor null
#### Use ext to customize the required attributes for the task

Of course, in addition to using existing attributes, we can also use Ext to customize the attributes required for the task, as follows:

task Gradle_First() {
    ext.good = true
}

task Gradle_Last() {
    doFirst {
        println Gradle_First.good
    }
    doLast {
        println "I am not $Gradle_First.name"}}Copy the code

Task Type Description

By default, we create tasks that inherit from DefaultTask. We can use the type property to make them inherit from another class. We can also use the extends keyword to make them inherit from another class.

// 1. Delete the build file from the root directory
task deleteTask(type: Delete) {
    delete rootProject.buildDir
}

// This is specified with the extends keyword
class DeleteTask extends Delete{

}
DeleteTask deleteTask = tasks.create("deleteTask",DeleteTask)
deleteTask.delete(rootProject.buildDir)


// 2. Copy = Copy
task copyTask(type: Copy) {
    / /...
}

// This is specified with the extends keyword
class CopyTask extends Copy{
    / /...
}
Copy the code

TaskContainer introduction

TaskContainer is a Task container that is used by Project objects to manage tasks. TaskContainer is a Task container that is used to manage tasks.

/ / search task
findByPath(path: String): Task 
getByPath(path: String): Task
getByName(name: String): Task

/ / create a task
create(name: String): Task
create(name: String, configure: Closure): Task 
create(name: String, type: Class): Task
create(options:Map<String, ? >): Task create(options:Map<String, ? >, configure: Closure): Task// Listen when a task is added to a TaskContainer
whenTaskAdded(action: Closure)
Copy the code

3. Detailed explanation of Task execution

Tasks usually use doFirst and doLast to perform operations during execution. The sample code looks like this:

// Use Task to perform operations during the execution phase
task myTask3(group: "MyTask".description: "task3") {
    println "This is myTask3"
    doFirst {
        / / second
        println "This group is 2"
    }

    doLast {
        / / old
        println "This description is 3"}}// You can also add a task using the taskname.doxxx method
myTask3.doFirst {
    // This mode is executed first => boss
    println "This group is 1"
}
Copy the code

Task Execution

Next, we will use doFirst and doLast to calculate the build time. The complete code is as follows:

// Task execution practice: calculate the time elapsed during build execution
def startBuildTime, endBuildTime
// 1, after the Gradle configuration phase is complete,
// This ensures that all tasks to be executed are configured
this.afterEvaluate { Project project ->
    // 2. Find the first task executed by the current project, that is, preBuild task
    def preBuildTask = project.tasks.getByName("preBuild")
    preBuildTask.doFirst {
        // 3, obtain the timestamp when the first task started executing
        startBuildTime = System.currentTimeMillis()
    }
    // 4. Find the last task executed under the current project, that is, build task
    def buildTask = project.tasks.getByName("build")
    buildTask.doLast {
        // 5, obtain the timestamp of the last task
        endBuildTime = System.currentTimeMillis()
        // 6, output the build execution time
        println "Current project execute time is ${endBuildTime - startBuildTime}"}}// Execute the build task
./gradlew build

// Print the result
Current project execute time is 21052
Copy the code

Task dependencies and execution order

In Gradle, there are three ways to specify Task execution order: a dependsOn method

2. Input and output by Task

3. Specify the execution order through the API

1, dependsOn strong dependency mode

DependsOn can be divided into static and dynamic dependencies

  • DependsOn: When a Task is created, a dependsOn Task is specified
  • DependsOn: when a Task is created, it is not known which Task is to be dependent on
/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = static rely on = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
task taskA{
    doLast {
        println 'taskA'
    }
}

task taskB{
    doLast {
        println 'taskB'
    }
}

task taskC(dependsOn: taskA){DependsOn :[taskA,taskB] dependsOn:[taskA,taskB]
    doLast {
        println 'taskC'}}/ / taskC execution
./gradlew taskC

// Print the result
> Task :app:taskA
taskA

> Task :app:taskC
taskC
Copy the code

When we execute taskC, we rely on taskA, so taskA will execute first, before executing taskC

Note: When a Task is dependent on multiple tasks, if there is no dependency between the dependent tasks, then the order of execution is random, as follows:

task taskC(dependsOn:[taskA,taskB]){
    doLast {
        println 'taskC'}}Copy the code

TaskA and taskB are executed in a random order.

/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = in the dynamic dependence = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
// Task dynamic dependency mode
task lib1 {
    doLast{
        println 'lib1'
    }
}
task lib2 {
    doLast{
        println 'lib2'
    }
}
task lib3 {
    doLast{
        println 'lib3'}}// Dynamically specify that taskX depends on all tasks starting with lib
task taskDynamic{
    // Dynamically specify dependencies
    dependsOn tasks.findAll{ Task task ->
        return task.name.startsWith('lib')
    }

    doLast {
        println 'taskDynamic'}}/ / taskDynamic execution
./gradlew taskDynamic

// Print the result
> Task :app:lib1
lib1

> Task :app:lib2
lib2

> Task :app:lib3
lib3

> Task :app:taskDynamic
taskDynamic
Copy the code

2. Specify the execution order based on the Task input and output

When a parameter is an input parameter to TaskA and an output parameter to TaskB, TaskA executes TaskB first. That is, the output Task executes before the input Task

However, in the actual test process, I found that the input Task will be executed first, and then the output Task will be executed, as follows:

ext {
    testFile = file("${projectDir.path}/test.txt")
    if(testFile ! =null| |! testFile.exists()){ testFile.createNewFile() } }/ / output Task
task outputTask {
    outputs.file testFile
    doLast {
        outputs.getFiles().singleFile.withWriter { writer ->
            writer.append("erdai666")
        }
        println "OutputTask Execution ends"}}/ / input Task
task inputTask {
    inputs.file testFile
    doLast {
        println "Read the file content: ${inputs. Files. SingleFile. Text}"
        println "InputTask execution ends"}}/ / test Task
task testTask(dependsOn: [outputTask, inputTask]) {
    doLast {
        println "TestTask1 execution ends"}}/ / testTask execution
./gradlew testTask

// In theory, outputTask is executed first, inputTask is executed, and testTask is executed last
// But actually print the result
> Task :app:InputTask Read file contents: inputTask End >Task :app:OutputTask outputTask End >Task :app:TestTask testTask1 No further action is requiredCopy the code

I ended up specifying specific dependencies on inputTask to get the desired effect:

task inputTask(dependsOn: outputTask) {
    inputs.file testFile
    doLast {
        println "Read the file content: ${inputs. Files. SingleFile. Text}"
        println "InputTask execution ends"}}// The modified print result
> Task :app:OutputTask outputTask End >Task :app:InputTask Read the file content: erdai666 inputTask End >Task :app:TestTask testTask1 No further action is requiredCopy the code

3. Specify the execution order through the API

The apis for specifying Task execution order are:

MustRunAfter: Specifies which Task must be executed after completion

ShouldRunAfter: Similar to mustRunAfter, except that it is not mandatory and not commonly used

FinalizeBy: Specifies the Task to be executed after the current Task completes execution

Let’s do this in code:

//======================================= mustRunAfter ===========================
task taskX{
    doLast {
        println 'taskX'
    }
}

task taskY{
    mustRunAfter taskX
    doLast {
        println 'taskY'
    }
}

task taskXY(dependsOn: [taskX,taskY]){
    doLast {
        println 'taskXY'}}/ / taskXY execution
./gradlew taskXY

// Print the result
> Task :app:taskX
taskX

> Task :app:taskY
taskY

> Task :app:taskXY
taskXY

//======================================= finalizeBy ===========================
task taskI{
    doLast {
        println 'taskI'
    }
}

task taskJ{
    finalizedBy taskI
    doLast {
        println 'taskJ'
    }
}


task taskIJ(dependsOn: [taskI,taskJ]){
    doLast {
        println 'taskIJ'}}/ / taskIJ execution
./gradlew taskIJ

// Print the result
> Task :app:taskJ
taskJ

> Task :app:taskI
taskI

> Task :app:taskIJ
taskIJ
Copy the code

4. The custom Task is attached to the Android application build process

1) Task dependency plug-in introduction

We can introduce the following plug-in to view a dependency of a Task

//1. Add the following code to the root build.gradle
buildscript {
    repositories {
      	/ /...
        maven{
           url "https://plugins.gradle.org/m2/"
        }
    }
    dependencies {
      	/ /..
        classpath "gradle.plugin.com.dorongold.plugins:task-tree:1.5"}}// 2. Apply the plugin in the app build.gradle
apply plugin: com.dorongold.gradle.tasktree.TaskTreePlugin

/** * 3. Run the./gradlew < task name > taskTree --no-repeat command to view the build task
./gradlew build taskTree --no-repeat
Copy the code

After the above three steps, let’s take a look at the dependency graph and cut only parts:

2) The custom Task is attached to the Android build process

As we know, Gradle executes tasks and their dependencies in the execution phase. For example, as shown in the build Task dependency diagram in the screenshot above, Gradle executes tasks in an orderly manner according to this dependency diagram.

So what if I want to hook my own custom Task into the build process?

1. Specified by dependsOn

Note: When using dependsOn alone, the Task in the build process must be dependent on our custom Task or our Task will not take effect

The following code demonstrates this:

task myCustomTask{
    doLast {
        println 'This is myCustomTask'
    }
}

afterEvaluate {
    //1, find the required build process Task
    def mergeDebugResources = tasks.findByName("mergeDebugResources")
    //2. Specify by dependsOn
    mergeDebugResources.dependsOn(myCustomTask)
  
    // The custom Task will not take effect if the following rule is used
    //myCustomTask.dependsOn(mergeDebugResources)
}
Copy the code

So let’s verify that

First take a look at the Task dependency diagram:Our custom Task is attached to the mergeDebugResources

If we build our Task, we can see that our Task has been executed:

2. Specify by finalizedBy

After a Task is executed, specify the Task to be executed

task myCustomTask{
    doLast {
        println 'This is myCustomTask'
    }
}

afterEvaluate {
    def mergeDebugResources = tasks.findByName("mergeDebugResources")
    // Hang myCustomTask after mergeDebugResources to execute
    mergeDebugResources.finalizedBy(myCustomTask)
}
Copy the code
3. Specify through mustRunAfter with dependsOn

Insert a custom Task between two tasks

task myCustomTask{
    doLast {
        println 'This is myCustomTask'
    }
}

afterEvaluate {
    // Insert myCustomTask between mergeDebugResources and processDebugResources
    def processDebugResources = tasks.findByName("processDebugResources")
    def mergeDebugResources = tasks.findByName("mergeDebugResources")
    myCustomTask.mustRunAfter(mergeDebugResources)
    processDebugResources.dependsOn(myCustomTask)
}
Copy the code

The above Task depends on the change process:

processDebugResources -> mergeDebugResources ===> processDebugResources -> myCustomTask -> mergeDebugResources

Gradle related commands are introduced

1) View all Project objects of the Project

./gradlew project
Copy the code

2) Check all tasks in the module

./gradlew $moduleName:tasks

/ / demo
// View all tasks in the app
./gradlew app:tasks

// Check all tasks of the root Project
./gradlew tasks
Copy the code

3), execute a Task

./gradlew $taskName

// Execute build Task
./gradlew build
Copy the code

4) Check the dependencies of third-party libraries under Module

./gradlew $moduleName:dependencies

// Check the dependencies of third-party libraries in the app
./gradlew app:dependencies
Copy the code

reference

  • Gradle (2) Explores Gradle techniques (Sweereferees jsonchao and Renxhui)
  • Gradle core decrypt (Gradle core decrypt)
  • Gradle Learning Series ii: Gradle Core Exploration
  • Gradle infrastructure builds lifecycle and Hook technologies