preface

Gradle is an automated build tool that links the steps of compiling, testing, and deploying software together.

Build. Gradle Android {} and dependencies{}. Do you know what you can do in this process?

This article is a study note for learning Gradle. It will give you a new understanding of Gradle and help Gradle build your projects faster and more efficiently. At this time to share with you, with everyone

The main content of this note is as follows

  • Gradle is the most basic project configuration

  • Apply plugin: ‘XXXX’ and dependencies{}

  • Gradle Project/Task and custom tasks and plugins

  • Customize a plug-in process to rename your APP

  • APT technology – Java AbstractProcessor

  • Android Bytecode Enhancement – Transform (Android bytecode Enhancement)

This article is a bit longer, so if you already know the basics of Gradle, you can go directly to the directory to see what you want to see. It’s a great way to review and study.

First introduction to Gradle project build configuration

Gralde project structure

  1. Green: Gralde version configuration and scripts required by Gralde. Gradlew is a script for Linux/MAC, and gradle.bat is a script for Windows
  2. Gradle in red: Settings. gradle for the root project, build.gradle for the root project, build.gradle for the inner project

Gradle configuration order

Gralde project configuration identifies settings.gradle first and then configures each build.gradle. To illustrate the order of build execution, the corresponding code is set up in the basic Gradle project structure above

// settings.gradle
println "settings.gradle start"
include ':app'
println "settings.gradle end"
Copy the code
//root build.gradle
println "project.root start"
buildscript {
    repositories{}dependencies{}}allprojects{}println "project.root end"
Copy the code
//app build.gradle
println "project.app start"
project.afterEvaluate {
    println "project.app.afterEvaluate print"
}
project.beforeEvaluate {
    println "project.app.beforeEvaluate print"
}
println "project.app end"
Copy the code

For MAC/Linux, execute./gradlew to get the following result:

settings.gradle start
settings.gradle end

> Configure project :
project.root start
project.root end

> Configure project :app
project.app start
project.app end
project.app.afterEvaluate print
Copy the code

Groovy syntax

Here are some examples of groovy syntax. You can practice groovy syntax by opening the Android Studio Tools-> Groovy Console

Optional type definition that can omit the statement terminator semicolon (;)

int vs = 1
def version = 'version1'

println(vs)
println(version)
Copy the code

Parentheses are also optional

println vs
println version
Copy the code

String definition

def s1 = 'aaa'
def s2 = "version is ${version}"
def s3 = ''' str
is
many
'''
println s1
println s2
println s3
Copy the code

A collection of

def list = ['ant'.'maven']
list << "gradle"
list.add('test')
println list.size()
println list.toString()
//map
def years = ['key1':1000."key2":2000]
println years.key1
println years.getClass()
Copy the code

The output

[ant, maven, gradle, test]
1000
class java.util.LinkedHashMap
Copy the code

closure

The Groovy syntax supports closures, which are simply code blocks like this:

def v = {
    v -> println v
}
static def testMethod(Closure closure){
    closure('closure test')
}
testMethod v
Copy the code

Where v is defined as a closure, testMethod is a method, passing arguments to the closure, and then calling the closure.

Apply plugin: ‘XXXX’ and dependencies{}

To prepare, look at gradle’s source code

Let’s first change the build.gradle of the subproject to the following form

apply plugin: 'java-library'
repositories {
    mavenLocal()
}
dependencies {
    compile gradleApi()
}
Copy the code

In this way, you can directly view the source code of Gradle in External Libraries as follows

explain

Go to Build. gradle and click Apply to enter gradle source code

//PluginAware
 /** * Applies a plugin or script, using the given options provided as a map. Does nothing if the plugin has already been applied. * <p> * The given map is  applied as a series of method calls to a newly created {@link ObjectConfigurationAction}.
     * That is, each key in the map is expected to be the name of a method {@link ObjectConfigurationAction} and the value to be compatible arguments to that method.
     *
     * <p>The following options are available:</p>
     *
     * <ul><li>{@code from}: A script to apply. Accepts any path supported by {@link org.gradle.api.Project#uri(Object)}.</li>
     *
     * <li>{@code plugin}: The id or implementation class of the plugin to apply.</li>
     *
     * <li>{@codeto}: The target delegate object or objects. The default is this plugin aware object. Use this to configure objects other than  this object.</li></ul> * *@param options the options to use to configure and {@linkObjectConfigurationAction} before "executing" it * /
    void apply(Map
       
         options)
       ,>;
Copy the code

Groovy syntax makes it clear that apply is just a method, followed by a map, in which plugin is the key.

The same goes for Dependencies {}

//Project
/**
     * <p>Configures the dependencies for this project.
     *
     * <p>This method executes the given closure against the {@link DependencyHandler} for this project. The {@link
     * DependencyHandler} is passed to the closure as the closure's delegate.
     *
     * <h3>Examples:</h3>
     * See docs for {@link DependencyHandler}
     *
     * @param configureClosure the closure to use to configure the dependencies.
     */
    void dependencies(Closure configureClosure);
