preface

To be a good Android developer, you need a complete set ofThe knowledge systemHere, let us grow together into what we think ~.

On the face of it, Gradle is a powerful build tool, and many articles treat Gradle just as a tool. However, Gradle is more than just a powerful build tool. It looks more like a programming framework. The composition of Gradle can be broken down into three aspects:

  • 1),Groovy core syntax:This includes basic Groovy syntax, closures, data structures, object orientation, and more.
  • 2),Android DSL (Build Scrpit Block):Android plugins are unique to Gradle. We can do different things in different build scrpit blocks.
  • 3),Gradle API:Including Project, Task, Setting, etc..

As you can see, Gradle’s syntax is based on Groovy and it also has its own API, so we can think of Gradle as a programming framework that allows us to implement all of our requirements in the process of building projects. It is important to note that in order to use Gradle as you would like, you must have a good command of Groovy. If you are not familiar with Groovy, check out the article “Exploring Gradle Automation Building Techniques in Depth”.

Note that Groovy is a language and DSL is a domain-specific configuration file. Gradle is a framework tool based on Groovy and Gradlew is a compatible wrapper for Gradle.

Advantages of Gradle

1. Better flexibility

In terms of flexibility, Gradle provides a set of apis that allow you to modify or customize your project’s build process, as opposed to build tools like Maven and Ant. For example, you can use Gradle to dynamically change the generated APK package name, but if you use Maven, Ant, etc., you must wait for the generated APK to change the APK package name manually.

2. Finer granularity

In terms of granularity, with Maven, Ant, and other build tools, our source code and build scripts are separate and we don’t know what’s going on inside. Gradle, on the other hand, executes source code compilation, resource compilation, and APK generation one after another.

In addition, Gradle builds with granularity down to each task. And all of its Task source is open source, after we master the whole packaging process, we can modify its Task to dynamically change its execution process. For example, in the implementation of Tinker framework, it dynamically modifies Gradle’s packaging process to generate APK, and also generates various patch files.

3. Better scalability

Gradle supports a plug-in mechanism for extensibility, so we can reuse plug-ins as easily as we reuse libraries.

4. Greater compatibility

Gradle is not only powerful on its own, it is also compatible with all Maven and Ant features. In other words, Gradle takes the best of all build tools.

As you can see, Gradle’s advantages over other build tools are self-evident, and the core reason is that Gradle is a programming framework.

Gradle build lifecycle

The Gradle build process is divided into three parts: the initialization phase, the configuration phase and the execution phase. Let’s take a closer look at each of them.

1. Initialization phase

In this stage, the include information in setting.gradle in the root Project is read to determine how many projects are added to the build. Then, a corresponding Project instance is created for each Project (build.gradle script file). Finally, a hierarchy of projects is formed. Gradle script. A Settings. gradle script corresponds to a Settings object. The most common way to declare a project hierarchy is to include a method in the Settings object. When Gradle initializes, a Settings instance object is constructed to perform the initial configuration of each Project.

settings.gradle

In settings.gradle, we can add life cycle node listeners during gradle build as follows:

include ':app'
gradle.addBuildListener(new BuildListener() {
    void buildStarted(Gradle var1) {
        println 'Start building'
    }
    void settingsEvaluated(Settings var1) {
        // var1.gradle.rootProject
        // Because the initialization of Project has not been completed.
        println 'Settings evaluation completed (code executed in settings.gradle)'
    }
    void projectsLoaded(Gradle var1) {
        println 'Project structure loading completed (end of initialization phase)'
        println 'Initialization completed, root project accessible:' + var1.gradle.rootProject
    }
    void projectsEvaluated(Gradle var1) {
        println 'All project evaluations completed (end of configuration phase)'
    }
    void buildFinished(BuildResult var1) {
        println 'Build complete'}})Copy the code

After writing the appropriate Gradle lifecycle listening code, you can see the following information in the Build output screen:

Executing tasks: [clean, :app:assembleSpeedDebug] in project
/Users/quchao/Documents/The evaluation is complete (the code in settins.gradle is finished) the project structure is loaded (the initialization stage is finished) the initialization is complete, and you can access the root project: root project'Awesome-WanAndroid'
Configuration on demand is an incubating feature.
> Configure project :app
gradlew version > 4.0
WARNING: API 'variant.getJavaCompiler()' is obsolete and has been
replaced with 'variant.getJavaCompileProvider()'.
It will be removed at the end of 2019.
For more information, see
https://d.android.com/r/tools/task-configuration-avoidance.
To determine what is calling variant.getJavaCompiler(), use
-Pandroid.debug.obsoleteApi=trueon the command line to display more information. skip tinyPicPlugin Task!!!!!! skip tinyPicPlugin Task!!!!!! All project evaluation completed (end of configuration phase) >Task :clean UP-TO-DATE
:clean spend 1ms
...
> Task :app:clean
:app:clean spend 2ms
> Task :app:packageSpeedDebug
:app:packageSpeedDebug spend 825ms
> Task :app:assembleSpeedDebug
:app:assembleSpeedDebug spend 1Spend time >50ms:.Copy the code

