Gradle Series I -- Groovy, Gradle and custom Gradle plug-ins

1. overview The construction process of Android project is implemented by Gradle framework, which is implemented in Groovy language, and Gradle plug-i...
1. overview

The construction process of Android project is implemented by Gradle framework, which is implemented in Groovy language, and Gradle plug-in is implemented on the basis of Gradle framework. So it is necessary to learn some common grammars of Groovy.

Gradle plug-in source download:
gradle_3.0.0

2. Groovy grammar

Groovy extends the Java language by providing a simpler and more flexible grammar for dynamic type checking at runtime; therefore, Java language grammar is applicable to Groovy language.

Reference to Groovy grammar Proficiency in Groovy I won't explain it here.

3. Configure Gradle

3.1 Install Gradle

For installing gradle, you can refer to the official documents:
gradle Installation

3.2 Configuring Gradle in Android Project

What version of gradle is used for android project in Android studio? It can be configured in the gradle/wraaper/gradle-wrapper.properties file in the root directory of android project:

#Tue Nov 03 16:49:32 CST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip

The last line above configures the gradle version used in the android project to be 4.1

Note: The version correspondence between gradle and com.android.tools.build:gradle plug-in library


Above is the official website. Android Plugin for Gradle Release Notes Given.

4. Gradle

4.1 overview

Everything in Gradle is based on two basic concepts: project and task

Project
This interface is the main API that build file interacts with Gradle. All Gradle functions can be accessed through the Project interface.

Life Cycle of Project

