android-gradle-study

Introduction: 深入理解 Android Gradle
More: Author   ReportBugs   
Tags:

为什么写这个?

讲 Gradle 的文章和书很多,讲 Groovy 的文章和书也很多,但是在 Android 中如何使用 Gradle 和 Groovy,感觉没有一篇文章和书能够讲透,总觉得使用起来模模糊糊,云里雾里。所以,想把平时研究和应用 Gradle 的一些要点和心得记录下来,既是方便自己,也是方便大家。

  • [x] 会持续更新学习和研究 Gradle 的一些要点和心得,力求让你从根本上搞懂 Android Gradle,不求大而全,但求精。也许你在别处找不到的答案,在这里可以找到呢。
  • [x] 仓库中的工程是为了方便我经常调试和研究 Gradle 所搭建,可能有点乱,仅做参考。

Gradle 是什么

“Gradle is an open-source build automation system that builds upon the concepts of Apache Ant and Apache Maven and introduces a Groovy-based domain-specific language (DSL) instead of the XML form used by Apache Maven for declaring the project configuration.[1] Gradle uses a directed acyclic graph ("DAG") to determine the order in which tasks can be run.”——维基百科对 Gradle 的定义。

翻译过来就是:“Gradle 是一个基于 Apache Ant 和 Apache Maven 概念的项目自动化构建开源工具。它使用一种基于 Groovy 的特定领域语言(DSL)来声明项目设置,抛弃了基于 XML 的各种繁琐配置。”

还是很难懂。难怪有些人觉得 gradle 文件看起来很痛苦,看不懂。我的理解是:gradle 既是脚本,也是代码。可以像脚本那些执行,而且每一行脚本,都可以理解成执行了相应的对象中的一个方法。但是由于闭包的存在使得有些代码的执行顺序跟定义的顺序不一致。这样一来,Gradle 写起来和读起来都像配置文件,实际上是一系列代码,要以代码的角度来阅读和编写 Gradle。这样一来,Gradle 就要好理解得多。

AS 中的 Gradle Build Script

  1. 工程根目录下的 setting.gradle 和 build.gradle
  2. 每个 Module 目录下都有一个 build.gradle

执行顺序

Gradle 执行的时候遵循如下顺序:

  1. 首先解析 settings.gradle 来获取模块信息,这是初始化阶段
  2. 然后配置每个模块,配置的时候并不会执行 task
  3. 配置完了以后,有一个重要的回调 project.afterEvaluate,它表示所有的模块都已经配置完了,可以准备执行 task 了
  4. 执行指定的 task。

gradle 对象

每个 gradle 脚本都可以访问 gradle 对象,比如在 setting.gradle 下执行:

println("gradle name: " + gradle.class.name)

会输出:

gradle name: org.gradle.invocation.DefaultGradle_Decorated

这些 gradle 对象都是接口 Gradle 的实现:

public interface Gradle extends PluginAware {
    ....
}

比如有个叫 DefaultGradle 的实现:

public class DefaultGradle extends AbstractPluginAware implements GradleInternal {
...
}

通过 gradle 对象可以获取 Gradle 的相关信息和添加一些钩子。

setting 对象

每一个 setting.gradle 都对应一个 setting 对象

在 setting.gradle 中可以访问到 setting 对象:

println("setting.gradle: " + settings)
println("setting.gradle: " + this)

输出:

setting.gradle: settings 'android-gradle-study'
setting.gradle: settings 'android-gradle-study'

project 对象

每一个 build.gradle 都对应一个 project 对象。

  • [x] 在 project 的 build.gradle 中,获取到的 project 对象是 root project
  • [x] 在 module 的 build.gradle 中,获取到的 project 对象是 module project

比如在 root project 的 build.gradle 中和在 module 的 build.gradle 中执行如下代码:

println("Root build.gradle: " + project)
println("Root build.gradle: " + this)

root project 输出:

Root build.gradle: root project 'android-gradle-study'
Root build.gradle: root project 'android-gradle-study'

module project 输出:

App build.gradle: project ':app'
App build.gradle: project ':app'

但是,root project 和 module project 的类型是一样的,都是 DefaultProject。

Root build.gradle

buildscript

buildscript 用于配置插件的 classpath,插件跟引用的 aar 不同,插件不会编译到 apk 中,插件只是用于构建。设置 repositories 告诉 gradle classpath 的仓库地址,dependencies 用于配置具体的 classpath。