In addition, in settings.gradle, we can specify the location of other projects so that moudle from other external projects can be imported into the current project. The sample code looks like this:

if (useSpeechMoudle) {
    // Import the speech module of other apps
    include "speech"
    project(":speech").projectDir = new File(".. /OtherApp/speech")}Copy the code

2. Configuration phase

In the configuration phase, the Task is to execute the build.gradle script under each item to complete the configuration of the Project. Meanwhile, the Task dependency diagram is constructed so that the Task can be executed according to the dependency in the execution phase. The code executed during the configuration phase generally consists of the following three parts, as shown below:

  • 1) build. Gralde
  • 2) Closures.
  • 3) Configuration section statement in Task

Note that by executing any Gradle command, the code in the initialization and configuration phases is executed.

3. Execution phase

At the end of the configuration phase, Gradle creates a directed acyclic graph based on the dependencies of each Task. You can get the directed acyclic graph by using Gradle’s getTaskGraph method => TaskExecutionGraph. After the directed acyclic graph is built and 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

The Gradle build system then executes the respective tasks by calling Gradle < task name >.

4. Hook Gradle life cycle nodes

Joe_H Gradle life cycle sequence diagram to illustrate the entire process of Gradle life cycle, as shown below:

As you can see, the Gradle lifecycle process consists of four parts:

  • 1) First, parse settings.gradle to get module information. This is the initialization phase.
  • 2) Then, configure each module without executing tasks.
  • 3) Then, after configuration, there is an important callback project.afterevaluate, which indicates that all modules have been configured and are ready to execute task.
  • Finally, execute the specified task and its dependent tasks.

The most complex Gradle build command is the Gradle build command, because many other tasks are required to build a project.

Matters needing attention

  • 1) Listeners for each Hook point must be added before the lifecycle of the callback.
  • 2) If more than one project. AfterEvaluate callback is registered, the order of execution is the same as the order of registration.

5. Obtain the time consumption of each phase and task of construction

Now that we know about the Hook methods in the Gradle lifecycle, we can use them to get information about how long it takes to build a project at various stages and tasks. We can add settings. Gradle with the following code:

long beginOfSetting = System.currentTimeMillis()
def beginOfConfig
def configHasBegin = false
def beginOfProjectConfig = new HashMap()
def beginOfProjectExcute
gradle.projectsLoaded {
    println 'Initialization phase, time:' + (System.currentTimeMillis() -
beginOfSetting) + 'ms'
}
gradle.beforeProject { project ->
    if(! configHasBegin) { configHasBegin =true
        beginOfConfig = System.currentTimeMillis()
    }
    beginOfProjectConfig.put(project, System.currentTimeMillis())
}
gradle.afterProject { project ->
    def begin = beginOfProjectConfig.get(project)
    println 'Configuration phase,' + project + 'Time:' +
(System.currentTimeMillis() - begin) + 'ms'
}
gradle.taskGraph.whenReady {
    println 'Configuration phase, total time:' + (System.currentTimeMillis() -
beginOfConfig) + 'ms'
    beginOfProjectExcute = System.currentTimeMillis()
}
gradle.taskGraph.beforeTask { task ->
    task.doFirst {
        task.ext.beginOfTask = System.currentTimeMillis()
    }
    task.doLast {
        println 'Execution phase,' + task + 'Time:' +
(System.currentTimeMillis() - task.beginOfTask) + 'ms'
    }
}
gradle.buildFinished {
    println 'Execution phase, time:' + (System.currentTimeMillis() -
beginOfProjectExcute) + 'ms'
}
Copy the code

Gradle creates an instance for each type of configuration script. Gradle has three types of configuration scripts, as shown below:

  • 1),Build Scrpit:This corresponds to a Project instance, i.e. each build.gradle is converted to a Project instance.
  • 2),Init Scrpit:This corresponds to a Gradle instance, which is created during build initialization and exists as a singleton throughout build execution.
  • 3),Settings Scrpit:Each settings.gradle is converted to a Settings instance.

As you can see, a Gradle build process consists of one or more project instances, and each project instance consists of one or more tasks. Next, let’s get to know Project.

Third, the Project

Project is the entry point for Gradle to build the entire application, so it is very important that we have a deep understanding of it. Unfortunately, there are few good articles about project on the web, but that’s okay. We’ll take a closer look at the Project API.

Gradle has a Project instance that corresponds to build.gradle. In build.gradle, we usually configure a series of Project dependencies, such as the following dependency:

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

Dependency keywords like implementation and API are essentially a method call. Above, we use the implementation() method to pass in a map parameter with three key-values in it. The full form is as follows:

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

When we use the aar file for implementation and API dependencies, Gradle will find the aar file in the Repository. Your repository may contain jCenter, Maven, etc. Each repository is actually a collection server that relies on files, and they are classified and stored by the group, name, and version mentioned above.

1. Project core API decomposition

There are many apis in Project, but they can be divided into six parts according to their attributes and uses, as shown in the figure below:

