ASM全埋点开发实战

978-7-115-61261-8
作者: 王灼洲张伟
译者:
编辑: 张天怡

图书目录:

详情

本书由业内知名团队神策数据的专业人士编写,结合实战案例,深入浅出地介绍了ASM技术和Android全埋点技术。 作者从神策数据服务超过2000家客户的经历中,发现了行业用户对全埋点技术的迫切需求。本书针对这一点,详细、客观地阐述了ASM在Android全埋点中的应用,涵盖各种真实商业场景,并清晰地讲解其技术原理和实现步骤,以帮助用户利用好全埋点技术的特长和优势。 本书作为一本技术参考书,特别适合非专业开发工程师在日常工作中使用。

图书摘要

版权信息

书名:ASM全埋点开发实战

ISBN:978-7-115-61261-8

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    王灼洲 张 伟

责任编辑 张天怡

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315

内 容 提 要

本书由业内知名团队神策数据的专业人士编写,结合实战案例,深入浅出地介绍了ASM技术和Android全埋点技术。

作者从神策数据服务超过2000家客户的经历中,发现了行业用户对全埋点技术的迫切需求。本书针对这一点,详细、客观地阐述了ASM在Android全埋点中的应用,涵盖各种真实商业场景,并清晰地讲解其技术原理和实现步骤,以帮助用户利用好全埋点技术的特长和优势。

本书作为一本技术参考书,特别适合非专业开发工程师在日常工作中使用。

 序

在移动应用开发中,埋点技术是不可或缺的一环。通过对用户行为的合规监控和分析,开发者可以更好地了解用户需求,优化应用性能,提升用户体验。但是,传统的手动埋点方式存在诸多问题:开发成本高、维护成本大、埋点覆盖不全等。因此,寻找一种更加高效和自动化的埋点方式成了开发者的共同需求。

神策数据作为一家大数据分析与营销科技服务提供商,在用户行为数据埋点、传输、存储和分析等领域积累了多年经验。灼洲与他所负责的团队,一直负责各个客户端与服务端数据采集 SDK 的研发工作,并且为客户提供完整的数据采集方案,以及解决客户在数据采集过程中碰到的各种疑难杂症。在整个研发与服务客户的过程中,灼洲团队积累了丰富的经验,并且通过采集 SDK 开源、持续举办各种技术沙龙,为技术社区做出贡献。

灼洲通过之前编写的《Android全埋点解决方案》和《iOS 全埋点解决方案》两本书,对于Android 和 iOS 应用的自动化埋点技术进行了全面且深入的介绍。而本书则是在这一基础之上,更进一步讲述关于Android应用自动化埋点技术的实用指南。本书深入浅出地介绍了如何使用ASM技术对Android应用进行全埋点,从而降低埋点的成本和代价。

ASM技术作为一种基于Java字节码的操作工具,可以在不改变原有代码逻辑的情况下,对字节码进行修改和增强。这使得ASM技术成为一种理想的自动化埋点工具。本书从基础的ASM技术原理入手,详细介绍了ASM在Android应用中的应用方式,同时,本书还介绍了ASM在实际项目中的应用案例,让读者更好地理解ASM技术的应用场景和优势。

除了介绍ASM技术,本书还涵盖了其他与自动化埋点相关的知识,如常见的埋点方案、埋点数据分析等。通过全面的介绍,读者不仅可以掌握ASM技术的实现方法,还可以对自动化埋点的相关知识进行全面了解。本书不仅适合Android开发者阅读,对于任何对自动化埋点感兴趣的读者来说都是一本值得阅读的图书。

神策数据联合创始人&CTO曹犟

前  言

为什么要写这本书

大数据时代已经到来,数据采集变得愈加重要,企业对这方面的需求也越来越多。我的第一本书《Android全埋点解决方案》自发布后,收到了许多读者的反馈,有的与我分享他们的阅读感悟,有的说这本书改变了他们的职业生涯,有的与我探讨具体技术。其中,有不少读者跟我探讨了在实际编写Android插件时遇到的各种问题,主要原因是读者对 Gradle 和 ASM还不够熟悉。

ASM确实是入手门槛比较高的一项技术,它更加偏向底层,需要我们对底层涉及的技术有所了解。ASM 的主要用途是操作字节码,它的应用非常广,例如Java JDK、Groovy编译器、Gradle、AGP中都需要用到它。

神策数据深耕埋点技术多年,在此方面积累了大量的经验,同时神策数据也是开源领域的践行者,为此,我专门编写本书,将沉淀的知识毫无保留地分享出来,希望能够推动该领域进一步向上发展。本书结构设计合理,充分考虑读者渐进式接受知识的方式,内容翔实,即便是初学者也能够掌握。

读者对象

初级、中级、高级水平的Android开发工程师。

Java工程师、Java架构师。

对Gradle和ASM技术感兴趣的读者。

对埋点技术和数据行为分析感兴趣的读者。

勘误和支持

为了方便读者更好地学习ASM技术,读者可以从https://github.com/sensorsdata/asm-book获取本书案例源码。

由于作者的水平有限,以及技术不断更新和迭代,书中难免会出现一些不恰到的地方,恳请读者批评指正。联系邮箱:zhangtianyi@ptpress.com.cn。

致谢

感谢神策数据创始人团队桑文锋、曹犟、付力力、刘耀洲在工作中对我的指导和帮助。感谢神策数据开源社区每一位充满活力和共享精神的朋友们。

谨以此书献给大数据行业的关注者和建设者!

王灼洲

2023年5月

1.Gradle插件介绍

ASM是一个通用的Java字节码操作和分析框架,其应用非常广泛,例如CGLib动态代理的实现、Java 8 Lambda功能的实现、字节码插桩等。目前国内有不少介绍ASM的资料,不过这些资料只对ASM 应用程序接口(Application Programming Interface,API)进行了简单介绍。读者如果参考这些资料来学习ASM框架,会发现学起来很费劲,通常只能“照葫芦画瓢”地抄一些代码,而且容易出错,出错的时候也不知道原因。产生这种现象的原因是ASM的入门门槛比较高,学习曲线很陡,要想学好ASM框架必须要具备一些基础知识。

本书是一本介绍ASM框架的专业书。本书结构设计合理,从基础知识开始讲解,先介绍基础知识再介绍ASM框架,最后介绍ASM框架在全埋点中的实际应用,知识点层层深入,最终形成知识闭环,即使完全不了解ASM的读者也能够较轻松地掌握这门技术。读者在学完本书中的知识后,可以发挥自己的奇思妙想,用ASM实现一些看起来很酷、很神奇的功能。

本书详细介绍了ASM在Android全埋点中的应用,而要实现全埋点功能,必须对Gradle插件有所了解。本章将介绍Gradle相关知识,以及如何在Android中定义一个Gradle插件。

1.1 什么是Gradle插件

Gradle是一款强大的构建工具,不仅能构建Java/Android项目,还可以构建C++项目、JavaScript项目和Swift项目。对于Gradle,接触过Android开发的读者应该比较熟悉。那什么是Gradle插件呢?我们知道Android app工程中的build.gradle文件开头都会有一段apply plugin: 'com.android.application'代码,它的作用就是使用“com.android.application”这个插件。那么Gradle插件到底是怎么定义的呢?

Gradle插件就是Gradle工具提供的一种可以将用户的代码逻辑与他人分享的方式。为了使读者能更好地理解后续章节的内容,我们先简单介绍一些Gradle的基础知识。

1.2 Gradle基础知识

1.2.1 学习前提

首先按照Gradle官网的教程下载并安装Gradle,安装好以后在控制台中运行如下命令:

$ gradle help

这段命令的意思是运行Gradle中的help任务。另外,Gradle脚本通常使用Groovy语言来编写,因此读者需要花一点儿时间学习Groovy,包括Groovy基本语法、闭包、领域特定语言(Domain Specific Language,DSL)等知识。Groovy兼容Java,熟悉Java的读者可以很快上手。

与Groovy相关的教程请参考其官网。

1.2.2 Gradle项目结构

接下来介绍Gradle项目结构。此处创建一个名为Chapter1_00的Android项目。选择Android项目的原因是本书面向的读者主要是Android开发商,当然,读者可以使用gradle init来创建其他类型(如Java、C++)的项目。下面是项目的基本结构,我们以此来介绍Gradle项目的组成。

Chapter1_00
├── app(3)
│   ├── build.gradle(4) 
│   └── src
├── build.gradle(2)
├── gradle
│   └── wrapper(6)
│       ├── gradle-wrapper.jar(7)
│       └── gradle-wrapper.properties(8)
├── gradle.properties(5)
├── gradlew(9)
├── gradlew.bat(10)
├── local.properties(11)
└── settings.gradle(1)

接下来按照上面标注的序号,着重对前6个进行介绍。

(1)settings.gradle:该文件在Gradle项目中是可选的。如果一个Gradle工程中有多个子项目,那么必须要有settings.gradle文件,它的配置决定哪些项目会参与构建,例如:

include ':app'
rootProject.name = "Chapter1_01"

其中include表示使同级目录中的app这个模块参与构建,rootProject.name表示当前项目的名称。

(2)build.gradle:表示根项目的构建文件,每一个build.gradle文件都对应一个Project(项目)对象。根项目中的build.gradle对应的Project对象,我们称为root project;其他模块/项目我们称为sub project,例如这里的app目录下的build.gradle就是sub project。为了方便后续的内容介绍,我们约定用app/build.gradle来表示app项目的build.gradle构建文件,用root project/build.gradle来表示根项目的build.gradle构建文件。根项目的build.gradle用于做一些全局的配置,例如:

// 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:4.1.2"
        //动态添加
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}
 
allprojects {
    repositories {
        google()
        jcenter()
    }
}

说明如下。

① buildscript{}用于配置Gradle插件需要的依赖,其中,repositories{}用于声明插件所在的仓库地址,dependencies{}用于声明依赖的插件。

② allprojects{}用于对所有子项目进行配置,其中repositories{}用于声明子项目添加依赖包的仓库地址。

(3)app:子项目目录。

(4)app/build.gradle:子项目对应的构建文件。

(5)gradle.properties:用于配置Gradle运行时相关的值,也可以在其中定义一些其他键值对,配置的值可以在build.gradle或Project中使用。

(6)gradle/wrapper:这个目录下有gradle-wrapper.jar和gradle-wrapper.properties两个文件,这两个文件是给gradlew和gradlew.bat脚本使用的。这两个脚本的作用都是运行Gradle命令,不同点是gradlew脚本运用于macOS/Linux平台,而gradlew.bat脚本运用于Windows平台。gradlew是Gradle Wrapper的缩写。相较于直接运行Gradle命令,gradlew脚本在运行命令的时候会根据gradlew-wrapper.properties中的配置运行指定版本的Gradle。如果指定版本的Gradle不存在,会执行gradlew-wrapper.jar包中的逻辑去下载。这么做的好处是可以保证项目组中不同的成员都使用同样版本的Gradle进行构建。

再来看看gradle-wrapper.properties中的内容:

#Sat Mar 06 11:46:33 CST 2021
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip

说明如下。

① distributionBase:下载的gradle*.zip解压后所在位置的父目录。

② distributionPath:distributionBase确定了父目录,distributionBase + distributionPath的组合就能确定Gradle解包后的存放位置。

③ zipStoreBase:Gradle压缩包下载后存储的父目录。

④ zipStorePath:zipStoreBase确定了父目录,zipStoreBase + zipStorePath的组合就能确定Gradle压缩包的存放位置。

⑤ distributionUrl:Gradle指定版本的压缩包下载地址,上面的gradle-6.5-all.zip中的all表示会下载文件。当我们想要查看API的时候很有帮助,如果只想下载可执行文件,将all改成bin即可。

以上就是Gradle项目的典型结构。

1.2.3 生命周期

Gradle生命周期具有3个不同的阶段。

初始(initialization)阶段。Gradle支持单个或多个项目构建,Gradle在初始阶段,会查找settings.gradle文件并根据其中的配置决定让哪些项目参与构建,并且为每一个项目创建一个Project对象。对于settings.gradle文件,Gradle会首先在项目中查找。如果存在此文件,就按照多项目构建来执行;如果在当前目录中没有找到此文件,就从父目录中寻找,如果父目录中没有此文件,就将此项目作为单个项目进行构建。

配置(configuration)阶段。在这个阶段,将配置Project对象,即执行各项目下的build.gradle脚本,构造任务(task)的依赖关系并执行任务中的配置代码。

执行(execution)阶段。按照顺序执行任务中定义的动作(action),例如在Terminal中执行help任务。

$ ./gradlew help

1.2.4 Project API介绍

在Gradle构建体系中,build.gradle和Project对象是一对一的关系,每个build.gradle文件都对应一个Project对象。我们在build.gradle文件中调用的方法相当于调用Project类中的方法,例如rootProject/build.gradle中使用的buildscript{},它实际上调用的是Project类中的buildscript(Closure configureClosure)方法。

下面列出了Project类中的部分常用方法。

Project getProject():获取当前的Project对象。

File getProjectDir():获取当前项目的目录。

Set<Project> getAllprojects():获取当前项目和子项目的Project集合。

Map<String,Project> getChildProjects():获取子项目,此键值对中键是项目名(例如Chapter1_00中的项目app),值是Project实例。

File getBuildDir():获取当前项目的build文件夹。

void setBuildDir(File path):设置编译目录。

File getBuildFile():获取build.gradle文件。

ScriptHandler getBuildscript():获取build.gradle文件中对应的buildscript()方法中创建的值,例如rootProject/build.gradle中定义的内容。

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.1.2"
    }
}

我们可以根据获取到的ScriptHandler对象添加和修改内容。

void buildscript(Closure configureClosure):设置buildscript()中的内容,我们也可以通过getBuildScript()方法进行操作。

DependencyHandler getDependencies():获取build.gradle文件中对应的dependencies()方法中创建的值,例如1.2.2小节中序号为4的build.gradle中定义的内容。