allprojects

allprojects 进行的配置会应用到当前的 project 以及其所有 module,这里配置的 repositories 会在当前的 project 以及其所有 module 都生效。

Gradle Wrapper

Gradle Wrapper,就是对 gradle 的一层包装。在 AS 右侧的 Gradle 面板直接运行 task 等同于直接用 gradle 运行 task。但是由于 gradle 有不同的版本,所以希望使用统一的 gradle 版本进行构建,避免由于 gradle 版本不统一带来的问题。

AS 的工程下有两个脚本:gradlew 和 gradlew.bat。包括还有一个文件夹:gradle/wrapper/这个文件夹里面的 gradle-wrapper.properties 决定了我们使用 gradlew 的时候调用的 gradle 版本:

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

gradlew 下载的 gradle 一般放在如下的位置:

~/.gradle/wrapper/dists

Groovy 闭包

闭包就是一段代码块,以参数的形式传递给其他函数,其他函数可以执行这个闭包。这有点像 java 里面 listener 的作用。 示例:

task doSomething1 << {
    runEach({
        println(it)
    })
}

def runEach(closure) {
    for (int i in 1..10) {
        closure(i)
    }
}

由于 Groovy 里面方法调用可以省略括号,并且如果方法的最后一个参数是闭包,可以放到方法外面,因此我们经常可以方法调用闭包的写法类似这样:

runEach {
    println(it)
}

Task

Gradle 里面创建 task 的方法有很多,我最喜欢的一种是任务名字+闭包配置的方式:

task demoTask {
    description 'demo task'
}

查看源码可以发现,task 都是通过 TaskContainer 创建的:

public Task task(String task) {
    return this.taskContainer.create(task);
}

一个 task 由多个 action 组成,task 真正要完成的任务都放在这些 action 里面。

在源码 AbstractTask 中,我们可以找到一个存放 action 的 list:

public abstract class AbstractTask implements TaskInternal, DynamicObjectAware {
    private static final Logger BUILD_LOGGER = Logging.getLogger(Task.class);
    private static final ThreadLocal<TaskInfo> NEXT_INSTANCE = new ThreadLocal<TaskInfo>();

    private final ProjectInternal project;

    private final String name;

    private List<ContextAwareTaskAction> actions;

    ...
}

我们可以通过 doFirst 和 doLast 来添加 action,doFirst 把 action 添加到最前面,doLast 添加 action 最后执行:

task demoTask {
    description 'demo task'
    doFirst {
        println("this action will run first!")
    }
    doLast {
        println("this action will run last!")
    }
}

查看 Task 的源码可以发现,这两个操作无非是把 action 添加到 action list 的队头和队尾:

@Override
public Task doFirst(final Closure action) {
    hasCustomActions = true;
    if (action == null) {
        throw new InvalidUserDataException("Action must not be null!");
    }
    taskMutator.mutate("Task.doFirst(Closure)", new Runnable() {
        public void run() {
            getTaskActions().add(0, convertClosureToAction(action, "doFirst {} action"));
        }
    });
    return this;
}

@Override
public Task doLast(final Closure action) {
    hasCustomActions = true;
    if (action == null) {
        throw new InvalidUserDataException("Action must not be null!");
    }
    taskMutator.mutate("Task.doLast(Closure)", new Runnable() {
        public void run() {
            getTaskActions().add(convertClosureToAction(action, "doLast {} action"));
        }
    });
    return this;
}

<<操作符

因为 Task 的 doLast 用的很多,所以使用了一种 doLast 的短标记形式,这就是<<操作符:

task doSomething1 << {
    //doLast 的 action
}

<<对应的源码是 Task 的 leftShift 方法:

@Override
public Task leftShift(final Closure action) {
    DeprecationLogger.nagUserWith("The Task.leftShift(Closure) method has been deprecated and is scheduled to be removed in Gradle 5.0. Please use Task.doLast(Action) instead.");

    hasCustomActions = true;
    if (action == null) {
        throw new InvalidUserDataException("Action must not be null!");
    }
    taskMutator.mutate("Task.leftShift(Closure)", new Runnable() {
        public void run() {
            getTaskActions().add(taskMutator.leftShift(convertClosureToAction(action, "doLast {} action")));
        }
    });
    return this;
}