We can first have a general understanding of the functions of each part in the Project, so as to build an overall perception of the API system of the Project, as shown below:

  • 1),Project API:Give the current Project the ability to manipulate its parent Project and manage its children.
  • 2),Task related to the API:Provides the ability to add tasks and manage existing tasks for the current Project. Because Task is so important, we’ll cover it in Chapter 4.
  • 3),The Api associated with the Project attribute:Gradle gives us some Project properties up front, and the property-related API gives us the ability to add additional properties to a Project.
  • 4),The File related Api:The Project File API is used to handle some of the File processing under our current Project.
  • 5),Gradle Lifecycle API:The lifecycle API we covered in Chapter 2.
  • 6),Other API:Add dependencies, add configurations, introduce external files, and so on.

2, Project API

Every Groovy Script is compiled into Script bytecode by the compiler, and every build.gradle Script is compiled into Project bytecode by the compiler, So all the logic we write in build.gradle is written inside the Project class. Below, we’ll walk through a series of important apis for Project from easy to hard.

By default, we select the build.gradle script file of the root Project to learn a series of uses of Project. The getAllProject usage is as follows:

1, getAllprojects

GetAllprojects means to get all project instances. The example code is as follows:

/** * getAllProjects */
this.getProjects()

def getProjects() {
    println "< = = = = = = = = = = = = = = = = >"
    println " Root Project Start "
    println "< = = = = = = = = = = = = = = = = >"
    // the getAllprojects method returns a Set containing the root project and its subprojects
    // the eachWithIndex method is used to iterate over collections, arrays, and other iterable containers.
    // And returns the subscript at the same time, unlike the each method which only returns project
    this.getAllprojects().eachWithIndex { Project project, int index ->
        // 2. If the subscript is 0, the current traversal is rootProject
        if (index == 0) {
            println "Root Project is $project"
        } else {
            println "child Project is $project"}}}Copy the code

First, we define a getProjects method using the def keyword. Then, in comment 1, we call the getAllprojects method to return a Set containing the root project and its subprojects, and chain eachWithIndex to traverse the Set. Then, in comment 2, we determine if the current index is 0, if so, it indicates that the current traversal is rootProject, and output the name of rootProject, otherwise, the name of Child Project.

Here, we execute./gradlew clean on the command line, and the result is as follows:

quchao@quchaodeMacBook-Pro Awesome-WanAndroid %./gradlew clean Settings Evaluation completed (settings.gradle code completed) Project structure loading completed (initialization phase completed) After initialization, access to root project: root project 'awesome-wanandroid' Initialization phase, time: 5ms Configuration on demand is an incubating feature. > Configure project : <================> Root Project Start <================> Root Project is root project 'Awesome-WanAndroid' child Project Is project ':app' configuration phase, root project 'Awesome-WanAndroid' duration: 284ms > Configure project :app... Configuration phase, total duration: 428ms > Task :app:clean execution phase, Task ':app:clean' duration: 1ms :app:clean spend 2ms finish construction Tasks spend time > 50ms: Execution phase: 9msCopy the code

As you can see, after initialization, our rootProject will be configured first and the corresponding project information will be output. The configuration of the subproject app is then performed. Finally, the clean task is executed.

It should be noted that the rootProject and its sub-projects form a tree structure, but the height of the tree is limited to only two levels.

2, getSubprojects

GetSubprojects: obtains all sub-projects under the current project. The example code is as follows:

/** * getAllsubproject Example */
this.getSubProjects()

def getSubProjects() {
    println "< = = = = = = = = = = = = = = = = >"
    println " Sub Project Start "
    println "< = = = = = = = = = = = = = = = = >"
    The getSubprojects method returns a Set containing subprojects
    this.getSubprojects().each { Project project ->
        println "child Project is $project"}}Copy the code

As with getAllprojects, the getSubprojects method returns a Set of subprojects. Here we directly print out the names of each subproject using the each method. The running result is as follows:

quchao@quchaodeMacBook-Pro Awesome-WanAndroid %./gradlew clean Settings Evaluation completed (settings.gradle code completed)... > Configure project : < = = = = = = = = = = = = = = = = > Sub Project Start < = = = = = = = = = = = = = = = = > child Project is the Project ': app' configuration phase, Root project 'awesome-wanandroid' 本 版 : 289ms > Configure project :app... Total duration: 425ms > Task :app:clean Execution phase, Task ':app:clean' Duration: 1ms :app:clean spend 2ms construction end Tasks spend time > 50ms: the execution time is 9msCopy the code

As you can see, the name of the subproject is also printed in the Gradle configuration phase.

3, getParent

GetParent: getParent: getParent: getParent: getParent: getParent: getParent: getParent: getParent: getParent: getParent: getParent: getParent: getParent: getParent: getParent: getParent: getParent: getParent

. > Configure project : Root project 'Awesome-WanAndroid' 104ms > Configure project :app gradlew version > 4.0 my parent project is Awesome-WanAndroid configuration stage, project ':app' 282ms ... Complete all project evaluation (end of configuration phase) Configuration phase, total time: 443ms...Copy the code

As you can see, the current parent class of app Project is output, Awesome-WanAndroid Project.

4, getRootProject

What if we want to just get the current project instance in the root project? To get the project instance of the current root project, use getRootProject directly in any build.gradle file.

