As we know, the Gradle build tool is very flexible. It provides a series of apis that allow us to modify or customize the build process of a project, insert our own tasks and perform operations such as multichannel packaging, ASM code weaving and resource detection during the build process of the project.

To implement these functions, we first need to understand Gradle’s building process, know what Gradle does at each stage, and add what events we need to do at each stage. Then we can insert the code we want to execute through the Api provided by Gradle. Therefore, understanding Gradle’s lifecycle and Hook points can help us to sort out and extend the build process of a project.

The Gradle build process has a fixed life cycle. Understanding Gradle’s life cycle and Hook points will help you to sort out and extend the build process of your project.

Gradle build lifecycle


The Gradle build process has a fixed lifecycle, which is:

  1. Initialization phase
  2. The configuration phase
  3. Execution phase

Here’s a closer look at what these three phases do.

1. Initialization phase


The main task of the initialization phase is to create the Project hierarchy and create a Project instance object for each Project.

In the initialization phase, the settings.gradle script is executed and the include information in Settings. gradle is read to create a Project object for each Project (the build.gradle script file). The result is a project hierarchy. A settings.gradle script corresponds to a Settings object (created when Gradle is initialized), and the include tag that we most commonly use to declare the hierarchy of a project is a method under the Settings class. The Settings class also has the following methods, which can be accessed directly from the settings.gradle file:

1-1. Apply modules of other projects

For example, include and project methods can be used to reference any project module in any location:

include ':myjson' // The name of the module to be built
project(':myjson').projectDir = file('/Users/WorkSpace/AndroidDemo/MyJson/myjson')
Copy the code
  • Include: Specifies the name of the module that you are involved in building. The module name must be preceded by a colon (:).
  • The project method: loads the specified module and sets a project path for the module. The parameters must be the same as the include parameter.

In this way, the project can refer to modules in other locations. If the reference is a library module, then the project module can rely on the library module. For example:

implementation project(":myjson")
Copy the code

1-2. Listen for initialization

In the Settings class’s method list, there is a getGradle() method that returns a Gradle object. This Gradle object allows us to listen for callbacks to the various lifecycle methods during the Gradle build process. For example, in settings.gradle, add the following listener:

gradle.addBuildListener(new BuildListener() {
  
    void buildStarted(Gradle var1) {
        println 'buildStarted()-> Start build ' 
    }

    void settingsEvaluated(Settings var1) {
        println 'settingsEvaluated()-> settingsEvaluated() '
        // var1.gradle.rootProject: error when you access the Project object, the initialization of the Project is not complete
    }

    void projectsLoaded(Gradle var1) {
        println 'projectsLoaded()-> Project structure loaded (initialization phase finished) '
        println 'projectsLoaded()-> End of initialization, accessible root project: ' + var1.gradle.rootProject
    }

    void projectsEvaluated(Gradle var1) {
        println 'projectsEvaluated()-> All project evaluations completed (end of configuration phase) '
    }

    void buildFinished(BuildResult var1) {
        println 'buildFinished()-> buildFinished '}})Copy the code

Gradle can also be obtained in the build.gradle file, but if you add the above listener event to the build.gradle file, buildStarted, The settingsEvaluated and projectsLoaded methods are not called back because they are executed during the initialization phase when the settings.gradle file is executed, but the other two methods are.

In the root project build.gradle file, add the following code to better observe the callback time of the listener event added above:

allprojects {
    afterEvaluate {
        println ${name}: configuration complete}}Copy the code

The following information is displayed:

ProjectsLoaded ()-> settingsEvaluated()-> Settings evaluation completed (settins.gradle code execution completed) projectsLoaded()-> project structure loading completed (initialization phase ended) projectsLoaded()-> initialization completed, Root project 'KotlinLearning' Configure project Configure Project: App. Configure Project: KotlinLearning. KotlinLearning ProjectsEvaluated ()-> all project evaluations completed (end of configuration phase) buildFinished()-> buildFinishedCopy the code

2. Configuration phase


The tasks in the configuration phase are to execute the build.gradle script under each item, complete the configuration of the Project, and construct the Task dependency diagram so that the Task can be executed according to the dependency in the execution phase.

