Gradle packaging process (2) --- further understanding of gradle

1. What is gradle

1. Gradle is an automated build tool.

Gradle finally completes the automated construction by organizing a series of tasks, so task is the most important concept in gradle. Taking generating an available apk as an example, the whole process has to go through resource processing, javac compilation, dex packaging, apk packaging, signing and other steps, and each step corresponds to a task in gradle.

2. Gradle is written in groovy or kotlin.

Groovy is a DSL. So what are DSLs? DSL is the abbreviation of Domain Specific Language, that is, domain-specific language, which is a computer language specially designed to solve a certain type of task. DSL is simple to use and relatively concise in definition.

3. Whether it is written based on groovy or kotlin, it is a jvm-based language.

Therefore, they are all object-oriented in nature. The object-oriented feature is that everything is an object. Therefore, in gradle, the essence of the .gradle script is the definition of the class. The essence of some configuration items is method calls, and the parameters are the following {}Closure. For example, build.gradle corresponds to the Project class, and buildScript corresponds to the Project.buildScript method.

Two, gradle project analysis

First look at the project hierarchy of gradle, the following is an example picture:

 It can be viewed in conjunction with the gradle packaging process (1).

1、settings.gradle

settings.gradle is the script responsible for configuring the project.

Corresponding to the Settings class, during the gradle construction process, the Settings object will be generated according to settings.gradle, and there are several main methods:

  • include(projectPaths)
  • includeFlat(projectNames)
  • project(projectDir)

Generally, the method of referencing sub-project modules seen in the project is to use include, so that the sub-module is located one level below the root directory

include ':app'

If multiple projects, something like

include ':app', ':library'

If you want to specify the location of the submodule, you can use the project method to get the Project object and set its projectDir parameter

include ':app'
project(':app').projectDir = new File('./app')

2、rootproject/build.gradle

build.gradle is responsible for some configurations of the overall project, corresponding to the Project class.

When gradle is built, the Project object will be generated according to build.gradle, so the DSL written in build.gralde is actually some methods of the Project interface. Project is actually an interface, and the real implementation class is DefaultProject. Project has several main methods:

  • buildscript //classpath of the configuration script
  • allprojects //Configuration project and its subprojects
  • respositories //Configure the address of the warehouse, and all subsequent dependencies will go to the address configured here to find
  • dependencies //Configuration project dependencies

Continuing from the example diagram above that introduces the gradle project hierarchy, the corresponding project UI example, the content of rootproject/build.gradle is as follows:

// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply from:"config.gradle" //引入config.gradle
buildscript {  //配置项目的classpath
    repositories {  //项目的仓库地址,会按顺序依次查找
        google()
        jcenter()
    }
    dependencies {  //项目的依赖
        classpath "com.android.tools.build:gradle:4.1.2"

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

3、module/build.gradle

build.gradle is the configuration of the module, corresponding to the Project class.

Compared with the top-level build.gradle, the module-level build.gradle is similar, but we can also see an obvious difference, that is, in the module-level build.gradle, a plug-in apply plugin is referenced: 'com.android. application' or apply plugin: 'com.android.library' or apply plugin: 'java-library' etc. For example, apply plugin: 'com.android.application', the following android dsl is the extension of the application plugin.

There are several main methods:

  • compileSdkVersion //Compile the sdk version, that is, specify the Android API level used by gradle to compile the application
  • defaultConfig //default configuration
  • buildTypes //Compile type, generally debug and release
  • productFlavor //product variant

Let's look at the build.gradle of the app module in the project UI,

plugins {
    id 'com.android.application'
}  //引入android gradle插件

android {  //配置android gradle plugin需要的内容
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {  //默认配置
        applicationId "com.zdj.uidemo"
        minSdkVersion 23
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {  //编译类型
        release {  //release版本
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {  //指定java版本
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {  //模块需要的依赖

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'com.google.android.material:material:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.navigation:navigation-fragment:2.2.2'
    implementation 'androidx.navigation:navigation-ui:2.2.2'
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    if (rootProject.ext.isModule.toBoolean()) {
        implementation project(':phone')
    } else {
        implementation project(':zdjuilibrary')
    }
}

4. Dependence 

new configuration

deprecated configuration

Behavior

effect

implementation

compile

Dependencies are available to the module at compile time, and are only available to consumers of the module at runtime. For large multi-project builds, using the implementation instead of api/compile can significantly improve build times because it reduces the amount of projects that the build system needs to recompile. Most application and test modules should use this configuration.

The api will only be exposed to directly dependent modules. With this configuration, after the module is modified, only the directly dependent modules will be recompiled, and the indirectly dependent modules do not need to be changed.

api

compile

Dependencies are available to the module at compile time, and are also available to consumers of the module at compile time and runtime. This configuration behaves like compile (now deprecated), and should generally only be used in library modules. Application modules should use the implementation unless they want to expose their API to a separate test module.

The api will be exposed to indirectly dependent modules. With this configuration, after the module is modified, both the direct and indirect dependencies of the module need to be recompiled.

compileOnly

provided

A dependency is only available to a module at compile time, and not to its consumers at compile or runtime. This configuration behaves like provided (now deprecated).

Only depend on modules during compilation, and will not depend on runtime after packaging, which can be used to solve some library conflicts.

runtimeOnly

apk

Dependencies are only available to the module and its consumers at runtime. This configuration behaves like apk (now deprecated).

Modules are only depended on at runtime, not at compile time.

5、flavor

Several concepts: flavor, dimension, variant.

After android gradle plugin 3.x, each flavor must correspond to a dimension, which can be understood as a grouping of flavors, and then two groups of flavors in different dimensions are combined to form a variant.

Examples are as follows:

flavorDimensions "size", "color"
productFlavors {
    big {
        dimension "size"
    }
    small {
        dimension "size"
    }
    red {
        dimension "color"
    }
    green {
        dimension "color"
    }
}

Then the generated variant corresponds to bigRed, bigGreen, smallRed, smallGreen.

Each variant can use variantImplementation to introduce specific dependencies, such as:

bigRedImplementation will only be introduced when compiling the bigRed variant.

Three, gradle wrapper 

The gradlew/gradlew.bat file is used to download a specific version of gradle and then execute it. It does not require developers to install gradle locally. The advantage of this is that when developers install gradle locally, they will encounter problems that different projects use different versions of gradle, so using wrapper can solve this problem well. Different gradle versions can be used in different projects.

gradle/wrapper/gradle-wrapper.properties are some gradlewrapper configurations, among which distributionUrl is used more, which can execute the download address and version of gradle.

gradle/wrapper/gradle-wrapper.jar is a dependency package required for gradlewrapper to run.

Four, gradle life cycle and callback

The gradle build is divided into three phases:

Initialization phase, configuration phase, execution phase.

Initialization phase: The main thing to do in this phase is which projects need to be built, and then create Project objects for the corresponding projects.

Configuration stage: The main thing to do in this stage is to configure the project created in the previous step. At this time, the build.gralde script will be executed and the task to be executed will be generated.

Execution phase: The main thing to do in this phase is to execute the task and carry out the main construction work.

During the construction process, gradle will provide some callback interfaces to facilitate doing some things at different stages. The main interfaces are as follows:

gradle.addBuildListener(new BuildListener() {
    @Override
    void buildStarted(Gradle gradle) {
        println('构建开始')
        // 这个回调一般不会调用,因为我们注册的时机太晚,注册的时候构建已经开始了,是 gradle 内部使用的
    }

    @Override
    void settingsEvaluated(Settings settings) {
        println('settings 文件解析完成')
    }

    @Override
    void projectsLoaded(Gradle gradle) {
        println('项目加载完成')
        gradle.rootProject.subprojects.each { pro ->
            pro.beforeEvaluate {
                println("${pro.name} 项目配置之前调用")
            }
            pro.afterEvaluate{
                println("${pro.name} 项目配置之后调用")
            }
        }
    }

    @Override
    void projectsEvaluated(Gradle gradle) {
        println('项目解析完成')
    }

    @Override
    void buildFinished(BuildResult result) {
        println('构建完成')
    }
})

gradle.taskGraph.whenReady {
    println("task 图构建完成")
}
gradle.taskGraph.beforeTask {
    println("每个 task 执行前会调这个接口")
}
gradle.taskGraph.afterTask {
    println("每个 task 执行完成会调这个接口")
}

Five, custom task 

Task is created by default, inherited from DefaultTask.

task myTask {
    println 'myTask in configuration'
    doLast {
        println 'myTask in run'
    }
}

class MyTask extends DefaultTask {
    @Input Boolean myInputs
    @Output
    @TaskAction
    void start() {
    }
}

tasks.create("mytask").doLast {
}

Some important methods of Task are categorized as follows:

  • Task行为:Task.doFirst, Task.doLast
  • Task依赖顺序:Task.dependsOn, Task.mustRunAfter, Task.shouldRunAfter, Task.finalizedBy
  • Task group description: Task.group, Task.decription
  • Whether Task is available: Task.enabled
  • Task input and output: Task.inputs, Task.outputs, gradle will compare the inputs and outputs of the task to determine whether the task is the latest. If the inputs and outputs have not changed, the task is considered to be the latest, and the task will jump out and not execute.
  • Whether the Task is executed: You can force the task to execute Task.upToDateWhen by specifying Task.upToDateWhen = false

For example, if we want to specify the dependency order between tasks, we can write like this:

task task1 {
    doLast {
        println('task2')
    }
}
task task2 {
    doLast {
        println('task2')
    }
}
task1.finalizedBy(task2)
task1.dependsOn(task2)
task1.mustRunAfter(task2)
task1.shouldRunAfter(task2)
task1.finalizedBy(task2)

六、Android Transform 

The android gradle plugin provides the transform api to process the class in the process of converting the class file into dex. It can be understood as a special task, because the transform will eventually be converted into a task for execution.

To implement transform, you need to inherit com.android.build.api.tranform.Transform and implement its method. After implementing Transform, if you want to apply it, call project.android.registerTransform()

public class MyTransform extends Transform {
    @Override
    public String getName() {
        // 返回transform的名称,最终的名称会是transformClassesWithMyTransformForDebug这种形式
        return "MyTransform";
    }
    
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        /**
        返回需要处理的数据类型,有下面几种类型可选
        public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
        public static final Set<ContentType> CONTENT_JARS = ImmutableSet.of(CLASSES, RESOURCES); 
        public static final Set<ContentType> CONTENT_RESOURCES = ImmutableSet.of(RESOURCES);
        public static final Set<ContentType> CONTENT_NATIVE_LIBS = ImmutableSet.of(NATIVE_LIBS);
        public static final Set<ContentType> CONTENT_DEX = ImmutableSet.of(ExtendedContentType.DEX);
        public static final Set<ContentType> DATA_BINDING_ARTIFACT = ImmutableSet.of(ExtendedContentType.DATA_BINDING);
        */
        return TransformManager.CONTENT_CLASS;
    }
    
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        /**
        返回需要处理内容的范围,有下面几种类型
        PRIOJECT(1),只处理项目的内容
        SUB_PROJECTS(4),只处理子项目
        EXTERNAL_LIBRARIES(16),只处理外部库
        TESTED_CODE(32),只处理当前variant对应的测试代码
        PROVIDED_ONLY(64),处理依赖
        @Deprecated
        PROJECT_LOCAL_DEPS(2)
        @Deprecated
        SUB_PROJECTS_LOCAL_DEPS(8);
        */
        return Sets.immutableEnumSet(QualifiedContent.Scope.PROJECT);
    }
    
    @Override
    public boolean isIncremental() {
        //是否增量,如果返回true,TransformInput会包括一份修改的文件列表,返回false,会进行全量编译,删除上一次的输出内容
        return false;
    }
    
    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        //在这里处理class
        super.transform(transformInvocation)
        //在transform里,如果没有任何修改,也要把input的内容输出到output,否则会报错
        for (TransformInput input : transformInvocation.inputs) {
            input.directoryInputs.each { dir ->
                //获取对应的输出目录
                File output = transformInvocation.outputProvider.getContentLocation(dir.name, dir.contentTypes, dir.scopes, Format.DIRECTORY)
                dir.changedFiles //增量模式下修改的文件
                dir.file //获取输入的目录
                FileUtils.copyDirectory(dir.file, output) //input内容输出到output
            }
            input.jarInputs.each { jar ->
                //获取对应的输出jar
                File output = transformInvocation.outputProvider.getContentLocation(jar.name, jar.contentTypes, jar.scopes, Format.JAR)
                jar.file //获取输入的jar文件
                FileUtils.copyFile(jar.file, output) //input内容输出到output
                
            }
        }
    }
}

//注册transform
android.registerTransform(new MyTransform())

The processing in transform generally involves the modification of class files, and the tools for manipulating bytecodes are generally javasist and asm.

7. Handwriting plugin 

The gradle plugin can be seen as a collection of tasks.

In the app module/build.gradle script of the android project, the first line is apply plugin: 'com.android.application', which is to introduce the android gradle plugin, and there are tasks related to android packaging in the plugin.

So let's see now, how to implement a plugin of our own?

1. Initialize the project

(1), create a java module in android studio

(2), create a groovy directory in the src/main directory, and then create your own package name and plug-in class

(3) Create a resources/META-INFO/gradle-plugins directory in the src/main directory, and create a plugins.properties file with the following content:

implementation-class=com.zdj.plugin.MyPlugin //我们自己的插件类

(4), modify the build.gradle file

apply plugin: 'groovy'
apply plugin: 'java'

buildscript {
    repositories {
        mavenLocal()
        google()
        jcenter()
    }
}

repositories {
    mavenLocal()
    google()
    jcenter()
}

dependencies {
    compile gradleApi()
    compile localGroovy()
    compile 'com.android.tools.build:gradle:3.0.1'
}

java {
    sourceCompatibility = JavaVersion.VERSION_1_7
    targetCompatibility = JavaVersion.VERSION_1_7
}

2. Create Plugins 

In the plugin class we just created, we write the code for the plugin. The plug-in class inherits Plugin and implements the apply interface. apply is the interface to be called when applying plugin 'xxx' in build.gradle.

package com.zdj.plugin

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

/**
 * 在这个类中写插件的代码。
 * 首先继承Plugin,然后实现apply接口。
 * apply就是在build.gradle里apply plugin 'xxx'的时候要调用的接口。
 */
class MyPlugin implements Plugin<Project> {
    @Override
    void apply(Project target) {
        println("apply my plugin")
    }
}

3. Create a plugin task 

We then define a class MyTask, inherited from DefaultTask.

package com.zdj.plugin

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction

/**
 * 定义一个类MyTask,继承自DefaultTask。
 */
class MyTask extends DefaultTask {
    @TaskAction
    void action() {
        println("my task run")
    }
}

Then register this task in the plugin.

package com.zdj.plugin

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

/**
 * 在这个类中写插件的代码。
 * 首先继承Plugin,然后实现apply接口。
 * apply就是在build.gradle里apply plugin 'xxx'的时候要调用的接口。
 */
class MyPlugin implements Plugin<Project> {
    @Override
    void apply(Project target) {
        println("apply my plugin")
        //注册task
        target.tasks.create("mytask", MyTask.class)
    }
}

4. Install the plug-in locally

We have developed a simple plugin, so how to use it?

We first need to introduce the maven plug-in in build.gradle and configure install-related properties.

apply plugin: 'maven'

install {
    repositories.mavenInstaller {
        pom.version = '0.0.1'  //配置插件版本号
        pom.artifactId = 'myplugin'  //配置插件标识
        pom.groupId = 'com.zdj.plugin'  //配置插件组织
    }
}

After executing ./gradlew install, the plugin will be installed in the local maven repository.

If you install the plug-in locally and execute the ./gradlew install command, an error may be reported that could not find tools.jar. I ran into this problem.

My solution is: first check the JDK directory path (via the command /usr/libexec/java_home -V), and then manually copy tools.jar (via the command sudo cp).

5. Use our plugin 

Introduce the classpath of our plugin where it is used

classpath 'com.zdj.plugin:myplugin:0.0.1'

Finally, attach two important query addresses:

1、

Gradle DSL Version 7.3.3

(gradle dsl query address)

2、

https://developer.android.com/reference/tools/gradle-api

(Android gradle plugin dsl query address) 

 

 

 

 

 

Guess you like

Origin blog.csdn.net/zdj_Develop/article/details/122156240