/** * 4, getRootProject */
this.getRootPro()

def getRootPro(a) {
    def rootProjectName = this.getRootProject().name
    println "root project is $rootProjectName"
}
Copy the code

5, the project

A project represents an instance of a given project that is then operated on in a closure. Before using it, it’s worth looking at the source code for the project method, as follows:

    /** * 

Locates a project by path and configures it using the given closure. If the path is relative, it is * interpreted relative to this project. The target project is passed to the closure as the closure's delegate.

* * @param path The path. * @param configureClosure The closure to use to configure the project. * @return The project with the given path. Never returns null. * @throws UnknownProjectException If no project with the given path exists. */
Project project(String path, Closure configureClosure); Copy the code

As you can see, the project method has two parameters, one to specify the path of the project and the other to configure the closure of the project. Let’s take a look at how to use Project flexibly, with the following example code:

/** *

Closure arguments can be placed outside parentheses
project("app") { Project project ->
    apply plugin: 'com.android.application'
}

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

Once we’re comfortable with it, we’ll usually use note 2.

6, allprojects

Allprojects is used to configure the current project and each of its sub-projects, as shown below:

/** * 6, allprojects */

// More succinctly written like project
allprojects {
    repositories {
        google()
        jcenter()
        mavenCentral()
        maven {
            url "https://jitpack.io"
        }
        maven { url "https://plugins.gradle.org/m2/"}}}Copy the code

In AllProjects we are used to configure some general configurations, such as the most common global repository configuration above.

7, subprojects

Subprojects can uniformly configure all sub-projects under the current project, and the sample code is as follows:

/** * add a Maven configuration script for all subprojects to upload aar files to Maven server */
subprojects {
    if (project.plugins.hasPlugin("com.android.library")) {
        apply from: '.. /publishToMaven.gradle'}}Copy the code

In the example code above, we will first determine whether the subprojects under the current project are libraries, if they are libraries it is necessary to introduce the publishToMaven script.

3. Project attribute

Currently, only seven attributes are predefined in the Project interface, and their source code is shown below:

public interface Project extends Comparable<Project>, ExtensionAware.PluginAware {
    /** * The default project build file name */
    String DEFAULT_BUILD_FILE = "build.gradle";

    /** * the symbol that separates the project name from the task name */
    String PATH_SEPARATOR = ":";

    /** * Default build directory name */
    String DEFAULT_BUILD_DIR_NAME = "build";

    String GRADLE_PROPERTIES = "gradle.properties";

    String SYSTEM_PROP_PREFIX = "systemProp";

    String DEFAULT_VERSION = "unspecified";

    String DEFAULT_STATUS = "release"; . }Copy the code

Fortunately, Gradle provides the ext keyword to allow us to define our own extended properties. It enables global configuration of dependencies in our project. Let’s start with the ancient days of configuration to give us a deeper understanding of Gradle’s global dependency configuration.

Ext extended attributes

1. Ancient times

In the early days of AS, our dependency configuration code looked like this:

android {
    compileSdkVersion 27
    buildToolsVersion "28.0.3". }Copy the code

Slash-and-burn

However, this type of direct writing display is not standard, so we use this method later:

def mCompileSdkVersion = 27
def mBuildToolsVersion = "28.0.3"

android {
    compileSdkVersion mCompileSdkVersion
    buildToolsVersion mBuildToolsVersion
    ...
}
Copy the code

Iron plough cattle ploughing

If each subproject needs to be configured with the same Version, we will need to write a lot more duplicate code, so we can use the subproject and Ext we learned above to simplify:

// Build. Gradle in the root directory
subprojects {
    ext {
        compileSdkVersion = 27
        buildToolsVersion = "28.0.3"}}// Build. Gradle in app moudle
android {
    compileSdkVersion this.compileSdkVersion
    buildToolsVersion this.buildToolsVersion
    ...
}
Copy the code

The Industrial Age

There is still a serious problem with using the subprojects method to define general extension attributes. It will still define these extended attributes in each sub-project in the same way as before. In this case, we can remove subprojects and directly use Ext to define global attributes:

// Build. Gradle in the root directory
ext {
    compileSdkVersion = 27
    buildToolsVersion = "28.0.3"
}
Copy the code

5. The age of electronics

As the project gets bigger, the number of ext extension attributes defined in the root project increases. Therefore, we can configure this set of global attributes to be defined in another Gradle script, which we usually call config.gradle. The common template is as follows:

ext {

    android = [
            compileSdkVersion       : 27,
            buildToolsVersion       : "28.0.3". ]  version = [ supportLibraryVersion :"28.0.0". ]  dependencies = [// base
            "appcompat-v7"                      : "com.android.support:appcompat-v7:${version["supportLibraryVersion"]}". ]  annotationProcessor = ["glide_compiler"                    : "com.github.bumptech.glide:compiler:${version["glideVersion"]}". ]  apiFileDependencies = ["launchstarter"                                   : "Libs/launchstarter - release - 1.0.0. Aar." ". ]  debugImplementationDependencies = ["MethodTraceMan"                                  : "Com. Making. Zhengcx: MethodTraceMan: 1.0.7"
    ]

    releaseImplementationDependencies = [
            "MethodTraceMan"                                  : "Com. Making. Zhengcx: MethodTraceMan: 1.0.5 - it"]... }Copy the code