Copy the code

Dependencies are a method followed by arguments to a closure.

Question: Does android {} implement the same? Covered later

Gradle Project/Task

Gradle = setting.gradle = build.gradle = setting.gradle = setting.gradle = setting.gradle = build.gradle The outer build.gradle is the root Project and the inner one is the child Project. There can only be one root Project and multiple child projects.

Now that we know the basics of Gradle configuration, how can we use gradle for our own good?

Plugin

Apply plugin:’ XXXX ‘, these plugins are implemented according to gradle specification, Java and Android, so let’s implement a plugin of our own.

Change build.gradle to the following code

//app build.gradle
class LibPlugin implements Plugin<Project>{
    @Override
    void apply(Project target) {
        println 'this is lib plugin'
    }
}
apply plugin:LibPlugin
Copy the code

Run./gradlew and the result is as follows

> Configure project :app
this is lib plugin
Copy the code

The Extension of the Plugin

We need to obtain the configuration of Project in the custom Plugin, and we can obtain some basic configuration information through Project. Then how to configure and obtain some attributes we want to customize? At this time, we need to create Extension, and change the above code into the following form.

//app build.gradle
class LibExtension{
    String version
    String message
}
class LibPlugin implements Plugin<Project>{
    @Override
    void apply(Project target) {
        println 'this is lib plugin'
        / / create the Extension
        target.extensions.create('libConfig',LibExtension)
        // Create a task
        target.tasks.create('libTask', {doLast{
               LibExtension config = project.libConfig
               println config.version
               println config.message
           }
        })
    }
}
apply plugin:LibPlugin
/ / configuration
libConfig {
    version = '1.0'
    message = 'lib message'
}
Copy the code

After the configuration, run the./gradlew libTask command to obtain the following result

> Configure project :app
this is lib plugin
> Task :lib:libTask
1.0
lib message
Copy the code

Android {} is an Extension created by plugin ‘com.android. Application ‘or ‘com.android. Library’.

Task

In the above code, a task named libTask is created. Gradle can create tasks in many different ways. The TaskContainer class provides the interface for creating tasks

//TaskContainer
Task create(Map
       
         options)
       ,> throws InvalidUserDataException;
Task create(Map
       
         options, Closure configureClosure)
       ,> throws InvalidUserDataException;
Task create(String name, Closure configureClosure) throws InvalidUserDataException;
Task create(String name) throws InvalidUserDataException;
<T extends Task> T create(String name, Class<T> type) throws InvalidUserDataException;
<T extends Task> T create(String name, Class<T> type, Action<? super T> configuration) throws InvalidUserDataException;
Copy the code

A Project can’t run, so we need to define tasks to compile, run, package, etc. The com.android.application plugin defines a packaged task for us such as Assemble. The plugin we just defined adds a libTask for us to output.

Task API

The doLast API can be called directly in the created task because the doLast API is in the task class. You can view the corresponding code and see the corresponding API

Gradle tasks

Gradle defines common tasks such as clean, copy, and so on. These tasks can be created by name as follows:

task clean(type: Delete) {
    delete rootProject.buildDir
}
Copy the code

Depend on the task

We know Android uses assemble’s associated task when packaging, but it can’t be packaged directly, it relies on other tasks. So how do you create a dependent Task? The following code

task A{
    println "A task"
}
task B({
    println 'B task'
},dependsOn: A)
Copy the code

Execute./graldew B output

A task
B task
Copy the code

Customize a plugin to rename your APP

With a little primer on how Gradle is built, let’s start with a plugin that defines the android packaging process and renames your APP.

It is OK to write the Plugin directly in build.gradle, so how can we extract it for higher reusability?

The following

Which build. Gradle as

apply plugin: 'groovy'
apply plugin: 'maven'
repositories {
    mavenLocal()
    jcenter()
}

dependencies {
    compile gradleApi()
}