2-1. Code executed in the configuration phase

The configuration phase is also the most common part of the build phase, such as the external build plugin apply Plugin: ‘com.android.application’, configure the plugin property Android {compileSdkVersion 25… }, 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 executed during the configuration phase includes:

  • Various statements in build.gralde

  • closure

  • Configuration section statement in Task

Verify: add the following code to 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

The following information is displayed:

Configuration phase of build.gradle

The code executed in Dependencies

I execute the configuration code 2 in Task in order

< span style = “box-sizing: border-box; border-box: border-box; border-box: border-box; border-box: border-box; border-box: border-box;

It is important to note that with any Gradle command, the initialization and configuration code will be executed.

2-2. Task dependency configuration is complete

Another important Task in the configuration phase is to build a directed acyclic graph of Task dependencies. To put it simply, it is to assign an execution order to all tasks. During the execution phase, all tasks are executed in that order.

TaskExecutionGraph: TaskExecutionGraph: TaskExecutionGraph: TaskExecutionGraph: TaskExecutionGraph: TaskExecutionGraph: TaskExecutionGraph: TaskExecutionGraph: TaskExecutionGraph

  • void whenReady(Closure var1)
    Copy the code
  • void addTaskExecutionGraphListener(TaskExecutionGraphListener var1)
    Copy the code

In the build.gradle file, add the following code:

gradle.getTaskGraph().whenReady {
    println "WhenReady Task dependencies are built, size=${it.alltasks.size ()}"
    it.allTasks.forEach { task ->
        println "${task.name}"
    }
}

gradle.getTaskGraph().addTaskExecutionGraphListener(new TaskExecutionGraphListener() {
    @Override
    void graphPopulated(TaskExecutionGraph graph) {
        println Size =${graph.alltasks.size ()}"
        graph.allTasks.forEach { task ->
            println "${task.name}"}}})Copy the code

Click the Run button, and when the app is running, you can print out a list of tasks

WhenReady Task dependencies are built, size=40 preBuild preDebugBuild compileDebugAidl compileDebugRenderscript generateDebugBuildConfig checkDebugAarMetadata generateDebugResValues generateDebugResources mergeDebugResources createDebugCompatibleScreenManifests extractDeepLinksDebug processDebugMainManifest processDebugManifest processDebugManifestForPackage processDebugResources compileDebugKotlin javaPreCompileDebug compileDebugJavaWithJavac compileDebugSources mergeDebugNativeDebugMetadata mergeDebugShaders compileDebugShaders generateDebugAssets mergeDebugAssets compressDebugAssets processDebugJavaRes mergeDebugJavaResource checkDebugDuplicateClasses dexBuilderDebug desugarDebugFileDependencies mergeExtDexDebug mergeDexDebug mergeDebugJniLibFolders mergeDebugNativeLibs stripDebugDebugSymbols validateSigningDebug WriteDebugAppMetadata writeDebugSigningConfigVersions packageDebug assembleDebug graphPopulated Task dependencies to build complete size = 40 PreBuild… assembleDebug

3. Execution phase


Execution phase is to execute related tasks based on the Task dependencies built in the configuration phase.

When we run a project, Gradle executes tasks in turn according to their dependencies. You can also use the Gradle command to execute specific tasks. For example, to execute a build Task, type the following command in the console:

./gradlew build 
Copy the code

Build is the task name.

Gradle Hook point


Gradle provides a number of interface callbacks so that we can modify the behavior of the build process. The overall process is shown below:

Method description:

  • Gradle#settingsEvaluated method: Same as BuildListener’s settingsEvaluated execution time, it needs to be added to settings.gradle file, otherwise invalid.
  • Gradle#projectsLoaded method: The execution time of projectsLoaded is the same as BuildListener’s projectsLoaded. It needs to be added in settings.gradle file, otherwise it will not work.
  • Gradle#beforeProject method, executed before each project configuration,
  • The Project#beforeEvaluate method is executed before calling the method’s Project configuration. If called in a module’s build.gradle file, it will not be executed because by the time of build.gradle, The time point for beforeEvaluate execution has passed.
  • The Gradle#afterProject method is executed after each project has been configured. This method will be called even if there is an error in the construction process.
  • The Project#afterEvaluate method is called after the Project configuration that called it has been executed. At this point, the Project configuration is complete and all tasks can be retrieved in the method callback.
  • The Gradle#projectsEvaluate method is executed when all projects have been configured at the same time as the BuildListener’s projectsEvaluated method.