More intelligent now

Although we have a comprehensive global dependency profile, we still have to write a long string of dependency code in each module, so we can use a traversal approach to dependency, the template code is as follows:


// in the build.gradle script of each moulde
def implementationDependencies = rootProject.ext.dependencies
def processors = rootProject.ext.annotationProcessor
def apiFileDependencies = rootProject.ext.apiFileDependencies

// in the Dependencies closure of the build.gradle script in each moulde
// Handle all AAR dependencies
apiFileDependencies.each { k, v -> api files(v)}

// Handle all xxximplementation dependencies
implementationDependencies.each { k, v -> implementation v }
debugImplementationDependencies.each { k, v -> debugImplementation v } 
...

// Process annotationProcessor dependencies
processors.each { k, v -> annotationProcessor v }

// Process all dependencies containing exclude
debugImplementationExcludes.each { entry ->
    debugImplementation(entry.key) {
        entry.value.each { childEntry ->
            exclude(group: childEntry.key, module: childEntry.value)
        }
    }
}
Copy the code

Maybe in the future as Gradle continues to optimize there will be a more concise way, if you have a better way, we can discuss it.

Define the extended properties under gradle.properties

In addition to defining additional properties using ext extension properties, we can also define extension properties under gradle.properties, with the following example code:

/ / in gradle. The properties
mCompileVersion = 27

// Build. Gradle in app moudle
compileSdkVersion mCompileVersion.toInteger()
Copy the code

4. File related API

In Gradle, file-related apis can be summarized into two broad categories:

  • 1),Path acquisition API
    • getRootDir()
    • getProjectDir()
    • getBuildDir()
  • 2),API for file manipulation
    • File location
    • File copy
    • File tree traversal

1) Path acquisition API

There are three commonly used apis for path fetching, with the following example code:

/** * 1, path access API */
println "the root file path is:" + getRootDir().absolutePath
println "this build file path is:" + getBuildDir().absolutePath
println "this Project file path is:" + getProjectDir().absolutePath
Copy the code

Then we execute./gradlew clean, and the output looks like this:

> Configure project :
the root file path is:/Users/quchao/Documents/main-open-project/Awesome-WanAndroid
this build file path is:/Users/quchao/Documents/main-open-project/Awesome-WanAndroid/build
thisProject file path is: / Users/quchao/Documents/main - open - the Project/Awesome - WanAndroid configuration stage, the root of the Project'Awesome-WanAndroid'Time: 538 msCopy the code

2) API related to file operation

1. File location

The common file location API is File /files, with the following example code:

// In build.gradle under rootProject

/** * 1, file */
this.getContent("config.gradle")

def getContent(String path) {
    try {
        // Unlike new file, which requires an absolute path,
        // file starts the search relative to the current project
        def mFile = file(path)
        println mFile.text 
    } catch (GradleException e) {
        println e.toString()
        return null}}/** * 1, files */
this.getContent("config.gradle"."build.gradle")

def getContent(String path1, String path2) {
    try {
        // Unlike new file, which requires an absolute path,
        // file starts the search relative to the current project
        def mFiles = files(path1, path2)
        println mFiles[0].text + mFiles[1].text
    } catch (GradleException e) {
        println e.toString()
        return null}}Copy the code

2. Copy files

A common file copy API is copy, with example code as follows:

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

3. File tree traversal

We can use fileTree to convert the current directory to a number of files, and then take each tree element (node) and do something like this:

