Android Gradle learning notes sorting

Posted by Med on Tue, 14 Dec 2021 14:41:27 +0100

preface

Gradle is an automated build tool that links the steps of software compilation, testing and deployment.

For android developers, you already know build Gradle's android {} and dependencies {}, but what's his compilation process like? What can be done in this process?

This article is a learning note when learning Gradle, so that you can re understand Gradle and make Gradle speed up and improve the construction of your project. At this time, I will share with you and encourage you

The main contents of this note are as follows

  • Gradle's most basic project configuration
  • Groovy basic syntax and explain the apply plugin: 'xxxx' and dependencies {}
  • Gradle Project/Task and customize Task and Plugin
  • Customize a plug-in process to rename the APP name
  • APT technology - Java AbstractProcessor
  • Android bytecode enhancement technology - Transform (bytecode enhancement technology used in Android)

The content of the article is slightly longer. If you have mastered the basic knowledge of Gradle, you can directly view the content you want to see through the directory. It's good to review or study.

Get acquainted with Gradle project construction configuration

Grade project structure

As shown in the figure, it is a relatively small gradle configuration. Here we mainly talk about two parts

  1. The green part: the version configuration of grade and the scripts required for grade, where gradlew is the script under linux/mac, gradle Bat is the script required under windows
  2. Red part: settings Gradle is the project configuration of the root project, and the outer layer is build Gradle is the configuration of the root project, and the inner build Gradle is the configuration of subprojects

gradle configuration sequence

The item configuration of grade is to identify settings first Gradle, and then configure each build gradle.

To illustrate the build execution sequence, the corresponding code is set in the most basic gradle project structure above

// settings.gradle
println "settings.gradle start"
include ':app'
println "settings.gradle end"
//root build.gradle
println "project.root start"
buildscript {
    repositories {
    }
    dependencies {
    }
}

allprojects {
}
println "project.root end"
//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"

If it is mac/linux, execute/ gradlew got the following results:

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

Groovy syntax

Let's talk about some Groovy syntax. You can open Android studio tools - > Groovy console to practice Groovy syntax, as follows

Optional type definitions. Statement terminator semicolon (;) can be omitted

int vs = 1
def version = 'version1'

println(vs)
println(version)

Parentheses are also optional

println vs
println version

String definition

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

aggregate

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()

Output results

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

closure

groovy syntax supports closure syntax. Closure is simply a code block, as follows:

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

The v defined is closure, testMethod is a method, the incoming parameter is a closure, and then the closure is called.

Explain the apply plugin: 'xxxx' and dependencies {}

Preparatory work, look at the source code of gradle

Let's start with the sub project build Change gradle to the following form

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

In this way, we can directly look at the source code of gradle, as follows in External Libraries

explain

Enter build Gradle click apply to enter the gradle source code, which you can see

//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>{@code to}: 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 {@link ObjectConfigurationAction} before "executing" it
     */
    void apply(Map<String, ?> options);

It is clearly explained in Groovy syntax that apply is actually a method, followed by a map, in which the plugin is the key

So are 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);

dependencies is a method, followed by a closure parameter

Question: think about it. Is android {} the same implementation? Explain later

Gradle Project/Task

As mentioned in the previous chapter, the initialization configuration of grade is to parse and execute setting Gradle, and then execute build Gradle, so actually these build Gradle is Project, the outer build Gradle is the root Project, and the inner layer is the child Project. There can only be one root Project and multiple child projects

We know the most basic gradle configuration, so how to use some things in gradle to serve us?

Plugin

As mentioned earlier, apply plugins: 'XXXX'. These plugins are implemented according to the gradle specification, including java and Android. Let's implement our own plugin

Put build Change 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

Run/ gradlew's results are as follows

> Configure project :app
this is lib plugin

Plugin Extension

To obtain the Project configuration in the customized Plugin, we can obtain some basic configuration information through Project. How can we configure and obtain some properties we want to customize? At this time, we need to create an Extension and change the above code to 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 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
//to configure
libConfig {
    version = '1.0'
    message = 'lib message'
}

After configuration, execute/ gradlew libTask gets the following results

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

After reading the above code, we know that Android {} is actually an Extension. It is created by plugin 'com android. Application 'or' com android. Library 'is created.

Task

In the above code, a task named libTask is created. There are many ways to create a task in gradle. The specific creation interface is in the TaskContainer class

//TaskContainer
Task create(Map<String, ?> options) throws InvalidUserDataException;
Task create(Map<String, ?> 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;

Project cannot run, so we need to define some tasks to complete our compilation, running, packaging, etc. com. android. The application plug-in defines packaging tasks for us, such as assembly. The plug-in we just defined adds a libTask for us to output.

Task API

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

Some tasks of Gradle

gradle defines some common tasks for us, such as clean and copy. These tasks can be directly created by name, as follows:

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

Dependent task

We know that Android will use assembly related tasks when packaging, but it can't package directly. It will rely on other tasks So how to create a dependent task? The code is as follows

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

Execution/ Gradew B output

A task
B task

Customize a plug-in to rename the APP name

Through the above introductory explanations, we probably know how gradle is built. Now we define a plug-in that renames the APP name in the Android packaging process.

The above is in build It's OK for gradle to write Plugin directly, so how can we take this out in order to make it more reusable?

as follows

Including build Gradle is

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 warehouse
        }
    }
}