3. Specify the Task execution order


In Gradle, there are three ways to specify the order of execution of tasks:

  • DependsOn Indicates the strong dependency mode
  • Input and output through Task
  • Specify the order of execution through the API

3-1. The value is specified in dependsOn mode

DependsOn dependsOn dependsOn dependsOn dependsOn dependsOn dependsOn dependsOn dependsOn

  • DependsOn: when a task is created, it is defined as a dependsOn parameter or a dependsOn method to specify which task it dependsOn.

    A dependsOn, finalizedBy method is provided to manage a dependsOn, finalizedBy method. This means that a task cannot be executed independently and another task must be executed before or after the execution of this task.

  • DependsOn method: When a Task is created, it does not know which Task is to be dependent on

The sample code looks like this:

Statically specified dependencies

When defining a Task, specify a dependsOn parameter for the Task with the value of the name of the other dependent Task:

task taskX {
  doLast{
    println 'taskX'
  }
}

task taskY {
  doLast{
    println 'taskY'}}task taskZ(dependsOn:taskX) { DependsOn :[taskX,taskY] dependsOn:[taskX,taskY]
  doLast{
    println 'taskZ'}}// When we execute taskZ, we depend on taskX, so taskX is executed before: taskZ is executed
Copy the code

In addition to specifying the dependency of a Task when a Task is defined, you can also specify a dependency for a Task via the dependsOn method:

task taskZ {// Define Task without specifying dependencies
  doLast{
    println 'taskZ'}}// The dependsOn method of a task is dependsOn.
taskZ.dependsOn(taskX,taskY)
Copy the code

When a task depends on multiple tasks, the execution order of the dependent tasks is random if there is no dependency.

Dynamically adding dependencies

When a Task is defined, it does not know what Task it depends on. In the configuration phase, it finds out the Task that meets the conditions and relies on it.

task lib1 {
  doLask{
    println 'lib1'
  }
}
task lib2 {
  doLask{
    println 'lib2'
  }
}
task lib3 {
  doLask{
    println 'lib3'}}// Dynamically specify that taskX depends on all tasks starting with lib
task taskX{
  // Dynamically specify dependencies
  dependsOn this.tasks.findAll{ task->
    return task.name.startsWidth('lib')
  }
  doLast {
    println 'taskZ'}}Copy the code

3-2. Specify by Task input/output

When a parameter is used as an output parameter for TaskA, it is also used as an input parameter for TaskB. So when TaskB is executed TaskA is executed first. That is, the output Task executes before the input Task. Such as:


ext {
    testFile = file("${this.buildDir}/test.txt")}// The producer Task
task producer {
    outputs.file testFile
    doLast {
        outputs.getFiles().singleFile.withWriter { writer ->
            writer.append("I love China")
        }
        println "Execution of producer Task ends"}}// Consumer Task
task consumer {
    inputs.file testFile
    doLast {
        println "Read the file content: ${inputs. Files. SingleFile. Text}"
        println "Completion of Consumer Task"}}task testTask(dependsOn: [producer, consumer]) {
    doLast {
        println "End of test Task execution"}}Copy the code

The testFile is the output parameter of the producer and the input parameter of the consumer, so the producer takes precedence over the consumer.

3-3. Specify the order of execution through the API

There are other ways to specify the order of Task execution:

  • MustRunAfter: Specifies which Task must be executed after completion, such as taskA. MustRunAfter (taskB), which means taskA must be executed after taskB.
  • ShouldRunAfter: Similar to mustRunAfter, the difference is that it is not mandatory. Not commonly used.
  • FinalizedBy: Executes the specified Task after the Task has finished. For example, taskA. FinalizedBy (taskB), the taskB task is executed after taskA completes.

