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:
- Initialization phase
- The configuration phase
- 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