def versionName = "0.0.1"
group "com.ding.demo"
version versionName
uploadArchives{ // The current project can be published to a local folder
    repositories {
        mavenDeployer {
            repository(url: uri('.. /repo')) // Define the address of the local Maven repository}}}Copy the code

Apkname. The properties of

implementation-class=com.ding.demo.ApkChangeNamePlugin
Copy the code

ApkChangeNamePlugin

package com.ding.demo

import org.gradle.api.Project
import org.gradle.api.Plugin



class ApkChangeNamePlugin implements Plugin<Project> {

    static  class ChangeAppNameConfig{
        String prefixName
        String notConfig
    }

    static def buildTime() {
        return new Date().format("yyyy_MM_dd_HH_mm_ss", TimeZone.getTimeZone("GMT+8"))}@Override
    void apply(Project project) {
        if(! project.android){throw new IllegalStateException('Must apply \'com.android.application\' or \'com.android.library\' first! ');
        }
        project.getExtensions().create("nameConfig",ChangeAppNameConfig)
        ChangeAppNameConfig config
        project.afterEvaluate {
            config = project.nameConfig
        }
        project.android.applicationVariants.all{
            variant ->
                variant.outputs.all {
                    output ->
                        if(output.outputFile ! =null && output.outputFile.name.endsWith('.apk')
                                && !output.outputFile.name.contains(config.notConfig)) {
                            def appName = config.prefixName
                            def time = buildTime()
                            String name = output.baseName
                            name = name.replaceAll("-"."_")
                            outputFileName = "${appName}-${variant.versionCode}-${name}-${time}.apk"
                        }
                }
        }
    }
}
Copy the code

After the definition is complete, execute./gradlew uploadArchives to generate the corresponding plug-in in this directory

Application plug-ins are configured at the root build.gralde

buildscript {
    repositories {
        maven {url uri('./repo/')}
        google()
        jcenter()
    }
    dependencies {
        classpath 'com. Android. Tools. Build: gradle: 3.4.1 track'
        classpath 'com. Ding. Demo: apkname: 0.0.1'}}Copy the code

In the app. Gralde Settings

apply plugin: 'apkname'
nameConfig{
    prefixName = 'demo'
    notConfig = 'debug'
}
Copy the code

Gradle doc’s official website

That’s about all the basic API for Gradle.

Website address: docs.gradle.org/current/use… You can go to view the corresponding API, or directly through the source code to view

But that’s not the end of the notes. After learning the basics of Gradle, we need to make it work for us. Here are some practical applications.

APT technology

www.jianshu.com/p/94aee6b02… Blog.csdn.net/kaifa1321/a…

APT Annotation Processing Tool APT is a technique for parsing annotations at compile time and generating code. Some commonly used IOC framework implementation principle is it, the famous ButterKnife, Dagger2 is implemented with this technology, some injection in SpringBoot is also used for injection.

Service Provider Interface (SPI) automatically loads classes defined in files by searching for them in the meta-INF /** folder in the ClassPath path. The above custom ApkNamePlugin is implemented using this mechanism, as follows.

To implement an APT is also required to implement this technology, but Google has redefined this using APT technology, defined an auto-service, can simplify the implementation, the following implementation of a simple Utils document generation tool.

Utils document generation plug-in

We know that there may be a lot of Utils in the project, and every time a new employee or an old employee cannot complete it, they may repeatedly add some Utils, such as the density of the screen, the height of the box has a lot of Utils. We use a small plugin to generate a document that is easy to see at a glance when using Utils.

Create a New Java Libary called DocAnnotation

Define an annotation

@Retention(RetentionPolicy.CLASS)
public @interface GDoc {
   String name(a) default "";

   String author(a) default "";

   String time(a) default "";
}
Copy the code

Create a Java Libary called DocComplie

Then we introduced Google auto-service, we introduced DocAnnotation

apply plugin: 'java-library' dependencies { implementation fileTree(dir: 'libs', include: [' *. Jar ']) implementation 'com. Google. Auto. Services: auto - service: 1.0 rc2' implementation 'com. Alibaba: fastjson: 1.2.34' implementation project(':DocAnnotation') }Copy the code

Define an Entity class

public class Entity {

    public String author;
    public String time;
    public String name;
}
Copy the code

Defining annotation handlers

@AutoService(Processor.class) // This annotation is the SPI function provided by auto-service
public class DocProcessor extends AbstractProcessor{

    Writer docWriter;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);

    }

    @Override
    public Set<String> getSupportedAnnotationTypes(a) {
        // A collection of annotations that can be processed
        HashSet<String> annotations = new HashSet<>();
        String canonicalName = GDoc.class.getCanonicalName();
        annotations.add(canonicalName);
        return annotations;
    }

    @Override
    public SourceVersion getSupportedSourceVersion(a) {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        Messager messager = processingEnv.getMessager();
        Map<String,Entity> map = new HashMap<>();
        StringBuilder stringBuilder = new StringBuilder();
        for (Element e : env.getElementsAnnotatedWith(GDoc.class)) {
            GDoc annotation = e.getAnnotation(GDoc.class);
            Entity entity = new Entity();
            entity.name = annotation.name();
            entity.author = annotation.author();
            entity.time = annotation.time();
            map.put(e.getSimpleName().toString(),entity);

            stringBuilder.append(e.getSimpleName()).append("").append(entity.name).append("\n");
        }

        try {
            docWriter = processingEnv.getFiler().createResource(
                    StandardLocation.SOURCE_OUTPUT,
                    ""."DescClassDoc.json"
            ).openWriter();

            //docWriter.append(JSON.toJSONString(map, SerializerFeature.PrettyFormat));
            docWriter.append(stringBuilder.toString());
            docWriter.flush();
            docWriter.close();
        } catch (IOException e) {
            //e.printStackTrace();
            // Write failed
        }
        return true; }}Copy the code

Project reference

dependencies {
    implementation project(':DocAnnotation')
    annotationProcessor project(':DocComplie')}Copy the code

Apply an Utils

@GDoc(name = "Color Tools",time = "September 18, 2019 19:58:07",author = "dingxx")
public final class ColorUtils {}Copy the code

The resulting document is as follows:

ColorUtils Color tool class dingxxCopy the code

Of course, the final generated document can be determined by yourself, or directly HTML, etc.

Android Transform

Before we get to Android Transform, let’s look at the Packaging process for Android, when executing Task Assemble

While compiling. Class /jar/resources, apply plugin: ‘. Com. Android application this plug-in supports defines a callback (com. Android. View the build: gradle: 2. More than xx), similar to the interceptor, can define your own some processing, This is called Transform for Android

At this point, we can dynamically modify these classes to accomplish some of the things we want to do, such as fixing third-party library bugs, automatic burying points, adding function execution time to third-party libraries, implementing dynamic AOP, etc.

ARoute is known to have used this technique. Of course, he used Mr APT as a routing file and then loaded by Transform.

The following is a quote from ARoute ReadMe

Automatic loading of routing tables using Gradle plug-ins (optional)

apply plugin: 'com.alibaba.arouter'
buildscript {
    repositories {
       jcenter()
    }

    dependencies {
        classpath "com.alibaba:arouter-register:?"}}Copy the code

Optional, power by AutoRegister routing table loading via ARouter’s plug-in By default, it is loaded by scanning dex. Automatic registration by using Gradle plug-in can shorten the initialization time. It can solve the problem that you cannot directly access dex files due to application hardening.

ARoute’s LogisticsCenter shows that if the trasnform plugin is not used in init, it will iterate through all dex to find the relevant classes referenced by ARoute when registering

//LogisticsCenter
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
  if (registerByPlugin) {
        logger.info(TAG, "Load router map by arouter-auto-register plugin.");
    } else {
        Set<String> routerMap;

        // It will rebuild router map every times when debuggable.
        if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
             logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
            // These class was generated by arouter-compiler.
            routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
            if(! routerMap.isEmpty()) { context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP,routerMap).apply(); } PackageUtils.updateVersion(context);// Save new version name when router map update finishes.
        } else {
            logger.info(TAG, "Load router map from cache.");
            routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, newHashSet<String>())); }}... }Copy the code

Android Transform

If you want to process a.class file or jar file, you need a bytecode processing tool

  • ASM
  • Javassist
  • AspectJ

For more details, check out Meituan’s tweet Java bytecode Enhanced Exploration

How to define a Trasnfrom within, review the gradle Plugin implementation above, see the following code

public class TransfromPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        AppExtension appExtension = (AppExtension) project.getProperties().get("android");
        appExtension.registerTransform(new DemoTransform(), Collections.EMPTY_LIST);
    }
    
    class DemoTransform extends Transform{

        @Override
        public String getName(a) {
            return null;
        }

        @Override
        public Set<QualifiedContent.ContentType> getInputTypes() {
            return null;
        }

        @Override
        public Set<? super QualifiedContent.Scope> getScopes() {
            return null;
        }

        @Override
        public boolean isIncremental(a) {
            return false; }}}Copy the code

Combined with bytecode increase technology, you can achieve some of the dynamic AOP, due to space reasons, here is not detailed notes out, if you want to further study, recommend ARoute author of a buddy wrote AutoRegister, you can see the source code

conclusion

By now, Gradle’s study notes are basically finished. Due to the author’s limited level, if there are any mistakes in this article, please correct them. Thank you. I also recommend reading my other article, Android photoshop (background change, smudge repair)