/** ** /
fileTree("build/outputs/apk") { FileTree fileTree ->
    fileTree.visit { FileTreeElement fileTreeElement ->
        println "The file is $fileTreeElement.file.name"
        copy {
            from fileTreeElement.file
            into getRootProject(a).getBuildDir(a).path + "/apkTree/"
        }
    }
}
Copy the code

5. Other apis

1. Rely on related apis

Buildscript under the root project

The dependencies in buildScript to configure the project core. The original and simplified usage examples are as follows:

Original usage examples
buildscript { ScriptHandler scriptHandler ->
    // Configure the warehouse address of our project
    scriptHandler.repositories { RepositoryHandler repositoryHandler ->
        repositoryHandler.google()
        repositoryHandler.jcenter()
        repositoryHandler.mavenCentral()
        repositoryHandler.maven { url 'https://maven.google.com' }
        repositoryHandler.maven { url "https://plugins.gradle.org/m2/" }
        repositoryHandler.maven {
            url uri('.. /PAGradlePlugin/repo')
        }
        // Access the local private Maven server
        repositoryHandler.maven {
            name "personal"
            url "http://localhost:8081:/JsonChao/repositories"
            credentials {
                username = "JsonChao"
                password = "123456"}}}// Configure plugin dependencies for our project
    dependencies { DependencyHandler dependencyHandler ->
        dependencyHandler.classpath 'com. Android. Tools. Build: gradle: 3.1.4'. }Copy the code
Simplified usage examples
buildscript {
    // Configure the warehouse address of our project
    repositories {
        google()
        jcenter()
        mavenCentral()
        maven { url 'https://maven.google.com' }
        maven { url "https://plugins.gradle.org/m2/" }
        maven {
            url uri('.. /PAGradlePlugin/repo')}}// Configure plugin dependencies for our project
    dependencies {
        classpath 'com. Android. Tools. Build: gradle: 3.1.4'. }Copy the code

Dependencies under app moudle

Unlike dependencies in the root project buildScript, which configures plugin dependencies for our Gradle project, dependencies in App Moudle are used to add third-party dependencies to the application. For dependencies in app Moudle, we need to pay attention to the use of exclude and transitive. The example code is as follows:

implementation(rootProject.ext.dependencies.glide) {
        // Remove dependencies: usually used to solve problems related to resource and code conflicts
        exclude module: 'support-v4' 
        A => B => C; B => C;
        // If A is dependent on B, then A can use B
        C dependencies used in // are disabled by default, that is, false
        transitive false 
}
Copy the code

2. Execute external commands

We use the exec command provided by Gradle to execute external commands. Here is an example of the exec command used to copy APK files from our current project to the Downloads directory on our PC.

/** * use exec to execute external command */
task apkMove(a) {
    doLast { 
        // Execute in gradle execution phase
        def sourcePath = this.buildDir.path + "/outputs/apk/speed/release/"
        def destinationPath = "/Users/quchao/Downloads/"
        def command = "mv -f $sourcePath $destinationPath"
        exec {
            try {
                executable "bash"
                args "-c", command
                println "The command execute is success"
            } catch (GradleException e) {
                println "The command execute is failed"}}}}Copy the code

Fourth, the Task

Only tasks can be executed in the execution phase of Gradle (essentially a series of actions in the executed Task), so the importance of tasks is self-evident.

1. Start with an example 🌰

First, we can define a Task in any build.gradle file. Here is a complete example:

// create a Gradle task named JsonChao
task JsonChao
JsonChao {
    // Set JsonChao task closure to hello~,
    // Execution is in the second phase of gradle's life cycle, the configuration phase.
    println("hello~")
    // 3, attach some Action to task, execute in
    // The third stage of the Gradle lifecycle is the execution stage.
    doFirst {
        println("start")
    }
    doLast {
        println("end")}}// 4, the declaration is defined separately from the configuration and Action
// you can also combine them directly.
// Here we define an Android Task that relies on JsonChao
// Task, that is, the JsonChao task must be executed before
// To execute the Android Task, thus forming one between them
// Directed acyclic graph: JsonChao Task => Android Task
task Andorid(dependsOn:"JsonChao") {
    doLast {
        println("end?")}}Copy the code

First, in comment 1, we declare a Gradle Task named JsonChao. Next, in comment 2, hello~ is printed in the JsonChao Task closure, where the code will execute in the second phase of the Gradle life cycle, the configuration phase. Then, in comment 3, there are actions attached to task, i.e. DoFirst and doLast, whose closure will execute in the third phase of gradle’s life cycle, the execution phase.

For doFirst and doLast actions, their roles are as follows:

  • doFirst:Represents the Action invoked at the beginning of task execution.
  • doLast:Represents the Action to be invoked when the task is about to finish executing.

Note that doFirst and doLast can be executed multiple times.

Finally, in comment 4, we can see that in addition to defining declarations separately from configurations and actions in comments 1, 2, and 3, we can also combine them directly. Here we define an Android task, which depends on JsonChao Task. In other words, JsonChao Task must be executed before Android Task can be executed. Therefore, a directed acyclic graph is formed between them: JsonChao Task => Android Task.

Run the Android Gradle task and you can see the following output:

> Task :JsonChao start end Task ':JsonChao' Time: 1ms :JsonChao Spend 4ms > Task :Andorid end? Execution phase, task ':Andorid' duration: 1ms :Andorid spend 2ms construction end Tasks spend time > 50ms: execution phase, duration: 15msCopy the code

2. Definition and configuration of Task

There are two common ways to define a Task. Example code is as follows:

// Task definition method 1: create a Task directly from the Task function ("()" can not specify the group and description attribute)
task myTask1(group: "MyTask", description: "task1") {
    println "This is myTask1"
}

// Task definition method 2: Create a Task using TaskContainer
this.tasks.create(name: "myTask2") {
    setGroup("MyTask")
    setDescription("task2")
    println "This is myTask2"
}
Copy the code

After the Task is defined and the project is synchronized, you can see the corresponding Task Group and its Tasks.

The attribute of the Task

Note that no matter how a task is defined, we can configure its properties in “()” as follows:

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

The currently officially supported attributes can be summarized in the following table:

The selection describe The default value
“name” The task name None. This parameter must be specified
“type” Task Class to be created DefaultTask
“action” The closure or Action that needs to be executed when the task executes null
“overwrite” Replace an existing task false
“dependsOn” The collection of tasks on which this task depends []
“group” Group to which the task belongs null
“description” Description of the task null
“constructorArgs” Parameters passed to the Task Class constructor null