这也进一步说明了 doLast 和<<操作符是一致的。

Plugin

Plugin 用来干嘛?

研究一项技术之前如果不弄清楚这项技术用来干嘛,能带来什么好处,那就是为了研究技术而研究技术,没任何卵用。

Plugin 说白了就是可以把你之前写在 gradle 文件中的那些代码,提取出来,放到一个插件中。这个插件可以放到一个仓库中,可以下载下来使用。说白了,就是提高你那段代码的复用性。

使用 Plugin

apply plugin:[your-plugin]

当你调用这句话的时候,你写在你的自定义 Plugin 中的 apply 方法就会执行。就这么简单。

在使用 plugin 之前,需要在 root project 的 build.gradle 中指定 classpath 和相应的 repo 仓库地址:

buildscript {
    repositories {
        //定义 repo 仓库地址
    }
    dependencies {
        //定义 classpath
    }
}

设置 buildscript 的目的是为了让 gradle 能知道去哪找到你的插件。

自定义 Plugin

自定义 Plugin 很简单,直接参考代码里面的 plugin module 即可。

自定义的 Plugin 可以采用 Groovy 编写,也可以采用 java 编写。采用 Groovy 编写可以使用 Groovy 的一些特性,比如使用闭包。

你甚至可以像示例中的 plugin module 那样,同时使用 groovy 和 java。并且可以在一个 module,或者一个 jar 中包含多个 plugin。

配置一个 plugin module 可以参考示例代码,其中有几个关键点:

  • [x] apply plugin
apply plugin: 'groovy'
apply plugin: 'java'
  • [x] 配置 sourceSets
sourceSets {
    main {
        groovy {
            srcDir 'src/main/groovy'
        }

        java {
            srcDir 'src/main/java'
        }
    }
}
  • [x] 配置 dependencies
dependencies {
    compile gradleApi()
    compile localGroovy()
    compile 'com.android.tools.build:gradle:3.2.1'
}
  • [x] 配置 META-INF
  • 在 src/main/下新建目录:resources/META-INF/gradle-plugins/
  • 在这个目录下新建文件:xx.properties,这个 xx 就是 plugin id,也就是用户使用这个插件的时候 apply plugin:的那个 id
  • 在 xx.properties 中定义:implementation-class=[class name of your plugin]

调试自定义 Plugin

如何调试自定义 Plugin 呢,我一般喜欢用本地 repo 调试,如何把你的 plugin 上传到本地 repo 呢,只需要在你的 plugin 模块的 build.gradle 中加入以下代码:

apply plugin: 'maven'

group = 'com.xxx.xxx'
version = '0.0.1'

uploadArchives {
    repositories {
        mavenDeployer {
            repository(url: uri('../repo'))
        }
    }
}

这个时候 AS 的 gradle 面板会多一个 uploadArchives 任务,执行这个任务便可以把 plugin 上传到工程根目录的 repo 目录下。

android-apt 和 annotationProcessor

作用

APT(Annotation Processing Tool)是一种处理注释的工具,它对源代码文件进行检测找出其中的 Annotation,对这些 Annotation 进行处理。常用的处理方式包括根据这些注解自动生成一些 java 源文件或者 java class 文件。

android-apt

android-apt 是 annotationProcessor 出现之前的 apt 框架。要使用 android-apt 需要添加如下的代码:

添加 android-apt 到 Project 下的 build.gradle 中

//配置在 Project 下的 build.gradle 中
buildscript {
    repositories {
      mavenCentral()
    }
    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

在 Module 中 build.gradle 的配置(以 dagger 为例)

apply plugin: 'com.neenbedankt.android-apt'

dependencies {
    apt 'com.squareup.dagger:dagger-compiler:1.1.0'
}

annotationProcessor

annotationProcessor 也是一种 APT 工具,他是 google 开发的内置框架,不需要引入,可以直接在 module 的 build.gradle 文件中使用(以 butterknife 为例):

dependencies {
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
}

自定义注解处理器

创建一个 java module,编写一个类,继承 AbstractProcessor。并且重写其中的 process 方法:

public class MyProcessor extends AbstractProcessor {

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

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        ...
        return true;
    }

    @Override
    public Set getSupportedAnnotationTypes() {
        Set annotataions = new LinkedHashSet();
        annotataions.add(MyAnnotation.class.getCanonicalName());
        return annotataions;
    }

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

Export Processor

有两种方法 Export Processor:

手动暴露
  1. 在 processors 库的 main 目录下新建 resources 资源文件夹
  2. 在 resources 文件夹下建立 META-INF/services 目录文件夹
  3. 在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件
  4. 在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径
使用 AutoService

AutoService 注解处理器是 Google 开发的,用来生成 META-INF/services/javax.annotation.processing.Processor 文件的,你只需要在你定义的注解处理器上添加 @AutoService(Processor.class) 就可以了,简直不能再方便了。

  • [x] 添加依赖
dependencies {
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
}
  • [x] 用@AutoService 注解 Processor
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
    // ...
}