//build.gradle
dependencies {
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

当然,我们可以根据返回值动态地添加项目依赖。

Gradle getGradle():获取当前项目所属的Gradle对象。Gradle对象中提供了生命周期相关的回调。

ExtensionContainer getExtensions():为当前项目添加扩展对象,这对我们后面介绍的插件编写很有用。例如1.2.2小节的build.gradle文件中定义的android { ... }DSL语句,它的内容就是用在 “com.android.application”中的。

String getName():获取当前项目名。

Task task(String name):创建一个指定名字的Task,将其绑定到Project上并返回该Task对象。

Task task(String name, Closure configureClosure):创建一个指定名字的Task,将其绑定到Project上,然后运行configureClosure,最后返回该Task对象。

Task task(Map<String,?> args, String name):创建Task,与Task相关的配置可以放在Map中,其中Map支持如下类型。

type:Task的类型,默认值是DefaultTask。

overwrite:是否替换已经存在的Task,需要和type配合使用,默认值是false。

dependsOn:依赖的Task名字,默认值是 [ ]。

action:添加到Task的Action或者闭包,默认值是null(表示“空”)。

description:Task的描述,默认值是null。

group:Task所属的分组,默认值是null。

TaskContainer getTasks():获取当前Project的所有Task,这里是一个TaskContainer对象,通过它可以对Task进行增删改操作。

Map<Project,Set<Task>> getAllTasks(boolean recursive):获取所有项目中的Task,包括子项目的Task。

void setProperty(String name, Object value):此方法会查找有没有对应的属性,如果没有就会抛出异常,如果有就为其设置新的值。例如可以通过为Project设置extra、extension、gradle.properties等方式设置属性。

Map<String,?> getProperties():获取当前Project的所有属性。

void setVersion(Object version):设置当前项目的版本号。

Object getVersion():获取当前项目的版本号。

void setGroup():设置项目的组织信息,通常使用类似 “com.sensorsdata” 这种公众域名字符串来代替。Version和Group在Maven打包的时候需要用到。

Object getGroup():获取项目组织信息。

void defaultTasks(String...defaultTasks):设置默认Task,默认Task会在执行其他Task时自动执行。

<T> Iterable<T> configure(Iterable<T> objects, Action<? super T> configureAction):对给定的对象执行给定的配置动作。

void afterEvaluate(Closure closure):这是Project生命周期中提供的钩子函数,是在项目评估完成后的回调。另外,Task、Gradle都提供了类似的钩子函数,关于生命周期回调可以参考1.2.6小节。

void beforeEvaluate(Closure closure):这是Project生命周期中提供的钩子函数,是在项目评估前的回调。另外,Task、Gradle都提供了类似的钩子函数。

void apply(Closure closure):引入插件或脚本,例如apply plugin: 'com.android.application'。

void apply(Map<String,?> options):引入插件或脚本,例如apply plugin: 'com.android.application'。

PluginContainer getPlugins():获取包含所有插件的容器对象。

1.2.5 Gradle任务介绍

Gradle的核心模型是一个以任务为工作单元的有向无环图(Directed Acyclic Graph,DAG),这意味着一次构建实际上是配置一些任务并将它们按照DAG的关系联系在一起。例如图1-1描述了这种关系,左边是抽象图,右边是具体的依赖关系图。

图1-1 Gradle Task DAG(摘自Gradle官网)

当我们使用Gradle编译项目的时候,实际上是执行一个个Task,例如图1-1所示:任务之间使用DAG的方式相互依赖,当执行构建任务时会按照DAG中的依赖关系先执行check、assemble任务,而执行assemble任务的时候又会先执行jar任务,按照这样的规律执行任务。

一个任务由如下几个部分组成。

Action:任务执行片段,可以定义多个,当任务执行时会触发Action中的逻辑。

Input:供Action消费的资源,资源可以是values、files和directories。

Output:Action执行后输出的产物,产物可以是values、files和directories,可以供其他任务消费。

1.创建任务的方式

有多种方式可以创建任务,相关接口都集中在Project和TaskContainer这两个类中。参考前面介绍的Project中的API,下面的代码展示了在build.gradle脚本文件中创建任务的方式。

//1.对应:Project#task(String name)
task("style1")
 
//2.对应:Project#task(String name, Closure configureClosure)
task("style2"){
    println("create task style2")
}
 
//3.对应:Project#task(Map<String,?> args, String name)
task(["description":"create task style3"],"style3"){
    println("create task style3")
}
 
//4.先获取TaskContainer,然后通过TaskContainer#create()方法来创建任务
getTasks().create("style4"){
    println("create task style4")
}
 
//5.先获取TaskContainer,然后通过TaskContainer#register()方法来创建任务
//建议使用这种方式来创建任务,使用懒加载的方式,在使用的时候才真正调用闭包中的值
getTasks().register("style5"){
    println("create task style5")
    doLast{
        println("!23123")
    }
}
 
//6.先创建一个DefaultTask的子类,然后将类型传给TaskContainer#create()方法
class MyTask1 extends DefaultTask{
    @TaskAction
    void doSomething(){
        println("create task style6")
    }
}
//这里的tasks == getTasks,是Groovy的语法特性
tasks.create("style6", MyTask1.class)

上文演示了创建任务的基本方式,创建完任务以后,使用如下命令来运行。

# 运行style6这个Task
$./gradlew style6
# Windows中使用gradlew.bat这个命令

输出结果如下。

$ ./gradlew style6
Starting a Gradle Daemon (subsequent builds will be faster)
 
> Configure project :app  #配置阶段
create task style2
create task style3
create task style4
 
> Task :app:style6        #执行阶段
create task style6
 
BUILD SUCCESSFUL in 11s
1 actionable task: 1 executed

通过输出结果可以看到,当执行style6任务的时候,其他任务闭包中的代码也执行了,这是因为在Gradle生命周期的配置阶段中会执行各项目的build.gradle脚本并创建和执行任务中的配置代码。另外,我们注意到style5任务的闭包中的代码没有被执行,因为通过register()创建的任务是懒加载的,只有在使用的时候才会执行,这样效率更高,也是官方推荐的做法。

2.任务运作

任务运作是任务在执行的时候运行的代码,例如运行如下代码。

Task task1 = task("task1"){
    println("task1 configure")
    doLast{
        println("task1 action==>doLast")
    }
}
 
//运行任务task1
$./gradlew task1

输出结果如下。

> Configure project :app
task1 configure
 
> Task :app:task1
task1 action==>doLast
 
BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed

代码中doLast闭包的作用就是给任务添加动作,动作会在任务执行的时候运行。表1-1列出了任务中与动作相关的方法。

表1-1 任务中与动作相关的方法

返回值类型

方法

描述

Task

doFirst(Closure action)

将动作添加到动作列表的头部

Task

doFirst(String actionName,Action<? superTask> action)

将动作添加到动作列表的头部,actionName可用于日志输出

Task

doFirst(Action<? super Task> action)

将动作添加到动作列表的头部

Task

doLast Closure action)

将动作添加到动作列表的尾部

Task

doLast(String actionName, Action<? super Task> action)

将动作添加到动作列表的尾部

Task

doLast(Action<? super Task> action)

将动作添加到动作列表的尾部

List<Action<? super Task>>

getActions()

获取所有的动作

void

setActions(List<Action<? super Task>> actions)

设置动作集合,会删除已经添加的动作

任务内部维护了一个List<InputChangesAwareTaskAction> actions列表,doFirst表示添加到列表的头部,doLast表示添加到列表的尾部。除了上面提供的方法外,Gradle还提供了@TaskAction注解,用于添加动作,例如下面这种用法。

class MyTask extends DefaultTask{
    @TaskAction
    void action1(){
        println("action1")
    }
 
    @TaskAction
    void action2(){
        println("action2")
    }
}
 
Task task = tasks.create("myTask", MyTask)
task.doFirst {
    println("do first")
}
task.doLast {
    println("do last")
}

运行以后的输出结果如下。

$ ./gradlew myTask
> Task :app:myTask
do first
action1
action2
do last

3.查找任务

在实际使用中,可能经常要查找一些任务,找到后对其进行配置,例如为其添加依赖、动作等。Gradle提供了一些查找任务的方法,这些方法主要集中在TaskContainer类中,相关方法如表1-2所示。

表1-2 查找任务的方法

返回值类型

方法

描述

Task

findByPath(String path)

根据路径来查找任务,例如“:app:myTask”,意思是查找app项目中名字是myTask的任务,如果找不到则返回null

Task

getByPath(String path)

根据路径来查找任务,如果找不到则抛出UnknownTaskException

T

findByName(String name)

通过名字来查找任务,如果找不到则返回null

T

getByName(String name)

通过名字来查找任务,如果找不到则抛出UnknownTaskException

TaskProvider<T>

named(String name)

查找任务,这里返回的是TaskProvider,可以通过get()方法来获取任务

下面是使用示例。

getTasks().named("myTask").get().doLast {
    println("last2")
}
 
getTasks().findByName("myTask").doLast {
    println("last3")
}
 
getTasks().getByPath(":app:myTask").doLast {
    println("last4")
}

4.任务的依赖方式

前面讲过任务是按照DAG方式依赖的,当我们单击“AndroidStudio→Build→Make Project”的时候,实际上是执行Gradle的一系列任务。

Executing tasks: [:app:assembleDebug] in project /Users/wangzhuozhou/Documents/work/    others/book-asm/Chapter1_00
 
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:compileDebugAidl NO-SOURCE
> Task :app:compileDebugRenderscript NO-SOURCE
> Task :app:generateDebugBuildConfig UP-TO-DATE
> Task :app:javaPreCompileDebug UP-TO-DATE
...
> Task :app:mergeDexDebug UP-TO-DATE
> Task :app:mergeDebugJniLibFolders UP-TO-DATE
> Task :app:mergeDebugNativeLibs UP-TO-DATE
> Task :app:stripDebugDebugSymbols NO-SOURCE
> Task :app:validateSigningDebug UP-TO-DATE
> Task :app:packageDebug UP-TO-DATE
> Task :app:assembleDebug UP-TO-DATE
 
BUILD SUCCESSFUL in 2s
25 actionable tasks: 25 up-to-date
 
Build Analyzer results available

可以看到上面执行了:app:assembleDebug任务,同时执行了很多其他的任务,这是因为任务间会相互依赖,被依赖的任务也会执行,任务间的依赖按照DAG规则进行。可以通过如下命令来查看任务在执行时,有哪些依赖的任务会执行。

$ ./gradlew taskName --dry-run

也可以使用一些第三方的插件来更加直观地显示任务依赖树。

任务接口中也提供了dependsOn(Object...)或setDependsOn(Iterable)方法来设置依赖。

task("lib1"){
    doLast {
        println("lib1 executed!")
    }
}
 
task("lib2"){
    doLast {
        println("lib2 executed!")
    }
}
 
task("lib3"){
    doLast {
        println("lib3 executed!")
    }
    dependsOn("lib2", "lib1")
}

上面的代码定义了3个任务,其中lib3依赖lib2、lib1。运行lib3,输出结果如下。

$ ./gradlew lib3
 
> Task :app:lib1
lib1 executed!
 
> Task :app:lib2
lib2 executed!
 
> Task :app:lib3
lib3 executed!

可以看到运行lib3后,其依赖的任务先运行了,最后运行lib3。另外,还需注意到依赖的任务的运行顺序与通过dependsOn()方法添加的顺序不一致,所以dependsOn()方法并不能保证依赖的任务的运行顺序。其实还有一种非常重要的依赖类型,我们将在1.2.10小节对其进行介绍。

5.任务排序

Gradle中并没有真正的任务排序的功能,这是由Gradle的设计思想决定的,即用户不用关注任务链上的任务连接方式、不用关注任务链上有哪些任务,而只用声明在一个给定的任务运行之前什么任务应该被运行。Gradle为任务排序制定了两条规则——mustRunAfter和shouldRunAfter,对应着任务的如下两个方法。

Task mustRunAfter(Object... paths);

TaskDependency shouldRunAfter(Object... paths)。

这两个方法的使用方式如下。

(1)mustRunAfter的使用方式如下。

def taskA = tasks.register('taskA') {
    doLast {
        println 'taskA'
    }
}
def taskB = tasks.register('taskB') {
    doLast {
        println 'taskB'
    }
}
taskB.configure {
    mustRunAfter taskA
}

执行上面的taskA、taskB。

$ ./gradlew -q taskB taskA
taskA
taskB

(2)shouldRunAfter的使用方式如下。

def taskA = tasks.register('taskA') {
    doLast {
        println 'taskA'
    }
}
def taskB = tasks.register('taskB') {
    doLast {
        println 'taskB'
    }
}
taskB.configure {
    shouldRunAfter taskA
}

执行上面的taskA、taskB。

$ ./gradlew -q taskB taskA
taskA
taskB

这里要注意,通过上面这两种方式确定的任务执行顺序并不代表任务之间创建了依赖关系。例如单独执行上面的taskB并不会执行taskA。对于shouldRunAfter(),我们还需要注意一点,其可能并不起作用,前面介绍了Gradle的任务是一个DAG,假如破坏了这种结构,shouldRunAfter()将不起作用。

1.2.6 生命周期回调

前面介绍了Gradle生命周期的3个阶段,对此Gradle提供了相应的生命周期回调方法,接下来分别介绍Project、Gradle、Task这3个类中设置的回调方法。

1.Project类

Project类提供的回调方法如下。

//在Project配置前调用
void beforeEvaluate(Closure closure)
//在Project配置后调用
void afterEvaluate(Closure closure)

这里要注意beforeEvaluate()需要在父Project类中调用,如果在当前的build.gradle中调用,当执行到beforeEvaluate()方法的时候实际上评估已经结束了,所以不会回调。除了在父Project类中配置外,还可以在Gradle中配置,后面会介绍。

2.Gradle类

Gradle类中提供的回调方法非常多,与Project类也有一些相似的回调方法。

//需要在父类Project或者settings.gradle中设置才会生效
void beforeProject(Closure closure)
 
//在Project配置后调用
void afterProject(Closure closure)
 
//构建开始前调用
void buildStarted(Closure closure)
 
//构建结束后调用
void buildFinished(Closure closure)
 
//所有Project配置完成后调用
void projectsEvaluated(Closure closure)
 
//当settings.gradle中引入的所有Project都被创建好后调用,只在该文件设置才会生效
void projectsLoaded(Closure closure)
 
//settings.gradle配置完后调用,只对settings.gradle设置生效
void settingsEvaluated(Closure closure)

除了上面这些提供的方法外,Gradle还提供了一些Listener,通过添加支持的Listener也能取代上面介绍的方法。总之,Gradle既提供了一些单独的闭包回调方法,也提供了一些Listener来实现相同的效果。

/**
 * Adds the given listener to this build. The listener may implement any of the     given listener interface
 *
 * <ul>
 * <li>{@link org.gradle.BuildListener}
 * <li>{@link org.gradle.api.execution.TaskExecutionGraphListener}
 * <li>{@link org.gradle.api.ProjectEvaluationListener}
 * <li>{@link org.gradle.api.execution.TaskExecutionListener}
 * <li>{@link org.gradle.api.execution.TaskActionListener}
 * <li>{@link org.gradle.api.logging.StandardOutputListener}
 * <li>{@link org.gradle.api.tasks.testing.TestListener}
 * <li>{@link org.gradle.api.tasks.testing.TestOutputListener}
 * <li>{@link org.gradle.api.artifacts.DependencyResolutionListener}
 * </ul>
 *
 * @param listener The listener to add. Does nothing if this listener has already     been added.
 */