Task
A Project is essentially a collection of Task objects. Each Task performs some basic tasks, such as compiling classes, running unit tests, or compressing WAR files. Task can be added to a Project using a create () method on TaskContainer (such as TaskContainer.create (java.lang.String). Existing Tasks can be found using a lookup method on TaskContainer (such as TaskCollection.getByName (java.lang.String).

4.2 Life Cycle of Build

In Gradle, you can define the dependencies between task and task. Gradle ensures that these tasks are executed according to their dependencies, and that each task is executed only once. These tasks form a directed acyclic graph. Gradle completes the construction of a complete dependency graph before performing any tasks, which is the core of Gradle, making many things possible, otherwise it will be impossible.

Gradle build consists of three stages:

  1. Initialization
    Gradle supports single and multiple Project builds. During the initialization phase, Gradle determines which projects will participate in the build and creates a Project instance for each Project.
    In addition to the build script file, Gradle also defines a settings file, which is executed in the initialization phase. Multi-Project buiid must have settings.gradle files in the root Project of the multi-Project hierarchy. This is necessary because the settings file defines which projects are participating in a multi-Project build. For a single Project build, the settings file is optional.
    For build script, property access and method calls are delegated to a Project object. Similarly, property access and method calls in settings files are delegated to settings objects.
  2. Configuration
    At this stage, the Project object is configured by executing the corresponding construction script (such as the build.gradle file of the Android Project). The directed acyclic graph formed by Task is created at this stage.
  3. Execution
    First, a subset of Tasks created and configured in the configuration phase is determined for execution, which is determined by the Task name and parameters passed to the gradle command and the current directory. Gradle then executes the Task in the collection.

Monitoring the execution process of build script
Builscript can be notified during the execution of build script. These notifications usually take two forms: implementing a specific listening interface, or providing a closure to execute when the notification is triggered. Next, take the Android project HotFix (which includes app, patchClassPlugin, hackdex modules) as an example and process notifications in a closure manner:

  1. Notification is received immediately before and after the execution of the Project (that is, before and after the execution of the corresponding build script for the Project). This can be used to perform things such as additional configuration after the build script is executed, printing custom logs, or analysis:
allprojects { afterEvaluate { project -> println "Adding smile task to $project" project.task('smile') { doLast { println "Running smile task for $project" } } } }

The above code is added to build.gradle in the root directory of the Android project HotFix.
Operation results:

chenyangdeMacBook-Pro:HotFix chenyang$ ./gradlew smile > Configure project : Adding smile task to root project 'HotFix' > Configure project :app Adding smile task to project ':app' > Configure project :hackdex Adding smile task to project ':hackdex' > Configure project :patchClassPlugin Adding smile task to project ':patchClassPlugin' > Task :smile Running smile task for root project 'HotFix' > Task :app:smile Running smile task for project ':app' > Task :hackdex:smile Running smile task for project ':hackdex' > Task :patchClassPlugin:smile Running smile task for project ':patchClassPlugin'

This example uses the Project.afterEvaluate method to add a closure that will be called immediately after the build.gradle of the Project has been executed. If you want to add a smile task to the specified project, you can add such code directly to the corresponding build.gradle of the project:

afterEvaluate { project -> println "Adding smile task to $project" project.task('smile') { doLast { println "Running smile task for $project" } } }

In addition to the methods provided above, the following can also be used:

gradle.afterProject else { println "Evaluation of $project succeeded" } }

The above code was added to build.gradle in the app root directory of the Android project HotFix subproject, and the results were as follows:

chenyangdeMacBook-Pro:HotFix chenyang$ ./gradlew clean > Configure project :app Evaluation of project ':app' succeeded > Configure project :hackdex Evaluation of project ':hackdex' succeeded > Configure project :patchClassPlugin Evaluation of project ':patchClassPlugin' succeeded
  1. Task can be added to the Project and notified immediately. This can be used to set default values or add behavior before Task in the build file is available.
    The following example sets the srcDir property after adding each Task.
tasks.whenTaskAdded { task -> task.ext.srcDir = 'src/main/java' } task a println "source dir is $a.srcDir"

Operation results:

chenyangdeMacBook-Pro:HotFix chenyang$ ./gradlew a > Configure project :app source dir is src/main/java
  1. Task directed acyclic graph can be notified immediately after construction is completed
    Examples are as follows:
gradle.taskGraph.whenReady { println "task graph build completed" }
  1. Notification can be received immediately before and after the execution of any Task
    The following example records the beginning and end of each Task execution. Note that no matter whether Task completes successfully or fails and exceptions occur, afterTask notifications are received:
task ok task broken(dependsOn: ok) { doLast { throw new RuntimeException('broken') } } gradle.taskGraph.beforeTask { Task task -> println "executing $task ..." } gradle.taskGraph.afterTask { Task task, TaskState state -> if (state.failure) { println "FAILED" } else { println "done" } }

Operation results:

chenyangdeMacBook-Pro:HotFix chenyang$ ./gradlew broken > Task :app:ok executing task ':app:ok' ... done > Task :app:broken executing task ':app:broken' ... FAILED
  1. You can be notified immediately after the build is completed
    The monitoring methods are as follows:
gradle.buildFinished

Operation results:

chenyangdeMacBook-Pro:HotFix chenyang$ ./gradlew clean BUILD SUCCESSFUL in 2s 4 actionable tasks: 3 executed, 1 up-to-date buildResult = Build

The life cycle of Gradle Builder and the monitoring of important nodes in the life cycle are explained above. Here is a summary of the following:


For Android projects, the build.gradle file in the Android project root directory is parsed during the Configuration phase:

// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.0.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() } } task clean(type: Delete) { delete rootProject.buildDir }

The above classpath'com. android. tools. build: gradle: 3.0.0'is used to import the Gradle plug-in library for building Android projects. The parameters behind the classpath are composed of three parts. Let's take a look at the plug-in library. Build script:


Looking at the red box above, I believe you should understand the origin of the following three parts of the classpath.

After importing the Gradle plug-in library used to build Android projects, the next step is to apply the plug-ins in the build.gradle sub-module of Android projects:

apply plugin: 'com.android.application'

You should be familiar with the above sentence, which will execute the application method of the com.android.application plug-in to create the Gradle Task needed to build an Android project. How to create the Gradle Task is described in detail in the following section on the Transform API.

4.3 Building process of Android project

The build Android project also goes through the three stages mentioned above, and the build Android project is completed by executing a Task:

// This command executes assembleFreeWandoujiaRelease Task, // Free Wandoujia Release stands for [build variant](https://developer.android.google.cn/studio/build/build-variants.html) ./gradlew app:aFreeWandoujiaR

Task execution also goes through the above three stages. In the Android project construction process, it is usually necessary to perform a hook specified operation at the beginning or end of a Task. The Task above is composed of a Task collection. To see the order in which Task executes, I added the following code to the module's build.gradle file:

gradle.taskGraph.beforeTask { Task task -> println "executing: $task.name" }

The above function is to print the name of the task before it is executed. Let's take a look at assembleFreeWandoujiaRelease Task and the execution order of the Task it depends on:

chenyangdeMacBook-Pro:HotFix chenyang$ ./gradlew app:aFreeWandoujiaR | grep executing executing: preBuild executing: extractProguardFiles executing: preFreeWandoujiaReleaseBuild executing: compileFreeWandoujiaReleaseAidl executing: compileFreeWandoujiaReleaseRenderscript executing: checkFreeWandoujiaReleaseManifest executing: generateFreeWandoujiaReleaseBuildConfig executing: prepareLintJar executing: generateFreeWandoujiaReleaseResValues executing: generateFreeWandoujiaReleaseResources executing: mergeFreeWandoujiaReleaseResources executing: createFreeWandoujiaReleaseCompatibleScreenManifests executing: processFreeWandoujiaReleaseManifest executing: splitsDiscoveryTaskFreeWandoujiaRelease executing: processFreeWandoujiaReleaseResources executing: generateFreeWandoujiaReleaseSources executing: javaPreCompileFreeWandoujiaRelease executing: compileFreeWandoujiaReleaseJavaWithJavac executing: compileFreeWandoujiaReleaseNdk executing: compileFreeWandoujiaReleaseSources executing: mergeFreeWandoujiaReleaseShaders executing: compileFreeWandoujiaReleaseShaders executing: generateFreeWandoujiaReleaseAssets executing: mergeFreeWandoujiaReleaseAssets executing: processFreeWandoujiaReleaseJavaRes executing: transformResourcesWithMergeJavaResForFreeWandoujiaRelease executing: transformClassesAndResourcesWithProguardForFreeWandoujiaRelease executing: transformClassesWithDexForFreeWandoujiaRelease executing: mergeFreeWandoujiaReleaseJniLibFolders executing: transformNativeLibsWithMergeJniLibsForFreeWandoujiaRelease executing: transformNativeLibsWithStripDebugSymbolForFreeWandoujiaRelease executing: packageFreeWandoujiaRelease executing: lintVitalFreeWandoujiaRelease executing: assembleFreeWandoujiaRelease chenyangdeMacBook-Pro:HotFix chenyang$

You can see that a build process of the Android project executes many Tasks. Here are some key Tasks:
Merge Free Wandoujia Release Resources -- Collect all resources
Process Free Wandoujia Release Manifest -- Generate the final Android Manif. XML file
CompoileFreeWandoujiaRelease JavaWithJavac -- Compiling Java files
Merge Free Wandoujia Release Assets -- Collect all assets
Transf Classes AndResources With Proguard ForFree Wandoujia Release -- Confusion
Transf Classes WithDex ForFree Wandoujia Release -- Generating dex
Packing Free Wandoujia Release -- Packing generates apk
Knowing the Task that the build Android project has been executed, the next hook will follow the instructions in the previous section.

You can view all Gradle Task s in the Android project through the gradle window on the right side of Android Studio, as shown in the following figure:


All Gradle Tasks are listed in groups, which looks clearer, and can run the Gradle Task by double-clicking on a Gradle Task. Isn't it cool?

5. Custom Gradle plug-in

To illustrate the custom Gradle plug-in, I'll ask you a question and solve it in the form of a custom plug-in.

Question: How do I inject the specified code into the constructor of the class file?
Solution:
1 > Solved by hook compileFree Wandoujia Release Java WithJavac.
2 > Transform API is specially provided by Google to solve this problem.
Now that Google provides the Transform API, let's use the Transform API to solve the problem. Let's first look at Google's interpretation of the Transform API:

Starting with 1.5.0-beta1, the Gradle plugin includes a Transform API allowing 3rd party plugins to manipulate compiled class files before they are converted to dex files. (The API existed in 1.4.0-beta2 but it's been completely revamped in 1.5.0-beta1) The goal of this API is to simplify injecting custom class manipulations without having to deal with tasks, and to offer more flexibility on what is manipulated. The internal code processing (jacoco, progard, multi-dex) have all moved to this new mechanism already in 1.5.0-beta1. Note: this applies only to the javac/dx code path. Jack does not use this API at the moment. The API doc is http://google.github.io/android-gradle-dsl/javadoc/. To insert a transform into a build, you simply create a new class implementing one of the Transform interfaces, and register it with android.registerTransform(theTransform) or android.registerTransform(theTransform, dependencies). Important notes: The Dex class is gone. You cannot access it anymore through the variant API (the getter is still there for now but will throw an exception) Transform can only be registered globally which applies them to all the variants. We'll improve this shortly. There's no way to control ordering of the transforms. We're looking for feedback on the API. Please file bugs or email us on our adt-dev mailing list.

Perhaps it means:
Starting with 1.5.0-beta1, the Gradle plug-in contains a Transform API that allows third-party plug-ins to process compiled class files before they are converted to dex files.
The goal of this API is to simplify the operation of custom class files without processing Task, and to provide more operational flexibility. In version 1.5.0-beta1, internal code processing (jacoco, progard, multi-dex) has all been transferred to this new mechanism. To insert Transform into the build process, you just need to create a new class that inherits the Transform Abstract class, and then register it with android.registerTransform (the Transform) or android.registerTransform (the Transform, dependencies).

Gradle provides powerful custom plug-ins. Official Lecture Documents.
Gradle provides three ways to create custom plug-ins:

  1. Include the source code of the plug-in directly in the build script
    The advantage of this is that the plug-in can be automatically compiled and included in the classpath of the build script without performing any operations. But plug-ins are not visible outside the build script, so plug-ins cannot be reused outside the build script.
  2. Create custom plug-ins in buildSrc project
    You can put the source code of the plug-in in the rootProjectDir / buildSrc / src / main / groovy directory. Gradle will be responsible for compiling and testing plug-ins and making them available in the classpath of the build script. The plug-in is visible to every build script used by the build. But it is not visible outside of build, so you can't reuse the plug-in outside of build.
  3. Create custom plug-ins in separate projects (referring to sub-modules in Android projects)
    You can create a separate project for your plug-in. The project generates and publishes a JAR, which you can then use in multiple build s and share with others. Usually the JAR may contain plug-ins, or bundle several related Task classes into a library, or some combination of both.

The first two ways you can refer to Official Lecture Documents I will use the third way to illustrate it directly:

1 > First create a submodule named injectClassPlugin, then delete all the files in the module (except the build. grade file), and then create the following directory:



You can create it according to the directory structure of injectClassPlugin above (the directory structure required by Gradle plug-in).

2 > After the directory structure is created, let's take a look at the plug-in's build.gradle content (as shown in the figure above), which is very simple and has nothing to say. Now let's inherit the Plugin class and implement the first step of the custom plug-in.

class InjectClassPlugin implements Plugin<Project> { @Override void apply(Project project) { //AppExtension corresponds to android {...} in build.gradle def android = project.extensions.getByType(AppExtension) //Register a Transform def classTransform = new InjectClassTransform(project) android.registerTransform(classTransform) // Delivery of custom code to be injected through Extension def extension = project.extensions.create("InjectClassCode", InjectClassExtension) project.afterEvaluate { classTransform.injectCode = extension.injectCode } } }

The above code is very simple, mainly completing two things:

1 Register InjectClassTransform The above Google explanation of the Transform API mentions the way to register Transform, which is called The android.registerTransform (the Transform) method, where Android is the AppExtension type, And it's configured through android {...} in build.gradle, so the code for registering Transform above is well understood. 2 Delivery of custom code to be injected through Extension The Gradle script passes some configuration parameters to the custom plug-in through Extension, and in this case the injected code is passed through the InjectClassExtension object. First add an object named InjectClassCode type InjectClassExtension to the extensions container, and then apply the plug-in The build.gradle execution of the app module (which completes the assignment of the InjectClassCode object, as explained in the third step below) will be completed The injected code is passed to the classTransform object.

3 > Next let's look at the build.gradle and InjectClassExtension classes of app module:

apply plugin: 'com.android.application' apply plugin: 'com.cytmxk.injectclassplugin' android { compileSdkVersion 26 buildToolsVersion "26.0.2" defaultConfig { applicationId "com.cytmxk.hotfix" minSdkVersion 16 targetSdkVersion 26 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } debug { signingConfig android.signingConfigs.debug } } flavorDimensions "tier", "channel" productFlavors { free { dimension "tier" applicationIdSuffix ".free" versionNameSuffix "-free" } paid { dimension "tier" applicationIdSuffix ".paid" versionNameSuffix "-paid" } wandoujia { dimension "channel" } market91 { dimension "channel" } } } InjectClassCode { injectCode = """ android.widget.Toast.makeText(this,"test Toast Code!!",android.widget.Toast.LENGTH_SHORT).show(); """ } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support.constraint:constraint-layout:1.0.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' } /--------------------------------------------/ package com.cytmxk class InjectClassExtension { String injectCode }

The InjectClassCode closure above completes the assignment of the InjectClassCode object in the second step.

4 > Next, let's look at the implementation of InjectClassTransform:

/** * Used to inject the specified code into each calss file */ class InjectClassTransform extends Transform{ Project project String injectCode; InjectClassTransform(Project project) { this.project = project } /** * Set the Task name for our custom Transform, similar to: Transform Classes WithPreDex InjectCode ForXXX * @return */ @Override String getName() { return "PreDexInjectCode" } /** * Data types to be processed, CONTENT_CLASS represents processing class files * @return */ @Override Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS } /** * Specify the scope of the Transform * @return */ @Override Set<? super QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT } /** * Indicates whether the current Transform supports incremental compilation * @return */ @Override boolean isIncremental() { return false } @Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { super.transform(transformInvocation) transformInvocation.inputs.each { TransformInput input -> //traverse folder input.directoryInputs.each { DirectoryInput directoryInput -> //Injection code InjectClass.inject(directoryInput.file.absolutePath, project, injectCode) // Get the output directory def dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) // Copy the input directory to the output specified directory FileUtils.copyDirectory(directoryInput.file, dest) } //Traveling through jar files does not operate on jar, but it outputs to the out path input.jarInputs.each { JarInput jarInput -> // Rename the output file (copyFile conflicts with the same name file) def jarName = jarInput.name println("jar = " + jarInput.file.getAbsolutePath()) def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath()) if (jarName.endsWith(".jar")) { jarName = jarName.substring(0, jarName.length() - 4) } def dest = transformInvocation.outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR) FileUtils.copyFile(jarInput.file, dest) } } } }

Registering InjectClassTransform in the first step generates a Task with the corresponding name Transform Classes WithPreDex InjectCode For XXX, which represents the app module build variant When the Task is executed, the transform method of InjectClassTransform will be called. The annotations above are very clear and will not be repeated. Let's take a look at the implementation of injection code:

class InjectClass { //Initialization class pool private final static ClassPool pool = ClassPool.getDefault() static void inject(String path,Project project, String injectCode) { println("filePath = " + path) //Add the current path to the class pool, otherwise the class cannot be found pool.appendClassPath(path) //project.android.bootClasspath joins android.jar, otherwise all classes related to Android cannot be found pool.appendClassPath(project.android.bootClasspath[0].toString()) File dir = new File(path) if (dir.isDirectory()) { //traverse folder dir.eachFileRecurse { File file -> if (file.getName().equals("MainActivity.class")) { //Get MainActivity.class CtClass ctClass = pool.getCtClass("com.cytmxk.hotfix.MainActivity") println("ctClass = " + ctClass) //Thaw if (ctClass.isFrozen()) ctClass.defrost() //Get the OnCreate method CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate") println("Method name = " + ctMethod) println "injectCode = " + injectCode //Start injecting code into the method ctMethod.insertBefore(injectCode) ctClass.writeFile(path) ctClass.detach()//release } } } } }

The above code injects the code into the onCreate method of com.cytmxk.hotfix.MainActivity.class, and then builds the app module with the following command:

./gradlew app:assembleFreeWandoujiaRelease

Then let's look at the com.cytmxk.hotfix.MainActivity.class file:



You can see that the code has been injected.

9 January 2019, 04:48 | Views: 9267

Add new comment

For adding a comment, please log in
or create account

0 comments