Transform

Transform 用来干嘛

还是那句话,研究任何一个新技术之前都要先弄明白这个技术是用来干嘛的,不然就毫无意义。

前面介绍了 Plugin,但是 apply plugin 是发生在配置阶段,还没有涉及到真正的构建过程。如果我们想在构建过程中做一些事,比如我们想拿到编译时产生的 Class 文件,并在生成 Dex 之前做一些处理。

Transform 就是用来应对这种场景的。

另外一种处理方式

Transform API 是在 1.5.0-beta1 版开始使用的。在此之前,如果我们想拿到编译时产生的 Class 文件,并在生成 Dex 之前做一些处理,常用的方式是注册 project 的 afterEvaluate 方法,在这个方法中拿到一些构建过程中的 task,并在这个 task 中注入一些 action 来完成:

project.afterEvaluate {
    System.out.println(TAG + "execute afterEvaluate: " + project)
    def extension = project.extensions.findByType(AppExtension.class)
    extension.applicationVariants.all { variant ->
        String variantName = capitalize(variant.getName())
        Task mergeJavaResTask = project.tasks.findByName(
                "transformResourcesWithMergeJavaResFor" + variantName)
        System.out.println(TAG + "mergeJavaResTask: " + mergeJavaResTask)
        mergeJavaResTask.doLast {
            System.out.println(TAG + "mergeJavaResTask.doLast execute")
        }
    }
}

使用 Transform

定义 Transform

自定义 Transform,继承自 Transform 类:

class AgsTransform extends Transform {

    final String TAG = "[AgsTransform]"

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        System.out.println(TAG + "start transform")
        super.transform(transformInvocation)
    }

    @Override
    String getName() {
        return AgsTransform.simpleName
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }
}
输入的类型
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
    return TransformManager.CONTENT_CLASS
}

输入类型有两种,CLASSES 和 RESOURCES,在 DefaultContentType 指定:

enum DefaultContentType implements ContentType {
    /**
     * The content is compiled Java code. This can be in a Jar file or in a folder. If
     * in a folder, it is expected to in sub-folders matching package names.
     */
    CLASSES(0x01),

    /** The content is standard Java resources. */
    RESOURCES(0x02);

    private final int value;

    DefaultContentType(int value) {
        this.value = value;
    }

    @Override
    public int getValue() {
        return value;
    }
}

TransformManager 中定义了一系列的类型集合:

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);
public static final Set<ContentType> DATA_BINDING_BASE_CLASS_LOG_ARTIFACT =
        ImmutableSet.of(ExtendedContentType.DATA_BINDING_BASE_CLASS_LOG);
输入文件所属的范围
@Override
Set<? super QualifiedContent.Scope> getScopes() {
    return TransformManager.SCOPE_FULL_PROJECT
}

getScopes()用来指明自定的 Transform 的输入文件所属的范围, 定义在 Scope 中:

enum Scope implements ScopeType {
    /** Only the project content */
    PROJECT(0x01),
    /** Only the sub-projects. */
    SUB_PROJECTS(0x04),
    /** Only the external libraries */
    EXTERNAL_LIBRARIES(0x10),
    /** Code that is being tested by the current variant, including dependencies */
    TESTED_CODE(0x20),
    /** Local or remote dependencies that are provided-only */
    PROVIDED_ONLY(0x40),

    /**
     * Only the project's local dependencies (local jars)
     *
     * @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
     */
    @Deprecated
    PROJECT_LOCAL_DEPS(0x02),
    /**
     * Only the sub-projects's local dependencies (local jars).
     *
     * @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
     */
    @Deprecated
    SUB_PROJECTS_LOCAL_DEPS(0x08);