void addListener(Object listener);

3.Task类

Gradle配置阶段结束后,任务会被构建成一个DAG,这决定了任务的执行顺序,同样,Task类也提供了对应的回调方法:

//任务执行前调用
void afterTask(Closure closure)
//任务执行后调用
void beforeTask(Closure closure)
//任务准备好后调用
void whenReady(Closure closure)

下面我们针对上面介绍的这些回调方法编写一个例子来说明,其中回调方法主要集中在settings.gradle中。读者也可以在app/build.gradle中定义一个任务,然后观察任务的执行顺序。settings.gradle中的代码如下。

//include ':lib'
include ':app'
rootProject.name = "Chapter1_00"
 
//called after project evaluated
getGradle().afterProject {
    println("setting: after project===${it}")
}
 
//called before project evaluated
getGradle().beforeProject {
    println("setting: before project===${it}")
}
 
getGradle().addBuildListener(new BuildListener() {
    @Override
    void buildStarted(Gradle gradle) {
        println("setting: buildStarted===${gradle}")
    }
 
    @Override
    void settingsEvaluated(Settings settings) {
        println("setting: settingsEvaluated===${settings}")
    }
 
    @Override
    void projectsLoaded(Gradle gradle) {
        println("setting: projectsLoaded===${gradle}")
    }
 
    @Override
    void projectsEvaluated(Gradle gradle) {
        println("setting: projectsEvaluated===${gradle}")
    }
 
    @Override
    void buildFinished(BuildResult result) {
        println("setting: buildFinished===${result}")
    }
})
 
getGradle().addListener(new TaskExecutionGraphListener() {
    //TaskGraph构成以后
    @Override
    void graphPopulated(TaskExecutionGraph graph) {
        println("setting: graphPopulated===${graph}")
    }
})
 
getGradle().addListener(new ProjectEvaluationListener(){
    @Override
    void beforeEvaluate(Project project) {
        println("setting: beforeEvaluate===${project}")
    }
 
    @Override
    void afterEvaluate(Project project, ProjectState state) {
        println("setting: afterEvaluate===${project}")
    }
})
 
getGradle().addListener(new TaskExecutionListener(){
    @Override
    void beforeExecute(Task task) {
        println("setting:task beforeExecute")
    }
 
    @Override
    void afterExecute(Task task, TaskState state) {
        println("setting:task afterExecute")
    }
})
 
getGradle().addListener(new TaskActionListener(){
 
    @Override
    void beforeActions(Task task) {
        println("setting:task beforeActions")
    }
 
    @Override
    void afterActions(Task task) {
        println("setting:task afterActions")
    }
})
 
getGradle().beforeSettings {
    println("setting:beforeSettings")
}
 
getGradle().buildStarted {
    println("setting:buildStarted2")
}
getGradle().buildFinished {
    println("setting:buildFinished2")
}
getGradle().projectsLoaded {
    println("setting:projectsLoaded22")
}
 
getGradle().getTaskGraph().addTaskExecutionGraphListener(new TaskExecutionGraphListener() {
    @Override
    void graphPopulated(TaskExecutionGraph graph) {
 
    }
})
 
getGradle().getTaskGraph().addTaskExecutionListener(new TaskExecutionListener() {
    @Override
    void beforeExecute(Task task) {
 
    }
 
    @Override
    void afterExecute(Task task, TaskState state) {
 
    }
})
getGradle().getTaskGraph().whenReady {
    println("setting:whenReady")
}
 
getGradle().getTaskGraph().beforeTask {
    println("setting:beforeTask2")
}

最后我们用一张图将Gradle生命周期的不同阶段对应的回调方法串联起来,如图1-2所示。

图1-2 Gradle生命周期的回调方法

1.2.7 Gradle执行流程

前文介绍了Gradle的生命周期回调,本小节将补充生命周期各阶段的一些细节。

1.初始阶段

前面我们介绍初始阶段会执行setting.gradle,并根据setting.gradle中的内容构建Project对象。如果按照执行的时间顺序,其内容包括如下。

执行init.gradle;

查找settings.gradle;

编译buildSrc目录;

解析gradle.properties内容;

编译并执行settings.gradle;

创建project和subproject。

其中buildSrc是Gradle Plugin的一种写法,gradle.properties是环境配置的文件,后面会进一步介绍,这里主要是要明白运行这两项的时机,特别是buildSrc的编译时机。

2.配置阶段

配置阶段会编译并执行build.gradle文件,构建Project中的内容,同时还有一项重要的操作是计算和生成任务依赖的DAG。

3.执行阶段

执行阶段就是执行任务中定义的动作。

1.2.8 获取属性的几种常见方式

在Gradle中有下面几种常见的方式可用来获取属性。

从extra属性中获取,例如在Android中,大家都会把compileSdkVersion、buildToolsVersion、依赖库的版本号等对应的值放在rootProject.ext{}语句块中,它实际上是Android插件的一个扩展,这部分会在扩展部分进行详细介绍。

从extension中获取,例如在app工程的build.gradle中定义的android{}语句块就是采用这种方式。当我们需要提供一些配置信息的时候可以使用extension,这部分在扩展部分进行介绍。

从gradle.properties中获取,gradle.properties这个文件一般用来设置环境变量,但是我们也可以将一些属性以键值对的形式存放在此文件中。

Gradle中还有其他方式可以获取属性,读者可以参考官方介绍了解。

1.2.9 任务执行后的几种状态

当任务执行完以后,会在控制台上显示任务的执行结果,例如使用 ./gradlew assembleDebug来编译App debug包的时候,任务执行结果如图1-3所示。

图1-3 Gradlew assembleDebug任务执行结果

这里的UP-TO-DATE、NO-SOURCE表示任务执行后的状态结果。Gradle中的任务执行结果状态如表1-3所示。

表1-3 Gradle中的任务执行结果状态

任务执行结果状态

描述

前提

no label

或者EXECUTED

任务执行了动作

任务有动作并且执行了

任务没有动作但有一些依赖,这些依赖都被执行了

UP-TO-DATE

任务的输出没有变化

任务有输入和输出,这些输入和输出没有变化

任务有动作,但是任务告诉Gradle,其输出没有变化

任务没有动作但是有一些依赖,这些依赖都是UP-TO-DATE、SKIPPED或者FROM-CACHE

任务没有动作也没有依赖

FROM-CACHE

任务的输出可以从之前执行的缓存中找到

任务在构建缓存中可以找到相应的输出

SKIPPED

任务有动作,但是没有执行

任务在命令行中被声明为跳过不执行

任务的onlyIf判断返回false

NO-SOURCE

任务不需要去执行动作

任务有输入和输出,但是没有来源(Source)

1.2.10 增量构建

增量构建是Gradle中很重要的一个知识点,试想一下在编译的时候,当你输入的文件没有发生变化,意味着输出也不会有变化,那就不需要再执行编译,直接将上一次的编译结果返回即可;而当只修改了部分文件时,也只需要单独编译发生变化的文件即可,这就是增量编译。

前面介绍任务由输入、输出和动作组成,任务的输入和输出作为增量构建的一部分,Gradle会检查上一次构建依赖的输入和输出是否有变化,如果没有变化,Gradle认为该任务是最新的,就会跳过任务的执行,任务的结果状态是UP-TO-DATE。需要注意的是,在Gradle中至少要有一个任务输出,否则增量构建将无法工作。表1-4所示为Gradle提供的与增量相关的注解。

表1-4 Gradle提供的与增量相关的注解

注解

类型

描述

@Input

Serializable

可序列化的输入对象

@InputFile

File

单个输入文件 (不包括文件夹)

@InputDirectory

File

单个输入文件夹 (不包括文件)

@InputFiles

Iterable<File>

输入文件或文件夹的集合

@Classpath

Iterable<File>

Java路径的输入文件和文件夹的集合

@CompileClasspath

Iterable<File>

编译Java路径的输入文件和文件夹的集合

@OutputFile

File

单一输出文件

@OutputDirectory

File

单一输出文件夹

@OutputFiles

Map<String, File> 或Iterable<File>

输出文件的集合或者映射

@OutputDirectories

Map<String, File> 或Iterable<File>

输出文件夹的集合或者映射

@Destroys

File或Iterable<File>

指定此任务销毁的一个文件或文件集合。请注意,任务可以定义输入/输出或可销毁对象,但不能同时定义两者

@LocalState

File或Iterable<File>

本地任务状态,当任务从缓存恢复的时候,这些文件将会被移除

@Nested

任意自定义类型

嵌套属性

@Console

任意类型

辅助属性,指出修饰属性不为输入或者输出属性

@Internal

任意类型

内部使用属性,指出修饰属性不为输入或者输出属性

@ReplacedBy

任意类型

指示该属性已被另一个替换,作为输入或输出被忽略

@SkipWhenEmpty

File

和 @InputFiles / @InputDirectory配合使用, 告诉Gradle如果相应的文件或目录为空,以及使用此注释声明的所有其他输入文件为空,则跳过任务,任务的状态是NO-SOURCE

@Incremental

Provider<FileSystemLocation> 或FileCollection

和 @InputFiles / @InputDirectory配合使用, 告诉Gradle跟踪对文件属性的更改。可以通过InputChanges.getFileChanges() 查询更改

@Optional

任意类型

可选属性

@PathSensitive

File

文件属性的类型

让我们看看如何使用表1-4中的注解来创建增量任务。

class IncrementalTask extends DefaultTask {
 
    @OutputFile //用在Property()方法上
    File outputDir= getProject().file(".proguard-rules.pro")
 
    @OutputFile //用在get()方法上
    File getOutputFile(){
        return getProject().file(".proguard-rules.pro")
    }
 
    @TaskAction
    void doAction(){
        println("do something")
    }
}
 
getTasks().create("testIncremental", IncrementalTask)

注意,Gradle提供的与增量相关的注解要么用在Property()方法上,要么用在get()方法上。运行testIncremental任务两次,当第二次运行的时候会看到如下输出。

$./gradlew testIncremental
 
BUILD SUCCESSFUL in 840ms
1 actionable task: 1 up-to-date

可以看到up-to-date关键字,表示输入和输出没有变化,没有执行动作中的内容直接返回了结果,这就是增量构建的一个简单应用。

前面介绍任务依赖的知识时,还提到一种依赖,就是Gradle可以根据任务的输入和输出来推断依赖关系,例如下面的这段代码。

class MyTask1 extends DefaultTask {
    @OutputFiles
    FileCollection outputDir
 
    @TaskAction
    void processTemplates() {
 
    }
}
 
class MyTask2 extends DefaultTask {
    @InputFiles
    FileCollection inputFileTmp
 
    @TaskAction
    void t() {
        println("task2")
    }
}
 
def task1 =  tasks.create('task1', MyTask1) {
    println("task1")
    doLast{
        println("task1 doLast")
    }
    outputDir = 
        files(layout.buildDirectory.getAsFile().get().getAbsolutePath()+"/log.txt")
}
 
tasks.create("task2", MyTask2){
    inputFileTmp = files(task1.outputs)
}

上面的代码是通过输入和输出的依赖关系来确定两个任务之间的依赖,当执行任务task2的时候,task1也会一起执行,这其实就是第2章介绍的Android Gradle Plugin Transform对应的TransformTask的关键原理。

1.3 插件类型

前文介绍了什么是Gradle插件,以及有关Gradle的一些基础知识,本节将正式介绍Gradle插件的用法,首先来认识Plugin<T>接口。

package org.gradle.api;
 
/**
 * <p>A <code>Plugin</code> represents an extension to Gradle. A plugin applies     some configuration to a target object.
 * Usually, this target object is a {@link org.gradle.api.Project}, but plugins     can be applied to any type of
 * objects.</p>
 *
 * @param <T> The type of object which this plugin can configure.
 */
public interface Plugin<T> {
    /**
     * Apply this plugin to the given target object.
     *
     * @param target The target object
     */
    void apply(T target);
}

可以看到插件的这个接口只有一个apply()方法,通常这里的泛型T基本上都是org.gradle.api.Project类型。本书中所指的插件均指实现了此接口的插件,需要注意的是apply()方法中的代码会在Gradle配置阶段执行。插件的使用也比较简单,例如apply plugin: 'com.android.application',这里实际上调用的是Project.apply(Map<String, ?> options)方法。

根据插件代码所在位置的不同,Gradle提供了3种创建插件的方式。

直接在构建脚本(build.gradle)中定义插件,我们称之为脚本插件;

创建一个名为buildSrc的模块(module),并在其中定义插件,我们称之为buildSrc插件;

在一个单独的项目中定义插件,我们称之为单独项目插件。

插件的实现可以使用任何基于Java虚拟机(Java Virtual Machine,JVM)的编程语言,例如Java、Kotlin、Groovy。通常实现插件的时候推荐使用Java或Kotlin,相对于Groovy,这两种语言是静态类型的,在开发的过程中借助集成开发环境(Intergrated Development Environment,IDE)的语法检测会方便很多。

接下来依次介绍这3种类型的插件。

1.3.1 脚本插件

脚本插件是指在构建脚本中定义的插件。这么做的好处是插件会被自动编译并且添加到构建的classpath中,用户不需要做其他操作;坏处是只能在当前的构建脚本中使用,其他的构建脚本无法使用。

下面创建一个Android项目并在app/build.gradle中实现脚本插件。

第一步:创建一个新的Android项目,名称为Chapter1_01。

第二步:在app/build.gradle文件中创建插件的实现类。

class MyScriptPlugin implements Plugin<Project> {
 
    @Override
    void apply(Project target) {
        println("My First Plugin")
        target.task("getBuildDir") {
            println("get build dir at configure phase") //配置阶段输出
            doLast { //为任务添加动作
                println("build dir: ${buildDir}")
            }
        }
    }
}

上述代码在apply()方法中输出了一条日志,并创建了一个名为getBuildDir的任务,该任务输出编译目录。

第三步:引用插件。

在app/build.gradle中引用插件。

//使用我们定义的插件
apply plugin: MyScriptPlugin

第四步:执行getBuildDir任务进行测试。

$ ./gradlew getBuildDir
 
> Configure project :app
My First Plugin
get build dir at configure phase
 
> Task :app:getBuildDir
build dir: /Users/wangzhuozhou/Documents/work/others/book-asm/Chapter1_01/app/build

可以看到在配置阶段输出了“My First Plugin” 并且输出了build目录。

完整的app/build.gradle文件内容如下。