Use “$” to refer to another task’s property

Here, we can use “$” in the current task to reference another task’s attribute, as shown in the following example:

task Gradle_First() {

}

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

Use Ext to customize the required properties for task

In addition to using existing attributes, you can also use Ext to customize task attributes as shown in the following code:

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

Use the defaultTasks keyword to identify tasks to be performed by default

In addition, we can also use the defaultTasks keyword to mark some tasks as defaultTasks to be performed, as shown below:

defaultTasks "Gradle_First"."Gradle_Last"

task Gradle_First(a) {
    ext.good = true
}

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

Matters needing attention

Each task goes through a complete lifecycle process of initialization, configuration, and execution.

3. Detailed explanation of Task execution

Tasks typically use both doFirst and doLast to operate during execution. The sample code is shown below:

// Use Task to perform operations in 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 use taskname. doxxx to add tasks
myTask3.doFirst {
    // The first execution of this method => boss
    println "This group is 1"
}
Copy the code

Task execution

Next, we use doFirst and doLast to calculate the time spent during build execution. The complete code is shown below:

// Task execution: Calculate the time spent during build execution
def startBuildTime, endBuildTime
// After Gradle configuration is complete
// To ensure that the task to be executed is configured
this.afterEvaluate { Project project ->
    // Find the first task to execute in the current project, i.e. preBuild task
    def preBuildTask = project.tasks.getByName("preBuild")
    preBuildTask.doFirst {
        // get the timestamp of the first task execution time
        startBuildTime = System.currentTimeMillis()
    }
    // find the last task executed under the current project, i.e. Build task
    def buildTask = project.tasks.getByName("build")
    buildTask.doLast {
        // get the timestamp of the last task before it finished executing
        endBuildTime = System.currentTimeMillis()
        // 6. Output the time spent during build execution
        println "Current project execute time is ${endBuildTime - startBuildTime}"}}Copy the code

4. Task dependencies and execution order

There are three ways to specify the execution order of tasks, as shown in the following figure:

1) dependsOn

DependsOn can be broken down into static and dynamic dependencies. The code for this example is as follows:

Static dependence

task task1 {
    doLast {
        println "This is task1"
    }
}

task task2 {
    doLast {
        println "This is task2"}}// Task static dependency 1 (common)
task task3(dependsOn: [task1, task2]) {
    doLast {
        println "This is task3"}}// Task static dependency mode 2
task3.dependsOn(task1, task2)
Copy the code

Dynamic dependence

// Task dynamic dependency mode
task dytask4 {
    dependsOn this.tasks.findAll { task ->
        return task.name.startsWith("task")
    }
    doLast {
        println "This is task4"}}Copy the code

2) Specify input and output by Task

We can also specify input and output by Task. In this way, we can efficiently implement a Gradle script that automatically maintains release documents. The input and output code is as follows:

task writeTask {
  inputs.property('versionCode'.this.versionCode)
  inputs.property('versionName'.this.versionName)
  inputs.property('versionInfo'.this.versionInfo)
  // 1. Specify the output file as destFile
  outputs.file this.destFile
  doLast {
    // Write the input to the output file
    def data = inputs.getProperties()
    File file = outputs.getFiles().getSingleFile()
    
    // Write version information to the XML file. } task readTask {// 2. Specify the input file as the output file of the last task (writeTask) destFile
  inputs.file this.destFile
  doLast {
    // Read the contents of the input file and display it
    def file = inputs.files.singleFile
    println file.text
  }
}

task outputwithinputTask {
  // execute write first, then execute read
  dependsOn writeTask, readTask
  doLast {
    println 'I/O task terminated'}}Copy the code

First, we define a WirteTask, and then, in comment 1, we specify the output file as destFile and write the version information to the XML file. Next, we define a readTask and, in comment 2, specify that the input file is the output file of the previous task (writeTask). Finally, in comment 3, link the two tasks with a dependsOn in which the order of input and output is write before read. In this way, a real case of input and output is implemented. To see the complete implementation code, check out the releaseInfo.gradle script for awesome-wanAndroid.

DependsOn inserts a Task into the Gradle build process in McImage. The key code is as follows:

// inject task
(project.tasks.findByName(chmodTask.name) as Task).dependsOn(mergeResourcesTask.taskDependencies.getDependencies(mergeResourcesTask))
(project.tasks.findByName(mcPicTask.name) as Task).dependsOn(project.tasks.findByName(chmodTask.name) as Task)
mergeResourcesTask.dependsOn(project.tasks.findByName(mcPicTask.name))
Copy the code

Specify the order of dependencies through the API

In addition to the dependsOn method, a mustRunAfter method can be used in a Task closure to specify the task’s dependency order. Note that in the latest Gradle API, A mustRunAfter must be used in conjunction with a dependsOn strong dependency, with the following example code:

// Specify the dependency order through the API
task taskX {
    mustRunAfter "taskY"

    doFirst {
        println "this is taskX"
    }
}

task taskY {
    // Use mustRunAfter to specify one or more dependent front tasks
    // We can also use shouldRunAfter, but it is an optional dependency
// shouldRunAfter taskA
    doFirst {
        println "this is taskY"}}task taskZ(dependsOn: [taskX, taskY]) {
    mustRunAfter "taskY"
    doFirst {
        println "this is taskZ"}}Copy the code

5. Task type

In addition to defining a new task, we can also use the Type attribute to directly use an existing task type, such as Gradle’s Copy, Delete, Sync task, etc. The sample code looks like this:

1. Delete the build file in the root directory
task clean(type: Delete) {
    delete rootProject.buildDir
}
// copy doc to build/target
task copyDocs(type: Copy) {
    from 'src/main/doc'
    into 'build/target/doc'
}
// 3. Copy the source files to the destination directory and delete all non-copied files from the destination directory
task syncFile(type:Sync) {
    from 'src/main/doc'
    into 'build/target/doc'
}
Copy the code

6. Hook to the build lifecycle

We can hook our own tasks into the build lifecycle using a series of lifecycle apis provided by Gradle, For example, the afterEvaluate method is used to attach the writeTask we defined in section 3 to gradle after all tasks have been configured:

// Execute writeTask after the configuration phase
this.project.afterEvaluate { project ->
  def buildTask = project.tasks.findByName("build")
  doLast {
    buildTask.doLast {
      // Use finalizedBy on 5.x
      writeTask.execute()
    }
  }
}
Copy the code

Note that after the configuration is complete, we need to import the releaseInfo script we defined in the app moudle as follows:

apply from: this.project.file("releaseinfo.gradle")
Copy the code

Fifth, SourceSet

SourceSet is mainly used to set the location of source code or resources in our project, and the two most common use cases of SourceSet are as follows:

  • 1) Modify the location of so inventory.
  • 2) Subcontracting storage of resource files.