apkname.properties is

implementation-class=com.ding.demo.ApkChangeNamePlugin

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"
                        }
                }
        }
    }
}

When the definition is complete, execute/ gradlew uploadArchives will generate the corresponding plug-ins in this directory

Apply the plug-in in the root build Grade configuration

buildscript {
    repositories {
        maven {url uri('./repo/')}
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.4.1'
        classpath 'com.ding.demo:apkname:0.0.1'
    }
}

On app Grade settings

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

Gradle doc official website

The basic API of Gradle is almost finished.

Official website address: docs.gradle.org/current/use...

You can view the corresponding API or directly through the source code

But the notes are not finished. After learning the basics of Gradle, we want it to serve us. Here are some practical applications

APT Technology

The full name of APT is Annotation Processing Tool, which is a technology for parsing annotations and generating code during compilation. It is the implementation principle of some commonly used IOC frameworks. The famous ButterKnife and Dagger2 are implemented with this technology, and some injections in SpringBoot are also injected with it

Before introducing APT, let's first introduce SPI (Service Provider Interface), which automatically loads the classes defined in the file by searching the file in the META-INF / * * folder under the ClassPath path. The above customized ApkNamePlugin is implemented using this mechanism, as follows

SPI technology has also been used to decouple in the process of componentization.

This technology is also needed to implement an apt, but Google has redefined this using APT Technology and defined an auto service, which can simplify the implementation. Here is a simple Utils document generation tool.

Utils document generation plug-in

We know that there may be many Utils in the project. Whenever new employees or old employees can't complete them, we know that there are those Utils. We may repeatedly add some Utils, such as obtaining the density of the screen and many Utils in the box height We use a small plug-in to generate a document. When using Utils, we can see the document at a glance

Create a new Java Libary named DocAnnotation

Define an annotation

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

   String author() default "";

   String time() default "";
}

Create a new Java Libary named DocComplie

Then introduce Google's auto service and DocAnnotation

apply plugin: 'java-library'
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    implementation 'com.alibaba:fastjson:1.2.34'
    implementation project(':DocAnnotation')
}

Define an Entity class

public class Entity {

    public String author;
    public String time;
    public String name;
}

Define annotation processor

@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 collection of treatable annotations
        HashSet<String> annotations = new HashSet<>();
        String canonicalName = GDoc.class.getCanonicalName();
        annotations.add(canonicalName);
        return annotations;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        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;
    }
}

Reference in project

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

Apply a Utils

@GDoc(name = "Color tool class",time = "2019 September 18, 2009:58:07",author = "dingxx")
public final class ColorUtils {
}

The final generated documents are as follows:

name              function            author
ColorUtils      Color tool class        dingxx

Of course, the final generated document can be determined by yourself or directly html

Android Transform

Before talking about Android Transform, first introduce the Android packaging process when executing task assembly

Yes During the compilation of class /jar/resources, apply plugin: 'com android. Application 'this plug-in supports the definition of a callback (com.android.tools.build:gradle:2.xx above), which is similar to an interceptor. You can carry out some definition processing yourself. This is called Android Transform

At this time, we can dynamically modify these class es and complete some things we want to do, such as repairing bug s in third-party libraries, automatically burying points, adding functions to third-party libraries, taking time to execute, completing dynamic AOP, etc

The ARoute we know uses this technology Of course, he first uses APT to generate a routing file, and then loads it through Transform

The following is quoted from ARoute ReadMe

Automatic loading of routing table using Gradle plug-in (optional)

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

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

Optional. The routing table is automatically loaded (power by AutoRegister) through the registration plug-in provided by ARouter. By default, it is loaded by scanning dex. Automatic registration through the gradle plug-in can shorten the initialization time and solve the problem that the application reinforcement can not directly access the dex file and the initialization fails. It should be noted that, The plug-in must be combined with API 1.3 Use version above 0!

From the logistics center of ARoute, you can see that if the plugin of trasnform is not used during init, it will traverse all dex and find the relevant classes referenced by ARoute during registration, as shown below

//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, new HashSet<String>()));
        }
    }
    ....
}

Introduction to the implementation of Android Transform

Through the above, we know that the callback is class file or jar file, then it needs to be processed class files or jar files need tools related to bytecode processing. There are commonly used tools related to bytecode processing

  • ASM
  • Javassist
  • AspectJ

For specific details, you can view meituan's tweets Java bytecode enhanced exploration

How to define a Trasnfrom, review the above gradle plugin implementation and 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() {
            return null;
        }

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

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

        @Override
        public boolean isIncremental() {
            return false;
        }
    }
}

This article is transferred from https://juejin.cn/post/6844903944439726087 , in case of infringement, please contact to delete.