plugins{
    id 'com.android.application'
}
apply plugin: MyScriptPlugin //使用插件
 
android {
    ...
}
 
dependencies {
    ...
}
 
class MyScriptPlugin implements Plugin<Project> {
 
    @Override
    void apply(Project target) {
        println("My First Plugin")
        target.task("getBuildDir") {
            println("get build dir at configure phase") //配置阶段输出
            doLast { //为任务添加动作
                println("build dir: ${target.buildDir}")
            }
        }
    }
}

1.3.2 buildSrc插件

buildSrc插件指的是在项目中定义一个名为buildSrc的模块,在模块中实现插件逻辑。这种插件的优点是项目中的所有模块都可以使用、Gradle负责编译并将其加入构建环境中,缺点是不方便单独维护和发布。下面介绍如何将脚本插件转换成buildSrc插件。

第一步:创建一个新的Android项目,名称为Chapter1_02。

第二步:创建一个新的模块,类型选择“Java Library”,名称为buildSrc。在Android Studio中的完整操作路径是:File→New→New Module→Java Library→Next→Finish。在创建完buildSrc Library以后,Gradle会重新编译,Android Studio会报图1-4所示的错误。

图1-4 编译报错

这是因为buildSrc是一个保留名字,不能用于项目名,只需将settings.gradle文件中的include ':buildSrc'配置删除即可。

第三步:创建插件类,首先在buildSrc模块中创建MyBuildSrcPlugin.java类来实现Plugin接口,项目结构和示例代码如图1-5所示。

注意,这里使用的语言是Java,也可以使用Groovy或Kotlin来实现。

第四步:在app/build.gradle文件中使用插件。

apply plugin: cn.sensorsdata.asmbook.plugin.MyBuildScrPlugin

第五步:执行getBuildDir任务进行测试,可以看到控制台中输出了插件的内容。

$ ./gradlew getBuildDir
 
> Configure project :app
My Second Plugin
 
> Task :app:getBuildDir
build dir: /Users/wangzhuozhou/Documents/work/others/book-asm/Chapter1_02/app/build

图1-5 项目结构和示例代码

本小节介绍了buildSrc插件,使用的时候只需要在模块中进行引入即可,这种插件通常适合项目内部使用。

1.3.3 单独项目插件

相对于脚本插件和buildSrc插件,单独项目插件要复杂一些,可以将其打成插件包并发布出去,发布后更方便他人使用。接下来介绍如何创建单独项目插件。

第一步:创建一个新的Android项目,名称为Chapter1_03。

第二步:创建一个新的模块,类型选择“Java Library”,名称为myPlugin。在Android Studio中的完整操作路径是:File→New→New Module→Java Library→Next→Finish。

第三步:修改myPlugin/build.gradle文件,将依赖的java-library插件改成依赖java插件,并依赖Gradle API,修改后的内容如下。

apply plugin:'java' //依赖java插件
apply plugin:'maven-publish' //依赖maven-publish插件
 
java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}
 
dependencies {
    implementation gradleApi() //依赖Gradle API,才能使用Plugin这个接口
}
 
//组织或者公司名称
group="cn.sensorsdata.asmbook.plugin"
 
//版本号
version='1.0.0'
 
//模块名称
archivesBaseName='myPlugin'
 
//打包Maven
//使用命令 ./gradlew publish或者publish{PubName}PublicationTo{RepoName}Repository形式                 的命令
publishing {
    publications {
        myLibrary(MavenPublication) {
            from components.java
        }
    }
 
    repositories {
        maven {
            url '../maven-repo'
        }
    }
}

publishing{ }语句块用于配置Maven仓库的版本信息,此处的配置是将插件发布到本地。Maven仓库发布还需要指定如下信息。

group:指groupId,一般为组织或者公司名称。

version:库的版本。

archivesBaseName:指artifactId,是项目名或者模块名。

第四步:编写插件。

在myPlugin模块中创建插件实现类。

public class MyStandalonePlugin implements Plugin<Project> {
 
    @Override
    public void apply(Project project) {
        System.out.println("My Third Plugin");
 
        Task task = project.task("getBuildDir");
        task.doLast(task1 -> System.out.println("build dir: " + project.    getBuildDir()));
    }
}

第五步:编译插件。

单击图1-6所示的publish任务即可编译插件并发布到本地Maven仓库。

图1-6 publish任务

也可以在终端中执行publish任务。

$ ./gradlew publish

该任务的作用是打包Maven并将其发布到本地。此时,在项目的根目录下会多一个maven-repo目录,目录中的文件即构建的插件,如图1-7所示。

图1-7 maven-repo目录

第六步:添加对插件的依赖。修改项目根目录下的rootProject/build.gradle文件,添加对插件的依赖。

// Top-level build file where you can add configuration options common to all sub-    projects/modules.
buildscript {
    repositories {
        google()
        mavenCentral()
        maven {
            url(uri("maven-repo"))
        }
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.1.2"
        classpath 'cn.sensorsdata.asmbook.plugin:myPlugin:1.0.0' //依赖插件
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}
 
allprojects {
    repositories {
        google()
        mavenCentral()
    }
}
 
task clean(type: Delete) {
    delete rootProject.buildDir
}

下面这段代码是声明在哪里寻找插件,因为是在rootProject/build.gradle文件中声明的,所以这里的意思是在build.gradle同级目录中寻找maven-repo。

maven{
    url(uri('maven-repo'))
}

另外,下面这段代码表示添加插件的依赖(即将库添加到classpath中,这样编译的时候才能找到)。

dependencies {
    classpath 'cn.sensorsdata.asmbook.plugin:myPlugin:1.0.0'
}

第七步:使用插件。

修改app/build.gradle文件,添加引用插件的代码。

apply plugin: cn.sensorsdata.asmbook.myplugin.MyStandalonePlugin

然后运行getBuildDir任务。

$ ./gradlew getBuildDir
 
> Configure project :app
My Third Plugin
 
> Task :app:getBuildDir
build dir: /Users/wangzhuozhou/Documents/work/others/book-asm/Chapter1_03/app/build

可以看到插件正常运行了。你可能注意到引用插件时需要填写完整的包名和类名,字符串比较长,难以书写和记忆。我们也可以给插件起一个精简且有意义的名字,比如“testplugin”,下面介绍如何操作。

1.3.4 单独项目插件优化

首先在myPlugin/src/main目录下创建resources/META-INF.gradle-plugins目录,再在gradle-plugins目录下新建一个属性配置文件testplugin.properties,这里的testplugin就是最终的插件名,最后在testplugin.properties文件中添加如下内容。

implementation-class=cn.sensorsdata.asmbook.myplugin.MyStandalonePlugin

效果如图1-8所示。

上述内容声明了插件的实现类,重新编译插件,这样就可以通过如下方式依赖插件。

apply plugin: 'testplugin'

再次运行getBuildDir任务,可以看到输出了预期的结果。

$ ./gradlew getBuildDir
 
> Configure project :app
My Third Plugin
 
> Task :app:getBuildDir
build dir: /Users/wangzhuozhou/Documents/work/others/book-asm/Chapter1_03/app/build

图1-8 定义插件META-INF信息后的效果

以上就是一个单独项目插件的标准做法。可以看到定义一个单独项目插件的操作步骤很多,那么有没有方式可以简化操作步骤呢?答案是有,Gradle官方提供了java-gradle-plugin插件,该插件具有如下作用。

添加java插件,这样就不需要添加apply plugin: 'java'这段代码;

自动依赖Gradle API,这样就不需要添加implementation gradleApi() 这段代码;

自动生成resources/META-INF.gradle-plugins目录和properties文件,这样就不需要手动添加;

打包时自动生成插件标识产物(plugin marker atifacts),1.3.5小节会详细介绍;

配合maven-publish插件生成对应的Maven包和Gradle Plugin Portal包。

接下来继续对插件进行优化。复制一份Chapter1_03项目,将其副本的名称改为Chapter1_03_better。然后修改myPlugin/build.gradle文件,添加java-gradle-plugin插件。修改后的内容如下。

plugins{
    id 'java-gradle-plugin'
    id 'maven-publish'
}
 
java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}
 
//不再需要依赖Gradle API
dependencies {
    implementation gradleApi()
}
 
//组织或者公司名称
group="cn.sensorsdata.asmbook.plugin"
 
//版本号
version='1.0.0'
 
//模块名称
archivesBaseName='myPlugin'
 
//Gradle插件配置
gradlePlugin {
    plugins {
        myPluginB {
            id = 'mytestPlugin'
            implementationClass = 'cn.sensorsdata.asmbook.myplugin.    MyStandalonePlugin'
        }
    }
}
 
//打包
publishing {
    //可以不需要
    publications {
        myLibrary(MavenPublication) {
            from components.java
        }
    }
 
    repositories {
        maven {
            url '../maven-repo'
        }
    }
}

引入了java-gradle-plugin后就可以删除Gradle API的依赖与resources/META-INF目录中的配置。在上述配置中新添加了gradlePlugin{ }语句块,它的作用是配置插件发布信息,其中的id项定义了插件的唯一标识,implementationClass项定义了插件的实现类。这一段配置相当于创建了resources/META-INF.gradle-plugins/mytestPlugin.property文件,文件内容是implementation-class=cn.sensorsdata.asmbook.myplugin.MyStandalonePlugin。

运行 ./gradlew publish命令来打包,可以看到生成了maven-repo,其内容如图1-9所示。

可以看到maven-repo目录下有两个子目录,其中cn这个子目录,与之前发布的包结构是一致的,子目录mytestPlugin与cn不一样,它的结构符合插件标识产物的结构,这样我们就可以使用Gradle提供的plugins{} DSL来声明插件,例如:

plugins{
    id 'com.android.application'
    id 'mytestPlugin' version '1.0.0' //试用此种方式来使用插件
}
 
android {
    ...
}

图1-9 maven-repo的内容

关于plugins{}会在1.3.5小节详细介绍。根据myPlugin/build.gradle文件中的gradlePlugin{ }语句块的作用,新生成插件的id是mytestplugin,这一点可以通过反编译myPlugin-1.0.0.jar看到,图1-10所示为反编译的结果。

图1-10 反编译结果

对此需要调整app/build.gradle,修改后的结果如下。

apply plugin: 'com.android.application'
apply plugin: 'mytestPlugin'

当然也可以使用下面的方式来声明插件。

plugins{
    id 'com.android.application'
    id 'mytestPlugin'
}

以上就是优化后的插件,接下来介绍plugins{} DSL的用法。

1.3.5 插件使用方式

前文简单介绍了插件的使用方式,本小节对此做更进一步的介绍。Gradle在使用插件时需要执行两步操作。

识别插件;

应用插件。

其中,识别插件的意思是从插件JAR包中找到对应的版本并将其添加到构建环境中;应用插件意味着具体执行Plugin.apply(T)方法。为能识别插件,需要在rootProject/build.gradle的buildscript块中引入插件,再在module/build.gradle中应用插件。Gradle为简化识别插件和应用插件的步骤,引入了plugins{} DSL语句块,将两个步骤放在一个步骤中,例如使用下面的代码引入插件。

plugins {
    id 'com.jfrog.bintray' version '1.8.5'
}

相比于“传统”的apply()方法,plugins{} DSL不仅简化了使用方式,还有如下优点。

(1)优化了插件的加载速度;

(2)不同的项目可以使用不同版本的插件;

(3)良好的IDE支持。

例如对于同一个插件,不同的模块可以使用不同的版本插件,这个对于传统的apply()方法是无法实现的。不过使用plugins{}也有一些限制,plugins{}语句块必须在build.gradle文件的顶部声明,而apply()可以在任何地方使用;再有plugins{}的语句块有严格的语法限制,如果不满足语法就会产生编译器报错,其语法格式如下。

plugins {
    id«plugin id»                                         (1)
    id«plugin id»version«plugin version» [apply«false»]   (2)
}

其中(1)表示应用核心插件(例如java插件)或者对构建脚本来说插件已经可用;(2)表示应用插件并指定某个版本,apply«false»表示引入插件但不使用。假设项目中有一个名为module-a的模块,rootProject/build.gradle文件中有如下内容。

plugins {
    id 'com.example.hello' version '1.0.0' apply false
}

上述代码表示在rootProject/build.gradle中引入com.example.hello插件,根据加载的顺序,此时插件对module-a来说已经可用。module-a中可按照如下方式引入插件,即不需要指定版本号。

plugins {
    id 'com.example.hello'
}

除了上述两个限制外,plugins{}在使用上还有一个限制,前面提到plugins{}将识别插件和应用插件集中在一个步骤中,这是有前提的,即它需要满足Gradle Plugin Portal(Gradle官方插件平台)的格式标准才能如此简单地使用,如果插件托管在Maven上,仍然需要在rootProject/build.gradle的buildscript块中声明插件信息。

Gradle Plugin Portal要求的插件信息称为插件标识产物,它的格式如下。

{plugin.id}:{plugin.id}.gradle.plugin:{plugin.version}

Gradle加载plugins{}语句块中的插件时会按照上述格式去定位插件。另外,如果插件已经上传到Gradle Plugin Portal,就可以在plugins{}中直接声明插件;如果插件在本地,还需要在settings.gradle中声明插件的位置,这与传统的apply()需要在rootProject/build.gradle中声明repositories是不同的。下面使用具体的例子来演示带有插件标识产物的插件在本地的用法。

复制项目Chapter1_03_better,并将其副本的项目名修改为Chapter1_03_best。使用java-gradle-plugin和maven-publish插件,java-gradle-plugin插件会在使用maven-publish发布版本时生成符合插件标识产物的插件,运行 ./gradlew publish命令,其输出结果如图1-11所示。

图1-11 ./gradlew publish命令的输出结果

可以看到maven-repo中mytestPlugin的目录结构符合插件标识产物的格式。接下来就是引入插件,首先删除rootProject/build.gradle中传统的插件依赖方式,修改后的结果如下。