    private final int value;

    Scope(int value) {
        this.value = value;
    }

    @Override
    public int getValue() {
        return value;
    }
}

同样,TransformManager 中定义了一系列的 Scope 集合:

public static final Set<ScopeType> PROJECT_ONLY = ImmutableSet.of(Scope.PROJECT);
public static final Set<Scope> SCOPE_FULL_PROJECT =
        Sets.immutableEnumSet(
                Scope.PROJECT,
                Scope.SUB_PROJECTS,
                Scope.EXTERNAL_LIBRARIES);
public static final Set<ScopeType> SCOPE_FULL_WITH_IR_FOR_DEXING =
        new ImmutableSet.Builder<ScopeType>()
                .addAll(SCOPE_FULL_PROJECT)
                .add(InternalScope.MAIN_SPLIT)
                .build();
public static final Set<ScopeType> SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS =
        ImmutableSet.of(Scope.PROJECT, InternalScope.LOCAL_DEPS);
重写 transform 方法

我们可以通过 TransformInvocation 来获取输入,也可以获取输出的功能:

@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    System.out.println(TAG + "start transform")
    super.transform(transformInvocation)
    //处理输入
    System.out.println(TAG + "处理输入")
    for (TransformInput input : transformInvocation.inputs) {
        input.jarInputs.parallelStream().forEach(new Consumer<JarInput>() {
            @Override
            void accept(JarInput jarInput) {
                File file = jarInput.getFile()
                JarFile jarFile = new JarFile(file)
                Enumeration<JarEntry> entries = jarFile.entries()
                while (entries.hasMoreElements()) {
                    JarEntry entry = entries.nextElement()
                    System.out.println(TAG + "JarEntry: " + entry)
                }
            }
        })
    }
    //处理输出
    System.out.println(TAG + "处理输出")
    File dest = transformInvocation.outputProvider.getContentLocation(
            "output_name",
            TransformManager.CONTENT_CLASS,
            TransformManager.PROJECT_ONLY,
            Format.DIRECTORY)
}

注册 Transform

在 Plugin 中注册:

def extension = project.extensions.findByType(AppExtension.class)
System.out.println(TAG + extension)
extension.registerTransform(new AgsTransform())

extension

extension 用于向你的 Plugin 中传入一些配置信息,使用起来很简单。

创建 Bean

创建一个存放配置信息的 bean:

public class DemoExtension {

    private boolean enable;
    private String message;

    public boolean isEnable() {
        return enable;
    }

    public void setEnable(boolean enable) {
        this.enable = enable;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("enable=").append(enable).append(";");
        sb.append("message=").append(message).append(";");
        return sb.toString();
    }
}

在 Plugin 中 create extension

DemoExtension extension = project.getExtensions().create("demoConfig", DemoExtension.class);

在 build.gradle 中进行配置

demoConfig {
    enable = true
    message = 'hello world'
}

使用 extension 的一个坑

完成以上步骤之后,就可以使用 extension 了,但是 extension 有个坑,就是如果 create 之后立即使用,这个时候 bean 里面是默认值,也就是说 build.gradle 中的信息还没有加载进去,需要在整个 gradle 配置完成之后 bean 中才会填充上相应的值:

final DemoExtension extension = project.getExtensions().create("demoConfig", DemoExtension.class);
System.out.println(TAG + "extension: " + extension);
project.afterEvaluate(new Action<Project>() {
    @Override
    public void execute(Project project) {
        System.out.println(TAG + "extension afterEvaluate: " + extension);
    }
});

这样一来,如果你有些信息是需要在配置阶段读取到的,就不适合使用 extension 了。

使用 properties

有些时候我们的配置信息是写在 extension 里面的,但是如果要在 plugin 中读取这些配置信息,需要在脚本 Evaluate 之后,如果要在配置的时候就使用配置信息,最常用的办法就是使用 properties: 在 build.gradle 平级的位置建立一个名为 gradle.properties 的文件,里面是配置信息,如:

testProperty=hello

然后在 Plugin 的代码中用如下方式读取:

String testProperty = (String) project.property("testProperty");
Apps
About Me
GitHub: Trinea
Facebook: Dev Tools