1. Modify the location of so inventory

We only need to configure the following code in the Android closure of app Moudle to change the location of so inventory:

android {
    ...
    sourceSets {
        main {
            // change the location of so inventory
            jniLibs.srcDirs = ["libs"]}}}Copy the code

2. Subcontracting storage of resource files

Similarly, configure the following code in the Android closure of app Moudle to subcontract the resource file to store:

android {
    sourceSets {
        main {
            res.srcDirs = ["src/main/res"."src/main/res-play"."src/main/res-shop". ] }}}Copy the code

Alternatively, we can define sourceSets outside the Android closure with code like this:

this.android.sourceSets {
    ...
}
Copy the code

Gradle command

There are many Gradle commands, but we usually only use two types of commands:

  • 1) Command to get build information.
  • 2) Execute task command

Commands to get build information

// Run the following command to create your project: // Run the following command to create your project: // Run the following command to create your project: // Run the following command to create your project: // Run the following command to create your projectCopy the code

2. Run the task command

There are four common commands for executing tasks, as follows:

Gradlew JsonChao Gradle_Last // 2, use -x to exclude a single task./gradlew -x JsonChao // 3, use -continue /gradlew -continue JsonChao // The following command is used to execute // Gradle_Last task./gradlew G_LastCopy the code

For tasks defined in subdirectories, we usually use the following command to execute them:

Gradlew -b app/build. Gradle MyTask // 2. In large projects we usually use -p instead of -b MyTaskCopy the code

Seven,

Now that we have covered the core API of Gradle, let’s review the main points of this article as follows:

  • Advantages of Gradle
    • 1. Better flexibility
    • 2. Finer granularity
    • 3. Better scalability
    • 4. Greater compatibility
  • Gradle build lifecycle
    • 1. Initialization phase
    • 2. Configuration phase
    • 3. Execution phase
    • 4. Hook Gradle life cycle nodes
    • 5. Obtain the time consumption of each phase and task of construction
  • Third, the Project
    • 1. Project core API decomposition
    • 2, Project API
    • 3. Project attribute
    • 4. File related API
    • 5. Other apis
  • Fourth, the Task
    • 1. Start with an example 🌰
    • 2. Definition and configuration of Task
    • 3. Detailed explanation of Task execution
    • 4. Task dependencies and execution order
    • 5. Task type
    • 6. Hook to the build lifecycle
  • Fifth, SourceSet
    • 1. Modify the location of so inventory
    • 2. Subcontracting storage of resource files
  • Gradle command
    • Commands to get build information
    • 2. Run the task command

Gradle’s core API is very important, which is absolutely necessary for us to implement a Gradle plug-in efficiently. Because only a solid foundation can go further, I hope we can move forward together.

Reference links:


  • 1. Chapter 6-8 of “MoOCs Gradle3.0 Automation Project Construction Technology Intensive + Actual Combat”

  • Gradle DSL API documentation

  • 3. Android Plugin DSL API documentation

  • Gradle DSL => Project

  • Gradle DSL => Task

  • 6. Gradle Script Basics

  • 7. Fully understand Gradle – execution timing

  • 8. Understand Gradle – Defining tasks

  • 9. Master Android Gradle

  • Gradle Foundation – Build lifecycle and Hook technology

Thank you for reading this article and I hope you can share it with your friends or technical group, it means a lot to me.

I hope we can be friends inGithub,The Denver nuggetsTo share knowledge.