buildscript {
    repositories {
        google()
        mavenCentral()
        //删除传统的插件依赖方式
        maven {
            url(uri("maven-repo"))
        }
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.1.2"
        classpath 'cn.sensorsdata.asmbook.plugin:myPlugin:1.0.0'//删除传统的插件依赖方式
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}
...

接着在settings.gradle文件中使用pluginManagement{}语句块声明插件仓库,修改后的结果如下。

//声明依赖关系
pluginManagement {
    plugins {
    }
    repositories {
        maven {
            url(uri("maven-repo"))
        }
    }
}
 
include ':myPlugin'
include ':app'
rootProject.name = "Chapter1_03_best"

然后就可以在app/build.gradle中使用插件,代码如下。

//传统方式
apply plugin: 'com.android.application'
apply plugin: 'mytestPlugin'
 
plugins{
    id 'com.android.application'
    id 'mytestPlugin' version '1.0.0' //使用插件
}
 
 
android {
   ...
}
 
dependencies {
    ...
}

本小节主要介绍了plugins{} DSL的使用方式,关于如何将插件放到Gradle Plugin Portal平台上,会在1.6.1小节详细介绍。

1.3.6 小结

本小节我们从简单到复杂依次介绍了脚本插件、buildSrc插件以及单独项目插件,关于这3种创建自定义插件的方式,我们做了总结,如表1-5所示。

表1-5 不同插件的总结

创建插件方式

总结

直接在构建文件build.gradle中自定义插件

简单直接,不方便在当前项目内的其他构建文件引用

创建buildSrc模块自定义插件

适用于项目内模块引用,并且不需要对插件进行发布

创建Gradle Plugin项目自定义插件

方便分享和传播,需要将插件单独发布

1.4 Gradle扩展

1.4.1 什么是扩展

关于扩展,首先来看看Android项目app/build.gradle中这段非常经典的代码。

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"
 
    defaultConfig {
        applicationId "cn.sensorsdata.asmbook.chapter1_04"
        minSdkVersion 16
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"
 
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
 
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile(
                'proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

相信Android开发者对这段配置很熟悉,但读者可能会有如下疑问:

Project类中并没有android{}这样的API,为何能这样配置?

其实,上面的这段配置用的是Gradle的扩展机制,简称扩展。它的作用是为插件提供配置,赋予插件特定的功能。

1.4.2 ExtensionContainer API介绍

要想自定义扩展,必须先了解ExtensionContainer。这是因为一般情况下,我们都是通过ExtensionContainer来管理扩展的,可以通过Project的getExtensions()方法获取当前Project的ExtensionContainer对象。Project中此方法的定义如下。

/**
 * Allows adding DSL extensions to the project. Useful for plugin authors.
 *
 * @return Returned instance allows adding DSL extensions to the project
 */
@Override
ExtensionContainer getExtensions();

ExtensionContainer中常用的API如下。

/**
 * Allows adding 'namespaced' DSL extensions to a target object.
 */
@HasInternalProtocol
public interface ExtensionContainer {
 
    /**
     * 在容器中添加一个新的扩展
     *
     * @param publicType扩展向外暴露的类型
     * @param name扩展的名字,例如上面提到的“android”,可以是任意字符串,
     * 但需要确保唯一性,否则会有异常
     * @param extension实现了publicType类型的扩展实例
     * @throws IllegalArgumentException如果该名字的扩展已经存在,就会抛出异常
     */
    <T> void add(Class<T> publicType, String name, T extension);
 
    /**
     * 在容器中添加一个新的扩展
     *
     * @param name要创建的扩展的名字,可以是任意字符串,
     * 但需要确保唯一性,否则会有异常
     * @param Extension实例
     * @throws IllegalArgumentException如果该名字的扩展已经存在就会抛出异常
     */
    void add(String name, Object extension);
 
    /**
     * 创建一个扩展并添加到容器中
     *
     * @param <T> 扩展对外暴露的类型
     * @param publicType扩展对外暴露的类型
     * @param name要创建的扩展的名字,可以是任意字符串,
     * 但需要确保唯一性,否则会有异常
     * @param instanceType此扩展的实例类型
     * @param constructionArguments构造扩展实例时需要的构造参数
     * @return返回创建的扩展实例
     * @throws IllegalArgumentException如果该名字的扩展已经存在就会抛出异常
     */
    <T> T create(Class<T> publicType, String name,  
          Class<? extends T> instanceType,Object...constructionArguments);
 
    /**
     * 创建一个扩展并添加到容器中
     *
     * @param name要创建的扩展的名字,可以是任意字符串,
     * 但需要确保唯一性,否则会有异常
     * @param type此扩展的实例类型
     * @param constructionArguments构造扩展实例时需要的构造参数
     * @return返回创建的扩展实例
     * @throws IllegalArgumentException如果该名字的扩展已经存在就会抛出异常
     */
    <T> T create(String name, Class<T> type, Object... constructionArguments);
 
    /**
     * 根据扩展类型获取实例,如果找不到会抛出异常
     *
     * @param type扩展类型
     * @return返回扩展,不会为null
     * @throws UnknownDomainObjectException如果根据扩展类型找不到扩展就会抛出异常
     */
    <T> T getByType(Class<T> type) throws UnknownDomainObjectException;
 
    /**
     * 根据扩展类型获取实例,如果找不到会返回null,注意与getByType的区别
     *
     * @param type 扩展类型
     * @return extension or null
     */
    @Nullable
    <T> T findByType(Class<T> type);
 
    /**
     * 根据扩展的名字获取实例,如果找不到会抛出异常
     *
     * @param name扩展的名字
     * @return返回扩展,不会为null
     * @throws UnknownDomainObjectException如果找不到会抛出异常
     */
    Object getByName(String name) throws UnknownDomainObjectException;
 
    /**
     * 根据扩展的名字获取实例,如果找不到返回null,注意与getByName的区别
     *
     * @param name扩展的名字
     * @return extension or null
     */
    @Nullable
    Object findByName(String name);
 
    /**
     * 从当前扩展容器中获取extra属性扩展,就是我们在build.gradle中定义的ext{}
     *
     * 这个扩展对应的名字是ext
     */
    ExtraPropertiesExtension getExtraProperties();
}

上面列出了ExtensionContainer中常用的API,下面我们用实例来展示具体的使用方式。

1.4.3 创建扩展

扩展的使用大概有3个步骤:

(1)定义扩展类;

(2)编译插件创建扩展;

(3)在Gradle脚本中配置扩展。

首先创建一个Android项目Chapter1_04,为了简单地运行和测试,我们直接在app/build.gradle文件中进行代码演示。

第一步:在app/build.gradle中创建ServerNode类。

class ServerNode{
    String address
    int cpuCount
 
    @Override
    public String toString() {
        return "ServerNode{" +
                "address='" + address + '\'' +
                ", cpuCount=" + cpuCount +
                '}';
    }
}

第二步:创建脚本插件并通过apply()方法使用它。

class MyExtensionTestPlugin implements Plugin<Project>{
 
    @Override
    void apply(Project target) {
        println("My Extension Test Plugin")
         //通过ExtensionContainer创建扩展实例
        ServerNode serverNode = target.getExtensions()
                                         .create("serverNode",ServerNode)
        //定义一个任务获取结果
        target.task("getResult"){
            doLast {
                println("result is: $serverNode")
            }
        }
    }
}
 
apply plugin:MyExtensionTestPlugin

上面这段代码创建了MyExtensionTestPlugin插件,然后在apply()方法中创建了一个名称是serverNode、类型是ServerNode并且不带构造函数的扩展实例,接着又定义了一个getResult任务用于获取配置的结果。

第三步:在app/build.gradle脚本里配置扩展。

serverNode{
    address = "SensorsData HeFei"
    cpuCount = 16
}

这里的serverNode需要与target.getExtensions().create("serverNode",ServerNode)中create()方法使用的名称保持一致。

第四步:运行getResult任务。

$ ./gradlew getResult
 
> Configure project :app
My Extension Test Plugin
 
> Task :app:getResult
result is: ServerNode{address='SensorsData HeFei', cpuCount=16}

可以看到,我们正确获取了配置内容中的值。

我们还可以给ServerNode添加默认值,具体有两种方式。一种是在定义成员变量的时候直接设置默认值,例如:

class ServerNode {
    String address = "SensorsData HeFei"
    int cpuCount = 16
...
}

另一种是调用create()方法的时候为其设置默认值。如果要在调用create()方法时,为其设置默认值,需要为ServerNode添加构造函数。

class ServerNode {
    String address
    int cpuCount
 
    //添加了一个构造函数
    ServerNode(String address, int cpuCount) {
        this.address = address
        this.cpuCount = cpuCount
    }
 
    @Override
    public String toString() {
        ...
    }
}

对应的create()调用方式的修改如下。

class MyExtensionTestPlugin implements Plugin<Project> {
 
    @Override
    void apply(Project target) {
        println("My Extension Test Plugin ")
        //添加构造函数
        ServerNode serverNode = target.getExtensions()
            .create("serverNode", ServerNode, "SensorsData Beijing", 36) 
        println("configure phase 's result: $serverNode")
        target.task("getResult") {
            doLast {
                println("result is: $serverNode")
            }
        }
    }
}

保持app/build.gradle中serverNode的配置不变,运行getResult任务的输出结果如下。

$ ./gradlew getResult
 
> Configure project :app
My Extension Test Plugin
configure phase 's result: ServerNode{address='SensorsData Beijing', cpuCount=36}     //输出默认值
 
> Task :app:getResult
result is: ServerNode{address='SensorsData HeFei', cpuCount=16}

通过结果可以看到输出了默认值。上述代码是对创建扩展方法<T> T create(String name, Class<T> type, Object... constructionArguments)的介绍。

1.4.4 添加和查找扩展

1.添加扩展

添加扩展有如下两种方法。

void add(String name, Object extension);
<T> void add(Class<T> publicType, String name, T extension);

对于扩展来说,create()和add()的区别是,create()是创建并添加扩展,add()是添加已有的扩展实例。

下面使用add()方法来添加ServerNode扩展。

ServerNode addServerNode = new ServerNode("SensorsData HeFei by Add", 4)
target.getExtensions().add("addServerNode", addServerNode)
target.task("getAddResult"){
    doLast {
        ServerNode tmpServerNode =
            target.getExtensions().findByName("addServerNode")
        println("add result is: $tmpServerNode")
    }
}

上面这段代码首先创建了一个ServerNode实例,然后通过add()方法添加到ExtensionContainer中,最后定义了一个任务查找添加的扩展(该任务通过ExtensionContainer提供的findByName API来查找ServerNode)。

接下来在app/build.gradle中添加addServerNode配置。

addServerNode{
    cpuCount = 8
}

最后运行getAddResult命令,输出结果如下。

add result is: ServerNode{address='SensorsData HeFei by Add', cpuCount=8}

2.查找扩展

上一个例子使用findByName()方法来查找已存在的扩展,另外还有如下几个方法可以查找扩展。

<T> T getByType(Class<T> type) throws UnknownDomainObjectException;
<T> T findByType(Class<T> type);
Object getByName(String name) throws UnknownDomainObjectException;
Object findByName(String name);

这些方法的主要区别在于它们的返回值是否可以为null或者抛出异常,在此不做详细介绍。至此我们已经了解了扩展的基本用法,完整的示例代码如下。

plugins {
    id 'com.android.application'
}
 
android {
    ...
}
 
dependencies {
    ...
}
 
class ServerNode {
    String address
    int cpuCount
 
    ServerNode(String address, int cpuCount) {
        this.address = address
        this.cpuCount = cpuCount
    }
 
    @Override
    public String toString() {
        return "ServerNode{" +
                "address='" + address + '\'' +
                ", cpuCount=" + cpuCount +
                '}';
    }
}
 
class MyExtensionTestPlugin implements Plugin<Project> {
 
    @Override
    void apply(Project target) {
        println("My Extension Test Plugin ")
 
        //示例1:创建扩展
        ServerNode serverNode = target.getExtensions()
                 .create("serverNode", ServerNode, "SensorsData Beijing", 36)
        println("1.configure phase 's result: $serverNode")
        target.task("getResult") {
            doLast {
                println("result is: $serverNode")
            }
        }
 
        //示例2:添加扩展
        ServerNode addServerNode = new ServerNode("SensorsData HeFei by Add", 4)
        target.getExtensions().add("addServerNode", addServerNode)
        target.task("getAddResult"){
            doLast {
                ServerNode tmpServerNode = 
                    target.getExtensions().findByName("addServerNode")
                println("add result is: $tmpServerNode")
            }
        }
 
    }
}
 
apply plugin: MyExtensionTestPlugin
 
serverNode {
    address = "SensorsData HeFei"
    cpuCount = 16
}
 
addServerNode{
    cpuCount = 8
}

1.4.5 扩展嵌套

什么是扩展嵌套呢?扩展嵌套就是扩展里嵌套扩展,例如下面的配置。

server {
    message = "Server Config Info"
    defaultConfig {
        address = "hefei"
        ip = "11.11.11.11"
        cpuCount = 8
    }
}

server里面嵌套了defaultConfig配置。对于这个配置,我们该如何创建对应的扩展呢?

首先创建对应的Extension Bean,代码如下。

class ServerExtension {
    String message
    ServerNode defaultConfig = new ServerNode()
 
    void defaultConfig(Action<ServerNode> action) {
        action.execute(defaultConfig)
    }
 
    @Override
    public String toString() {
        return "ServerExtension{" +
                "message='" + message + '\'' +
                ", defaultConfig=" + defaultConfig +
                '}';
    }
}
 
class ServerNode {
    String address
    String ip
    int cpuCount
 
    @Override
    public String toString() {
        return "ServerNode{" +
                "address='" + address + '\'' +
                ", ip='" + ip + '\'' +
                ", cpuCount=" + cpuCount +
                '}';
    }
}

其中,最关键的是下面这个方法的定义。

void defaultConfig(Action<ServerNode> action) {
    action.execute(defaultConfig)
}

上面的例子中的defaultConfig配置实际上就是调用了这个方法。定义好这样的Extension Bean之后,其他的操作跟前面创建扩展的操作是一样的,这里就不赘述了。

1.4.6 NamedDomainObjectContainer

1.4.5小节介绍的扩展嵌套,只是嵌套的一种方式,也是最常见的方式,其实还有一种更复杂的嵌套,即NamedDomainObjectContainer。观察android中的这段配置。

android {
   ...
 
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.    txt'), 'proguard-rules.pro'
        }
 
        debug {
            minifyEnabled true
            zipAlignEnabled true
        }
 
        preVersion {
            applicationIdSuffix ".pre"
        }
    }
}

可以看到buildTypes中定义的配置项,有我们熟悉的release和debug,除此之外还能够添加自定义的preVersion这个配置项,类似地可以添加任意多个自定义的配置项,有点像往列表(list)中添加数据一样。那这个功能是如何实现的呢?

android这个配置中的buildTypes是定义在Android Gradle插件的BaseExtension类中(注意,为方便演示代码,本书使用的是3.2.0版本,不同版本可能有所变化)的,查看其定义。

public abstract class BaseExtension implements AndroidConfig {
     
    ...
 
    private final NamedDomainObjectContainer<BuildType> buildTypes;
 
    ...
 
    /**
     * Encapsulates all build type configurations for this project.
     *
     * <p>For more information about the properties you can configure in
     * this block, see {@link
     * BuildType}.
     */
    public void buildTypes(
        Action<? super NamedDomainObjectContainer<BuildType>> action) {
        checkWritability();
        action.execute(buildTypes);
    }
    
    ...

从源码很容易看出,buildTypes的类型是NamedDomainObjectContainer<BuildType>,其中的泛型BuildType是对应的Extension Bean;对应的buildTypes(Action)方法与1.4.5小节的扩展嵌套一样,也使用Action,只不过泛型发生了变化。

当新建一个Android项目时,Android Studio会默认添加release和debug这两个配置项,实际上还可以增加其他的配置项,使配置更加丰富。因此可以根据不同场景或者需求定义不同的配置项,每个不同的配置项都会生成一个新的BuildType配置。这些都需要借助NamedDomainObjectContainer来实现。

那究竟什么是NamedDomainObjectContainer呢?

它直译过来就是命名领域对象容器,主要用途是:通过DSL创建指定类型的对象实例。

再来看一下官方对它的说明:

/**
 * <p>A named domain object container is a specialization of
 * {@link NamedDomainObjectSet} that adds the ability to create
 * instances of the element type.</p>
 *
 * <p>Implementations may use different strategies for creating
 * new object instances.</p>
 *
 * <p>Note that a container is an implementation of {@link java.util.SortedSet},
 * which means that the container is guaranteed
 * to only contain elements with unique names within this container. 
 * Furthermore, items are ordered by their name.</p>
 *
 * <p>You can create an instance of this type using the factory method
 * {@link org.gradle.api.model.ObjectFactory#domainObjectContainer(Class)}.</p>
 *
 * @param <T> The type of objects in this container.
 */
public interface NamedDomainObjectContainer<T> extends NamedDomainObjectSet<T>,
Configurable<NamedDomainObjectContainer<T>>

使用NamedDomainObjectContainer<T> 有两个限制。

指定的泛型T必须有一个public构造函数,并且必须带有一个name字符串参数;

它实现了SortedSet接口,要求所有类型对象的name属性必须是唯一的,在内部会使用name属性进行排序。

那如何创建NamedDomainObjectContainer呢?

Project类提供了如下几种方式来创建NamedDomainObjectContainer。

/**
 * 为指定的类型创建一个容器,指定的类型必须要有一个构造函数,构造函数中需要有一个name
 * 字符串参数,并且这个对象必须暴露它的name字段
 *
 * @param type这个容器包含的对象的类型
 * @return返回创建的容器
 */
<T> NamedDomainObjectContainer<T> container(Class<T> type);
 
/**
 * 为指定的类型创建一个容器,指定的类型必须要有一个构造函数,构造函数中需要有一个name
 * 字符串参数,并且这个对象必须暴露它的name字段
 *
 * @param type这个容器包含的对象的类型
 * @param factory自定义实现T的方式
 * @return返回创建的容器
 */
<T> NamedDomainObjectContainer<T> container(Class<T> type, NamedDomainObjectFactory     <T> factory);
 
/**
 * 为指定的类型创建一个容器,指定的类型必须要有一个构造函数,构造函数中需要有一个name
 * 字符串参数,并且这个对象必须暴露它的name字段。其中的闭包参数用于创建T实例
 * 这个闭包需要有一个name参数
 *
 * <p>All objects <b>MUST</b> expose their name as a bean property named "name".
 * The name must be constant for the life of the object.</p>
 *
 * @param type这个容器包含的对象的类型
 * @param factoryClosure通过闭包方式来创建实例
 * @return返回创建的容器
 */
<T> NamedDomainObjectContainer<T> container(Class<T> type, 
                                            Closure factoryClosure);

接下来通过一个具体的例子来说明NamedDomainObjectContainer如何使用。

第一步:创建一个Android项目Chapter1_05。

第二步:在app/build.gradle中添加相关的Extension Bean。

class ServerExtension {
    String message
    ServerNode defaultConfig = new ServerNode()
    NamedDomainObjectContainer<ServerNode> nodesContainer
 
    //通过构造函数将Project对象传入
    ServerExtension(Project project) {
        //调用project.container()方法创建NamedDomainObjectContainer
        nodesContainer = project.container(ServerNode) 
    }
 
    //默认节点配置
    void defaultConfig(Action<ServerNode> action) {
        action.execute(defaultConfig)
    }
 
    //其他节点配置,注意NamedDomainObjectContainer的泛型
    void otherConfig(Action<NamedDomainObjectContainer<ServerNode>> action) {
        action.execute(nodesContainer)
    }
 
    @Override
    public String toString() {
        return "ServerExtension{" +
                "message='" + message + '\'' +
                ", defaultConfig=" + defaultConfig +
                '}';
    }
}
 
class ServerNode {
    String name
    String address
    String ip
    int cpuCount
 
    ServerNode(String name) {
        this.name = name
    }
 
    @Override
    public String toString() {
        return "ServerNode{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                ", ip='" + ip + '\'' +
                ", cpuCount=" + cpuCount +
                '}';
    }
}

上面的代码中,需要关注ServerExtension(Project project)这个构造方法,这里需要将Project作为参数传入,然后调用project.container()方法创建ServerNode类型的NamedDomainObjectContainer。

第三步:创建插件,并在插件中创建扩展的实例。

class MyExtensionTestPlugin implements Plugin<Project> {
 
    @Override
    void apply(Project target) {
        println("My NamedDomainObjectContainer Test Plugin")
        //创建扩展,传入ServerExtension构造参数的值target
        ServerExtension serverNode =           
            target.getExtensions().create("server", ServerExtension, target) 
        target.task("getResult") {
            doLast {
                println("result is: $serverNode")
                 
                println("\nshow all other configs:")
                //遍历容器中所有的ServerNode配置
                serverNode.nodesContainer.all {
                    println(it)
                }
 
                println("\nshow shanghai config in other configs:")
                // 获取指定的配置
                ServerNode node = 
                    serverNode.nodesContainer.findByName("shanghai") 
                println(node)
            }
        }
 
    }
}
 
apply plugin: MyExtensionTestPlugin

上面的代码在app/build.gradle文件中创建了一个插件,并在插件中创建了对应的扩展,以及创建了一个getResult任务来获取ServerNode配置。代码中展示了两种获取ServerNode配置的方式,一种是通过all()方法获取所有的配置,一种是通过findByName()方法获取指定的配置。

第四步:添加ServerExtension配置。

server {
    message = "Server Config Info"
    defaultConfig {
        address = "hefei"
        ip = "11.11.11.11"
        cpuCount = 8
    }
 
    otherConfig {
        shanghai {
            address = "shanghai base"
            ip = "22.22.22.22"
            cpuCount = 8
        }
 
        chengdu {
            address = "chengdu base"
            ip = "122.122.122.133"
            cpuCount = 8
        }
    }
}

注意观察otherConfig中的两个配置项。

第五步:运行getResult任务。

$ ./gradlew getResult
 
> Configure project :app
My NamedDomainObjectContainer Test Plugin
 
> Task :app:getResult
result is: ServerExtension{message='Server Config Info', defaultConfig=ServerNode       {name='null', address='hefei', ip='11.11.11.11', cpuCount=8}}
 
show all other configs:
ServerNode{name='chengdu', address='chengdu base', ip='122.122.122.133', cpuCount=8}
ServerNode{name='shanghai', address='shanghai base', ip='22.22.22.22', cpuCount=8}
 
show shanghai config in other configs:
ServerNode{name='shanghai', address='shanghai base', ip='22.22.22.22', cpuCount=8}

注意观察这里使用all()遍历输出的结果,我们定义的配置项顺序是shanghai→chengdu,但是输出结果的顺序是chengdu→shanghai,这就是我们前面提到的使用NamedDomainObjectContainer有限制的原因:实现了SortedSet接口,会对name参数进行排序。

关于NamedDomainObjectContainer的完整代码如下。

plugins {
    id 'com.android.application'
}
 
android {
    ...
}
 
dependencies {
    ...
}
 
class ServerExtension {
    String message
    ServerNode defaultConfig = new ServerNode()
    NamedDomainObjectContainer<ServerNode> nodesContainer
 
    //通过构造方法将Project对象传入
    ServerExtension(Project project) {
        //调用project.container()方法创建NamedDomainObjectContainer
        nodesContainer = project.container(ServerNode) 
    }
 
    //默认节点配置
    void defaultConfig(Action<ServerNode> action) {
        action.execute(defaultConfig)
    }
 
    //其他节点配置,注意NamedDomainObjectContainer的泛型
    void otherConfig(Action<NamedDomainObjectContainer<ServerNode>> action) {
        action.execute(nodesContainer)
    }
 
    @Override
    public String toString() {
        return "ServerExtension{" +
                "message='" + message + '\'' +
                ", defaultConfig=" + defaultConfig +
                '}';
    }
}
  
 
class ServerNode {
    String name
    String address
    String ip
    int cpuCount
 
    ServerNode(String name) {
        this.name = name
    }
 
    @Override
    public String toString() {
        return "ServerNode{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                ", ip='" + ip + '\'' +
                ", cpuCount=" + cpuCount +
                '}';
    }
}
 
class MyExtensionTestPlugin implements Plugin<Project> {
 
    @Override
    void apply(Project target) {
        println("My NamedDomainObjectContainer Test Plugin")
        //创建扩展,传入ServerExtension构造参数的值target
        ServerExtension serverNode =           
            target.getExtensions().create("server", ServerExtension, target) 
        target.task("getResult") {
            doLast {
                println("result is: $serverNode")
                 
                println("\nshow all other configs:")
                //遍历容器中所有的ServerNode配置
                serverNode.nodesContainer.all {
                    println(it)
                }
 
                println("\nshow shanghai config in other configs:")
                // 获取指定的配置
                ServerNode node = 
                    serverNode.nodesContainer.findByName("shanghai") 
                println(node)
            }
        }
 
    }
}
 
apply plugin: MyExtensionTestPlugin
 
server {
    message = "Server Config Info"
    defaultConfig {
        address = "hefei"
        ip = "11.11.11.11"
        cpuCount = 8
    }
 
    otherConfig {
        shanghai {
            address = "shanghai base"
            ip = "22.22.22.22"
            cpuCount = 8
        }
 
        chengdu {
            address = "chengdu base"
            ip = "122.122.122.133"
            cpuCount = 8
        }
    }
}

上面在第三步中介绍了两种从NamedDomainObjectContainer中获取配置的方式,NamedDomainObjectContainer中还提供了如下几种常用方式,对应的方法如下,比较简单,在此不多做介绍。

//找不到返回null,不会抛出异常
T findByName(String name);
//找不到会抛出异常
T getByName(String name) throws UnknownDomainObjectException;
//遍历所有
void all(Closure action);

1.5 综合示例

前文我们介绍了Gradle的基础知识,本节用一个例子来对前面的内容做综合的总结。

1.5.1 概述

本节的这个例子比较简单,目标是简化神策Android全埋点SDK的集成步骤。关于神策Android全埋点,可以参考官方文档中的描述。

为方便阅读,下面简述神策Android全埋点SDK的集成步骤。

第一步:添加插件依赖。在rootProject/build.gradle中添加插件依赖,示例代码如下。

buildscript {
 repositories {
    mavenCentral()
 }
 dependencies {
  classpath 'com.android.tools.build:gradle:3.5.3'
  //添加神策分析android-gradle-plugin2依赖
  classpath 'com.sensorsdata.analytics.android:android-gradle-plugin2:3.3.4'
 }
}

第二步:添加SDK依赖和使用插件。在app/build.gradle文件中使用神策插件和依赖神策SDK,示例代码如下。

apply plugin: 'com.android.application'
apply plugin: 'com.sensorsdata.analytics.android' // 使用神策插件
 
dependencies {
   // 添加神策SDK库
   implementation 'com.sensorsdata.analytics.android:SensorsAnalyticsSDK:5.1.0' 
}

可以看到,这个集成步骤相对来说还是有点麻烦的,为了进一步简化这个步骤,现提出如下要求。

(1)使用插件进一步优化集成步骤;

(2)将这个插件发布到线上仓库上供别人下载使用。

1.5.2 集成步骤

第一步:创建一个新的Android项目,名称为Chapter1_06。

第二步:创建一个新的模块,类型选择“Java Library”,名称为myPlugin。在Android Studio中完整的操作路径是:File→New→New Module→Java Library→Next→Finish。

第三步:修改myPlugin/build.gradle文件,添加java-gradle-plugin插件以及maven-publish插件,并配置发布信息。修改后的内容如下。

plugins {
    id "java-gradle-plugin"
    id 'maven-publish'
}
 
java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}
 
repositories {
    google()
    mavenCentral()
    gradlePluginPortal()
}
 
dependencies {
    //添加神策分析android-gradle-plugin2依赖
    implementation "com.sensorsdata.analytics.android:android-gradle-plugin2:+"
    implementation "com.android.tools.build:gradle:4.1.2"
}
 
//组织或者公司名称
group="cn.sensorsdata.asmbook.plugin"
//版本号
version='1.0.0'
 
gradlePlugin {
    plugins {
        sensorsAutoPlugin {
            id = 'sensorsdata.autosdk'
            implementationClass = 
                'cn.sensorsdata.asmbook.myplugin.AutoAddSensorsDataSDKPlugin'
        }
    }
}
 
//./gradlew publish  发布到本地
publishing {
    publications {
        myLibrary(MavenPublication) {
            from components.java
        }
    }
 
    repositories {
        maven {
            name = 'myPluginRepo'
            url = '../repo_maven'
        }
    }
}

上述build.gradle中添加了implementation "com.sensorsdata.analytics.android:android-gradle-plugin2:+",它表示使用神策插件的最新版本。

第四步:编写Plugin<T> 的实现类。为了能够灵活地修改神策SDK版本号,我们需要创建一个扩展。这个扩展很简单,只有一个字段,代码如下。

package cn.sensorsdata.asmbook.myplugin;
 
public class SDKVersionExtension {
    String version;
 
    @Override
    public String toString() {
        return "SDKVersionExtension{" +
                "version='" + version + '\'' +
                '}';
    }
}

接着创建Plugin<T> 的实现,具体代码如下。

package cn.sensorsdata.asmbook.myplugin;
 
public class AutoAddSensorsDataSDKPlugin implements Plugin<Project> {
    //默认下载最新的版本
    String sdkVersion = "+";
 
    @Override
    public void apply(Project project) {
        System.out.println("Auto Add SensorsData AutoTrack SDK");
        //创建版本号扩展
        project.getExtensions().add("sdkVersion", SDKVersionExtension.class);//(3)
        project.afterEvaluate(project1 -> {
 
            Plugin saPlugin = project.getPlugins()
                        .findPlugin("com.sensorsdata.analytics.android");//(1)
            if (saPlugin == null) {
                //添加神策插件依赖
                project.getPluginManager()
                        .apply("com.sensorsdata.analytics.android");//(2)
                //查找扩展并获取其中的版本号设置
                Object sdkVersionExtension = 
                    project1.getExtensions().findByName("sdkVersion");//(4)
                if (sdkVersionExtension != null) {
                    SDKVersionExtension tmp = 
                        (SDKVersionExtension) sdkVersionExtension;
                    if (tmp.version != null) {
                        sdkVersion = tmp.version;
                    }
                }
                System.out.println("====final version====" + sdkVersion);
                //添加对SDK的依赖
                project.getDependencies().add("implementation", 
     "com.sensorsdata.analytics.android:SensorsAnalyticsSDK:" + sdkVersion);//(5)
            }
        });
    }
}

接下来按照代码中标注的序号,对应解释代码。

(1)因为用户可能已经集成了神策插件,为了避免重复引入,所以需要查询是否已经存在神策插件。

(2)如果没有集成神策插件,此处调用project.getPlugins().apply方法使用神策插件。

(3)创建配置版本号的扩展,注意在AutoAddSensorsDataSDKPlugin类中定义了一个sdkVersion="+"字段。假如用户没有配置扩展,就默认使用该字段的默认值来加载最新的版本。

(4)此处通过findByName()方法查找对应的扩展,如果找不到会返回null;如果找到,就将其中的version值赋给sdkVersion。

(5)在确定SDK版本号以后,调用project.getDependencies() 添加对SDK的依赖。

第五步:编译插件。运行 ./gradlew publish来打包插件,至此会在项目根目录下生成repo_maven目录,其内容如图1-12所示。

图1-12 repo_maven目录的内容

第六步:使用插件。此处我们使用plugins{} DSL来依赖插件,因此需要先在rootProject/settings.gradle文件中配置本地Maven仓库的信息,内容如下。

//plugins声明依赖关系
pluginManagement {
    plugins {
    }
    repositories {
        maven {
            url(uri("repo_maven"))
        }
        gradlePluginPortal()
        mavenCentral()
        google()
    }
}
 
include ':myPlugin'
include ':app'
rootProject.name = "Chapter1_06"

然后在app/build.gradle中添加插件和添加扩展配置,内容如下。

plugins {
    id 'com.android.application'
    id 'sensorsdata.autosdk' version '1.0.0'
}
 
android {
    ...
}
 
dependencies {
    ...
}
//配置版本号
sdkVersion{
    version = "6.0.0"
}

这里我们将SDK版本号设置为6.0.0。

第七步:运行项目,控制台输出结果如图1-13所示。

图1-13 控制台输出结果

通过图1-13可以看到插件起作用了,自动集成了神策的插件和SDK。借用这个例子对前面的内容进行总结:插件为用户提供了一个外部入口,使用户可以创建易于分发的工具,通过这个工具可以参与Gradle构建的流程中。

本节通过7个步骤创建并运行了自定义的插件,那如何将这个插件上传到公共仓库上呢?继续看1.6节的内容。

1.6 插件发布

通常,插件发布的平台有如下两个。

Gradle Plugin Portal;

Apache Maven。

1.6.1 Gradle Plugin Portal

Gradle Plugin Portal——Gradle自己的插件平台,该平台具有操作简单、支持性良好的特点,其最大的优势是可以配合plugins{} DSL来依赖插件。下面介绍如何将Chapter1_06项目上传到Gradle Plugin Portal平台。首先复制一份Chapter1_06项目,并将其副本改名为Chapter1_06_GPP(GPP:Gradle Plugin Portal)。

下面是具体的发布操作流程。

第一步:注册Gradle Plugin Portal平台账号。进入Gradle Plugin Portal登录页面,可以看到图1-14所示的页面效果。

图1-14 Gradle Plugin Portal登录页面

用户可以使用GitHub账号授权登录,也可以单击“Sign Up”注册一个账号后进行登录。

第二步:获取API Key。登录平台以后进入自己的个人账户页面,单击“API Keys”选项(见图1-15),其中的key是发布插件时需要的值。

图1-15 API Key

(1)将上述key配置在~/.gradle/gradle.properties文件中,这表示该配置是全局的,发布插件的时候会从该文件读取key。

(2)将key放在自己项目的gradle.properties文件中,例如在myPlugin中创建一个gradle.properties,如图1-16所示。

图1-16 API Key配置

(3)也可以在Gradle发布任务时以添加命令行参数的形式设置,例如:

$ ./gradlew publishPlugins -Pgradle.publish.key=<your-key> \
-Pgradle.publish.secret=<your-secret>

不管使用哪种方式都要确保key安全。

第三步:添加gradle publish插件。在myPlugin/build.gradle文件头部添加如下插件。

plugins {
    id "java-gradle-plugin"
    id 'maven-publish'
    id "com.gradle.plugin-publish" version "0.17.0"
}

其中com.gradle.plugin-publish是Gradle发布插件,顾名思义,其可用于发布插件。

第四步:插件发布配置。接着在myPlugin/build.gradle文件中添加发布插件所需要的配置。

group="cn.sensorsdata.asmbook.plugin" //(1)组织或者公司名称
version='1.0.0' //(2)版本号
 
gradlePlugin { //(3)java-gradle-plugin对应的扩展,用于生成插件描述信息和插件ID
    plugins { //(4)添加插件的方法
      sensorsAutoPlugin { //(5)插件名称
       id = 'cn.sensorsdata.autosdk'//(6)插件的唯一ID
       implementationClass = 
        'cn.sensorsdata.asmbook.myplugin.AutoAddSensorsDataSDKPlugin' //(7)实现类
      }
    }
}
 
pluginBundle {//(8) 配置发布到Gradle Plugin Portal上时的基本信息
    website = 'https://github.com/sensorsdata' //(9)网址
    vcsUrl = 'https://github.com/sensorsdata' //(10)GitHub项目的仓库地址
    description = 'SensorsData SDK' //(11)项目描述
    tags = ['sensorsdata'] //(12)项目标签
 
    plugins { //(13)
        sensorsAutoPlugin {//(14)
          // id is captured from java-gradle-plugin configuration
          description = 'SensorsData SDK Android Auto Plugin' //(15)插件描述
          version = '1.0.0' //(16)插件版本号
          tags = ['sensorsdata', 'autosdk']//(17)
          displayName = 'Plugin for SA' //(18)
        }
    }
}

下面按照标注的序号对代码进行详细介绍。

(1)group是插件发布时所需的组织或者公司名称信息,这个字段在生成包和引入插件时使用。

(2)version表示插件的版本号信息,如果插件没有设置版本号信息,则会使用这里给Project设置的版本号信息,可以对比序号16来理解。

(3)这个配置是java-gradle-plugin对应的扩展,用于配置插件的描述信息(包括implementationClass、description、displayName)和ID。

(4) plugins{}方法就是前面介绍的NamedDomainObjectContainer的实现,因此可以添加多个插件,例如序号6所示。

(5)sensorsAutoPlugin是自己定义的插件名称。

(6)ID必须是唯一的,不能重复,通常使用com.xxx.pluginName这样的命名格式。另外注意,这里与Chapter_06略有不同,这里的插件ID与group都是以cn.sensorsdata开头的,原因是Gradle Plugin Portal对第三方插件的ID命名有这方面的要求。

(7)implementationClass是当前插件的实现类,与前面介绍的在myPlugin/src/main/resources/META-INF/gradle-plugins/xxxproperties文件中添加插件的入口类类似。

(8)pluginBundle是配置上传到Gradle Plugin Portal平台上的插件信息。

(9)website是项目的网址信息。

(10)vcsUrl是项目的仓库地址。

(11)description是项目的描述信息。

(12)tags是项目的标签。

(13)因为在gradlePlugin {}中可以配置多个插件,如果想对其中的插件做单独的配置,可以在这里添加。

(14)表示对sensorsAutoPlugin做单独的配置。

(15)插件的描述信息。

(16)插件的版本号信息。

(17)插件的标签信息。

(18)插件的显示名称。

第五步:本地Maven配置。在发布插件之前,需要先在本地对其进行验证,确保其通过。利用maven-publish插件提供的发布功能,将插件发布到本地,相关配置如下。

publishing {
    repositories {
        maven {
            name = 'myPluginRepo'
            url = '../repo_maven'
        }
    }
}

然后在控制台中运行./gradlew publish命令来发布,可以在项目根目录下得到一个repo_maven目录,进行验证即可。

第六步:插件配置好并验证通过后,将其发布到Gradle Plugin Portal上,这里使用的是通过命令行添加key的方式来发布。

$ ./gradlew publishPlugins -Pgradle.publish.key=<your-key> \
-Pgradle.publish.secret=<your-secret>

发布成功以后可以在Gradle Plugin Portal的个人页面看到插件。图1-17表示插件正在审核中。

图1-17 插件审核界面

1.6.2 Maven Central简介

在介绍如何将包发布到Maven Central之前,先来了解一些关于Maven的基础知识。

1.什么是Maven

Maven是基于项目对象模型(Project Object Model,POM)的,可以通过一小段描述信息(配置)来管理项目的构建。Maven主要作为Java的项目管理工具,它不仅可以用于包管理,还有许多的插件,可以支持整个项目的开发、打包、测试、部署等一系列行为,而包管理则是其核心功能。

2.什么是仓库

Maven既然能够管理包,自然就需要存放包的地方,这样的地方称为仓库(repositories),仓库又分为本地仓库、中央仓库(maven central)和私服仓库。本地仓库,顾名思义是指存放在本地计算机中的Maven仓库。Maven会将项目中依赖的包从远端下载到本机的一个目录下管理,计算机中默认的仓库在 $user.home/.m2/repository目录下。当通过POM下载依赖包时会先从本地仓库中寻找,如果找不到就到中央仓库中寻找。中央仓库包含绝大多数流行的开源Java构件,以及源码、作者信息、源代码控制管理(Source Control Manager,SCM)信息、许可证信息等,几乎所有开源的Java项目依赖的构件都可以在这里下载到。但是中央仓库在国外,速度上可能无法保证,因此一些公司为了解决这个问题会选择搭建“私服仓库”。简单来说私服仓库会将项目中的一些依赖包下载到自己的服务器上,从而解决下载速度慢的问题,而这些私服仓库中,最著名的就是Sonatype公司的Nexus。同时Sonatype公司还提供了托管包,以及将包同步到Maven Central的服务,这个服务称为开源软件资源库托管(Open Source Software Repository Hosting,OSSRH)。

3.什么是Maven坐标

Maven上托管着众多的包,自然要求包满足一定的规则,以便于检索,这个规则称为“坐标”。Maven坐标是通过groupld、artifactld、version、packaging、classifier这些元素来定义的。

groupld:定义当前Maven项目隶属项目、组织。每一个groupld可以对应多个项目,如图1-18所示,com.android.tools这个group对应多个项目(common、annotations等)。

图1-18 groupId效果展示

artifactld:该元素定义当前实际项目中的一个模块,推荐的做法是使用实际项目名称作为artifactld值。

version:该元素定义使用构件的版本。

packaging:定义Maven项目打包的方式,打包方式通常有WAR/JAR/RAR/AAR等,默认是JAR。

classifier:该元素用来帮助定义构建输出的一些附件。例如JAR包的javadoc.jar、sources.jar等内容,这些附件也有自己的坐标。

上述5个元素中,groupld、artifactld、version是必须定义的,packaging是可选的,classifier不能直接定义,需要结合插件使用。另外OSSRH对groupld的命名有一定的要求,如果你的项目托管在开源的网站上,必须满足表1-6所示的命名规则。

表1-6 groupld命名规则样例

网站

groupId样例

GitHub

io.github.myusername

GitLab

io.gitlab.myusername

Gitee

io.gitee.myusername

Bitbucket

io.bitbucket.myusername

SourceForge

io.sourceforge.myusername

1.6.3 上传到Maven Central

接下来详细介绍如何将包上传到Maven Central。

第一步:注册。首先需要注册Sonatype的Jira账号。

第二步:提交issue。注册完以后需要在Sonatype Jira上提交一个issue,详细信息如图1-19所示。

图1-19 提交issue

第三步:等待反馈。提交完issue后会进入问题的详情页面,Sonatype的管理员会将所填信息中不合规的部分反馈给提交者,一定要注意刷新页面,通常Sonatype管理员的响应还是很及时的。下面举一个比较常见的反馈的例子。

If you do not own this domain, you may also choose a different Group Id that reflects your project hosting following this steps.

1.According to your project information, io.github.wangzhzh is valid and can be used. (com.github.* Group IDs are invalid now. The only allowed groupIds for Github projects are io.github.*)

2.Create a temporary, public repository called https://github.com/wangzhzh/OSSRH-75041 to verify github account ownership.

3.Edit this ticket and update the Group ID field with the new GroupId, and set Status to Open.

More info:https://central.sonatype.org/publish/requirements/coordinates/

上述反馈的大致意思有两点:第一点,填写的groupld不合规,因为项目是放在GitHub上托管的,要求groupld必须使用io.github.username的形式;第二点是用户需要在自己的GitHub账号中创建一个OSSRH-75041项目,目的是确保用户自己就是该账号的所有者。按照上述提示修改完后重新提交,如果顺利,会收到如下反馈信息,表示信息验证通过了。

io.github.wangzhzh has been prepared, now user(s) curious can:Publish snapshot and release artifacts to s01.oss.sonatype.org.Have a look at this section of our official guide for deployment instructions:https://central.sonatype.org/publish/publish-guide/#deployment

第四步:准备包信息。为了确保中央仓库中包的最低质量水平,确立了一些基本要求,接下来进行简单介绍。

(1)javadoc和source包。假如项目的groupld和artifactld的内容如下。

<groupId>com.example.applications</groupId>
<artifactId>example-application</artifactId>
<version>1.4.7</version>

对应的javadoc和source包的形式如下。

example-application-1.4.7-sources.jar
example-application-1.4.7-javadoc.jar

如果项目中没有javadoc和source包,需要创建一个空的JAR包,并且JAR包中需要使用README.md文件对原因进行说明。

(2)正确的Maven坐标。关于Maven坐标前面已做了简单的介绍,在此就不赘述。

(3)项目信息。操作者需要提供项目的一些基本信息,例如项目的名称、介绍、地址等,这些信息会在Maven Central中展示,方便别人了解自己的项目。下面是一个例子。

<name>Example Application</name>
<description>A application used as an example on how to set up pushing
  its components to the Central Repository.</description>
<url>http://www.example.com/example-application</url>

(4)版权信息。版权信息自然也是不可缺少的部分,对开源项目来说,最常用的有Apache License和MIT License。下面是这两种版权的使用示例。

Apache License:

<licenses>
  <license>
    <name>The Apache License, Version 2.0</name>
    <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
  </license>
</licenses>

MIT License:

<licenses>
  <license>
    <name>MIT License</name>
    <url>http://www.opensource.org/licenses/mit-license.php</url>
  </license>
</licenses>

(5)开发者信息。为了与项目关联,还需要提供开发者信息。下面是关于开发者信息的例子。

<developers>
    <developer>
      <name>wangzhzh</name>
      <email>xxx@sensorsdata.cn</email>
      <organization>SensorsData</organization>
      <organizationUrl>http://www.sensorsdata.cn</organizationUrl>
    </developer>
</developers>

(6)源码仓库信息。中央仓库中的包都是开源的,因此还需要提供源码仓库信息,方便别人能够定位到项目。根据源码托管系统的不同,这些信息也是略有不同的,如下是部分SCM对应的配置。

GitHub:

<scm>
  <connection>scm:git:git://github.com/simpligility/ossrh-demo.git</connection>
  <developerConnection>scm:git:ssh://github.com:simpligility/ossrh-demo.git         </developerConnection>
  <url>http://github.com/simpligility/ossrh-demo/tree/master</url>
</scm>

SubVersion:

<scm>
  <connection>scm:svn:http://subversion.example.com/svn/project/trunk/    </connection>
  <developerConnection>scm:svn:https://subversion.example.com/svn/project/trunk/    </developerConnection>
  <url>http://subversion.example.com/svn/project/trunk/</url>
</scm>

(7)GPG加密。为了保证数据的安全,Maven Central要求发布的内容使用GPG软件进行加密[GPG支持多种加密算法,例如RSA(RSA是3个共同发明人的姓氏首字母)、DSA(Digital Signature Algorithm,数字签名算法)等],用户可以使用提供的公钥进行解密验证。要了解GPG,首先要知道PGP(Pretty Good Privacy,良好保密协议)。1991年,程序员菲尔·齐默尔曼为了避开政府监视,开发了加密软件PGP。PGP通常用于签名、加密和解密文本、电子邮件和文件,不过PGP是一款商业软件,需要付费。于是在1997年7月,菲尔·齐默尔曼的公司PGP Inc. 向IETF(Internet Engineering Task Force,因特网工程任务组)提议制定一项名为OpenPGP的统一的标准,而GPG(GnuPG)就是OpenPGP协议的一个具体实现。

接下来我们介绍关于GPG的一些基本用法,主要内容包括:

创建密钥对;

将公钥发往公用服务器上,提供给用户校验。

首先安装GPG,可以从GnuPG网站上直接下载安装文件并安装,也可使用命令安装。以macOS为例,可以通过Homebrew安装,命令如下。

$ brew install gpg

下载完成以后,在控制台中运行如下命令查看版本信息。

$ gpg --version

输出示例如下。

gpg (GnuPG) 2.3.3
libgcrypt 1.9.4
Copyright (C) 2021 Free Software Foundation, Inc.
License GNU GPL-3.0-or-later <https://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
 
Home: /Users/wangzhzh/.gnupg
支持的算法:
公钥:RSA, ELG, DSA, ECDH, ECDSA, EDDSA
密文:IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH,
    CAMELLIA128, CAMELLIA192, CAMELLIA256
AEAD: EAX, OCB
散列:SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
压缩:  不压缩, ZIP, ZLIB, BZIP2

通过上述输出可以看到GPG支持很多种加密算法,Maven Central支持的是RSA算法,另外上述的Home路径表示GPG默认目录。

接下来运行如下命令创建密钥。

$ gpg --full-generate-key

运行上述命令会输出如下选项。

请选择您要使用的密钥类型:
   (1) RSA和RSA
   (2) DSA和Elgamal
   (3) DSA(仅用于签名)
   (4) RSA(仅用于签名)
   (9) ECC(签名和加密) *默认*
   (10) ECC(仅用于签名)
   (14)卡中现有密钥
您的选择是?1

这里选择RSA加密,后续步骤按照提示操作即可。在输入密码时,记住自己输入的密码,后续会用到。按照提示的步骤操作完成后,会得到类似如下的输出。

pub   rsa3072 2021-11-19 [SC]
      FADC0000236CA74AAA8C101ABBE33FDCCBA14508
uid                      wangzhzh (For MavenCentral) <curious.a@qq.com>
sub   rsa3072 2021-11-19 [E]

其中:rsa3072表示算法是RSA且长度是3072;FADC0000236CA74AAA8C101ABBE33FDCCBA14508为公钥,其后8位CBA14508是公钥的短写形式。使用如下命令可以显示所有的公钥。

$ gpg --list-keys

生成密钥以后就可以对文件进行加密。

$ gpg -ab Main.java

上述命令就是对文件进行加密,加密后会生成Main.java.asc文件。为了方便其他人验证加密结果,还需要将公钥放在服务器上。有3个服务器可以存放公钥,分别如下。

keyserver.ubuntu.com;

keys.openpgp.org;

pgp.mit.edu。

使用如下命令将公钥信息发到服务器上。

$ gpg --keyserver keys.openpgp.org --send-keys FADC0000236CA74AAA8C101ABBE33FDCC     BA14508

发送到服务器之后,其他用户可以使用如下命令接收公钥,进而对加密文件进行校验:

$ gpg --keyserver keys.openpgp.org --recv-keys FADC0000236CA74AAA8C101ABBE33FDCC     BA14508

通常情况下,在开发中很可能有不同的成员需要使用同一个密钥来加密文件,这个时候就需要导出密钥。下面是导出密钥的方式。

$ gpg --export-secret-keys > ~/.gnupg/secring.gpg

运行上述命令,就会将对应公钥的密钥导出到secring.gpg文件中,这个文件后面需要用到。

第五步:配置Gradle。第四步介绍了在Maven Central上发布包需要的基本信息,第五步根据前面的准备知识介绍如何使用Gradle配置这些信息。首先复制Chapter1_06_GPP项目,将其副本改名为Chapter1_06_Maven,然后修改myPlugin/build.gradle文件。修改后的结果如下。

plugins {
    id "java-gradle-plugin"
    id 'maven-publish'
    id 'signing'//(1)
}
 
java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}
 