Example code:

  • MustRunAfter specifies the order in which tasks are executed:

    task taskA {
        doLast {
            println "TaskA execution"
        }
    }
    
    task taskB {
        mustRunAfter(taskA)
        doLast {
            println "TaskB execution"
        }
    }
    
    task testAB(dependsOn: [taskA, taskB]) {
        doLast {
            println "TestAB execution"}}Copy the code

    Run the task testAB, and the following information is displayed:

ShouldRunAfter is similar to mustRunAfter, which is no longer tested.

  • FinalizedBy specifies the order in which tasks are executed

    task taskA {
        doLast {
            println "TaskA execution"
        }
    }
    
    task taskB {
        finalizedBy(taskA)
        doLast {
            println "TaskB execution"
        }
    }
    
    task testAB(dependsOn: [taskA, taskB]) {
        doLast {
            println "TestAB execution"}}Copy the code

    The same code replaces mustRunAfter with finalizedBy, which means that taskA is executed after taskB. Run the testAB task and the following information is displayed:

    This is consistent with what we expected.

4. Custom tasks are attached to the build process


1. Print the Task dependencies

If you are not familiar with the task dependencies of a build process, you can use a third-party plug-in to view this by adding the following code to the root project’s build.gradle:

buildscript {
  repositories {
    maven {
      url "https://plugins.gradle.org/m2/"
    }
  }
  dependencies {
    classpath "gradle.plugin.com.dorongold.plugins:task-tree:1.5"}}// Apply the plugin
apply plugin: com.dorongold.gradle.tasktree.TaskTreePlugin
Copy the code

Then run./gradlew < Task name > taskTree –no-repeat to see the dependencies of the specified Task, such as app:assembleDebug Task in an Android build:

2, custom Task hang up to build process

As we know, Gradle builds by executing a series of tasks, each Task completes its own unique work, according to the dependencies of the Task, to execute the next Task. Example: preBuild(Task executed before starting build)->mergeDebugResources(Task that merges resource files)->assembleDebug (Task that generates debug packages).

What if we want to insert our own Task into the build process and automatically execute our Task at run time? At this point, we can add our Task to the build process by specifying the order of execution of the Task. Specifically, we can specify which Task needs to be inserted after or before the Task, then find the Task, and insert our own Task before or after the Task.

You can use the following methods to insert a custom Task into a specified Task

  • DependsOn or finalizedBy method
    • The dependsOn method is used separately. It is best if the Task of the compilation process is dependent on its own Task, otherwise it will not work
    • FinalizedBy: Can be used separately to specify that you execute your own Task after the execution of a Task
  • Specified by mustRunAfter together with dependsOn

For example:

2-1. A custom Task (dependsOn) is executed before a Task

afterEvaluate {
  // 1. Find the Task that depends on the build process of your Task
    def mergeResourcesTask = tasks.findByName("mergeDebugResources")
    println "mergeResourcesTask=$mergeResourcesTask"
  // 2. Insert before the specified Task using the dependsOn method
    mergeResourcesTask.dependsOn(checkBigImage)
}
Copy the code

Task dependency diagram before inserting custom Task:

Task dependency diagram after inserting custom Task:

The mergeDebugResources Task does depend on the checkBigImage Task, so when you run the build app, when you execute the mergeDebugResources Task, Let’s go ahead and execute the checkBigImage task.

2-2. Execute a custom Task: finizedBy after a Task

afterEvaluate {
  // 1. Find the Task that depends on the build process of your Task
    def mergeResourcesTask = tasks.findByName("mergeDebugResources")
    println "mergeResourcesTask=$mergeResourcesTask"
  // 2. Insert after the specified Task using the finalizedBy method
    mergeResourcesTask.finalizedBy(checkBigImage)
}
Copy the code

2. Insert a custom Task (mustRunAfter) with dependsOn between two tasks

afterEvaluate {

  // 1. Find the Task to be mounted
    def mergeResourcesTask = tasks.findByName("mergeDebugResources")
    def processDebugResourcesTask = tasks.findByName("processDebugResources")

  // 2. Let the custom Task execute after the mergeDebugResources Task and before the processDebugResources Task
    checkBigImage.mustRunAfter(mergeResourcesTask)
    processDebugResourcesTask.dependsOn(checkBigImage)
}
Copy the code

Reference:

  • Android’s elegant packaging automates all RES resources

  • Gradle core Configuration