repositories {
    google()
    mavenCentral()
    gradlePluginPortal()
}
 
dependencies {
    //添加神策分析android-gradle-plugin2依赖
    implementation "com.sensorsdata.analytics.android:android-gradle-plugin2:+"
    implementation "com.android.tools.build:gradle:4.1.2"
}
 
//组织或者公司名称
group="io.github.gvczhang.asmbook.plugin"
//版本号
version='1.0.0-SNAPSHOT'
 
java { //(2)
    withJavadocJar()
    withSourcesJar()
}
 
javadoc {//(3)
    if(JavaVersion.current().isJava9Compatible()) {
        options.addBooleanOption('html5', true)
    }
}
 
//./gradlew publish命令发布到本地
publishing {
 
    publications {
        myLibrary(MavenPublication) {//(4)
            //meta info
            groupId = "io.github.gvczhang.asmbook.plugin"
            artifactId = 'sensorsdata.autosdk'
            version = '1.0.0-SNAPSHOT'
 
            from components.java
 
            pom {//(5)
                name = 'SensorsDataAutoPlugin'
                description = 'This is a plugin, that provide a easy way to integrate 
    SensorsData\'s SDK and plugin.'
                url = 'https://github.com/GvcZhang/SensorsDataAutoPlugin'
                licenses {
                    license {
                        name = 'The Apache License, Version 2.0'
                        url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
                developers {
                    developer {
                        id = 'curious'
                        name = 'ZhangWei'
                        email = 'curious.a@qq.com'
                    }
                }
                scm {
                    connection = 'scm:git:https://github.com/GvcZhang/SensorsDataAutoPlugin.
    git'
                    developerConnection = 'scm:git:ssh://github.com:GvcZhang/    SensorsDataAutoPlugin.git'
                    url = 'https://github.com/GvcZhang/SensorsDataAutoPlugin'
                }
            }
        }
    }
 
    repositories {
 
        mavenCentral {//(6)
            name = 'OSSRH'
            //publish to local
            // url = '../repo_maven'
             
 
            //(7)
            //public to sonatype
            //url = "https://s01.oss.sonatype.org/content/repositories/    snapshots" //SNAPSHOT版本
            url = 'https://s01.oss.sonatype.org/service/local/staging/deploy/    maven2/' //发布到Maven中
            credentials { //配置OSSRH服务的账户密码信息
                username = findProperty("ossrhUsername")
                password = findProperty("ossrhPassword")
            }
        }
    }
}
 
signing {//(8)
    sign publishing.publications.myLibrary
}

说明如下。

位置(1)引入signing插件,该插件会根据配置的GPG私钥自动对包内容生成加密文件。

位置(2)和(3)用于配置javadoc和source包内容。

位置(4)和(5)定义Maven Central需要的基本信息。

位置(6)和(7)定义仓库的位置信息,其中位置(7)定义Sonatype的基本配置;如果想发布到本地,修改URL即可,不过发布到本地不需要credentials。

位置(8)是加密配置,这里表示myLibrary加密。

在正式发布之前,还需要添加OSSRH的账户密码以及GPG的密钥信息,我们选择在myPlugin/gradle.properties中配置,也可以选择在运行任务的时候设置属性,gradle.properties中的内容类似如下。

ossrhUsername=Sonatype #JIRA账号
ossrhPassword=Sonatype #JIRA密码
 
#GPG信息
signing.keyId=CBA14508 #公钥短写
signing.password=test1111 #密码
signing.secretKeyRingFile=../secring.gpg #导出的私钥文件

配置好以上信息后,运行如下代码即可完成发布。

$ ./gradlew publishMyLibraryPublicationToOSSRHRepository

注意,发布到Maven Central上的信息不支持修改和删除,通常需要先发布SNAPSHOT版本,验证通过后再同步到Maven Central上。

1.7 插件调试

1.7.1 输出日志

输出日志是比较原始的一种查看日志的方式。例如在Java文件中,我们可以使用System.out.println来输出日志,在Groovy或Kotlin中,我们可以使用println来输出日志。Gradle中也提供了输出日志的方式,我们可以通过Project.getLogger()获取Logger对象来输出日志。Gradle中的Logger使用的是slf4j。使用Logger的好处是,我们可以控制日志输出的级别。Gradle的日志级别是定义在LogLevel类中的。

package org.gradle.api.logging;
  
/**
 * The log levels supported by Gradle.
 */
public enum LogLevel {
    DEBUG,
    INFO,
    LIFECYCLE,
    WARN,
    QUIET,
    ERROR
}

从枚举的定义可以看出,Gradle日志级别总共分为6种,如表1-7所示(级别由低到高依次排列)。

表1-7 Gradle日志级别

级别

调用方法

备注

DEBUG

project.logger.debug(message)

调试信息

INFO

project.logger.info(message)

内容信息

LIFECYCLE

project.logger.lifecycle(message)

进度信息

WARN

project.logger.warn(message)

警告信息

QUIET

project.logger.quiet(message)

重要信息

ERROR

project.logger.error(message)

错误信息

默认的级别是LIFECYCLE,即默认会输出LIFECYCLE及其之上级别的日志信息。那如何控制日志的输出级别呢?可以通过在gradle命令后面加上相应的参数来控制。

# 默认输出DEBUG及其之上级别的日志信息,即默认输出所有日志信息
./gradlew -d build

控制日志级别的参数如表1-8所示。

表1-8 控制日志级别的参数

参数

说明

无参数

LIFECYCLE及其之上级别的日志信息

-d或 --debug

DEBUG及其之上级别的日志信息

-i或 --info

INOF及其之上级别的日志信息

-w或 --warn

WARN及其之上级别的日志信息

-q或 --quiet

QUIET及其之上级别的日志信息

1.7.2 断点调试

Gradle插件是在代码的编译器中运行的,所以调试Android应用程序(简称应用,缩写为App或app)的方法不再适用于Android Gradle。下面我们介绍通过断点的方式调试Gradle插件。

第一步:添加断点。

这一步比较简单,单击代码行左侧部分即可添加/删除断点,如图1-20所示。

图1-20 添加断点

第二步:配置Run/Debug Configurations。

(1)打开Edit Configurations,操作方式如图1-21所示。

图1-21 打开Edit Configurations

(2)建立远程调试任务。

单击Run/Debug Configurations左上角的加号,然后选择Remote,如图1-22所示。

图1-22 选择Remote

(3)然后不需要做任何修改,直接单击OK按钮即可,如图1-23所示。

图1-23 单击OK按钮

第三步:执行构建。

在终端执行如下命令,开始构建。

./gradlew <任务名> -Dorg.gradle.daemon=false -Dorg.gradle.debug=true

注意,操作时,需要把 <任务名> 替换成实际执行的任务。

-Dorg.gradle.daemon=false:表示不使用守护进程,默认是开启的,也可换成 --no-daemon选项。

-Dorg.gradle.debug=true:开始gradle进程启动后需要等待调试器连接上才能开始运行。

命令执行之后,可以看到在Terminal中整个执行被阻塞了,并输出图1-24所示的信息。

图1-24 debug阻塞展示

第四步:启动Debugger Attach。

选择在第二步新建的Remote,即Unnamed,然后单击debug按钮,操作如图1-25所示。

图1-25 选择Unnamed并单击debug按钮

第五步:当编译执行到断点处就会停下来,如图1-26所示。

图1-26 debug进入断点

1.8 小结

本章介绍了Gradle基础知识和Gradle插件开发中涉及的知识点,读者可能会有疑问:为什么要花很多的篇幅来介绍Gradle的基础知识?这是因为Gradle的这些知识对后面理解Android插件的运作原理非常有帮助,而且整个Android应用的构建都依赖Gradle,所以这些知识对每一个Android开发者来说都是需要掌握的。

相关图书

Web应用安全
Web应用安全
龙芯嵌入式系统原理与应用开发
龙芯嵌入式系统原理与应用开发
沉浸式剖析OpenHarmony源代码:基于LTS 3.0版本
沉浸式剖析OpenHarmony源代码:基于LTS 3.0版本
统信UOS系统管理教程
统信UOS系统管理教程
深入解析Windows操作系统(卷2)  (英文版·第7版)
深入解析Windows操作系统(卷2) (英文版·第7版)
统信UOS操作系统基础与应用教程
统信UOS操作系统基础与应用教程

相关文章

相关课程