Gradle再回首之重点归纳
Posted 鸽一门
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Gradle再回首之重点归纳相关的知识,希望对你有一定的参考价值。
回顾
android应用的构建过程是一个复杂的过程,涉及到很多工具。首先所有的资源文件都会被编译,并且在一个R文件中引用,然后Java代码被编译,通过dex工具转换成dalvik字节码。最后这些文件都会被打包成一个APK文件,此应用被最终安装到设备之前,APK会被一个debug或者release的key文件签名。
一句话定义Gradle
Gradle是一种构建工具,其构建基于Groovy(DSL) ------ 一种基于JVM的动态语言,用于申明构建和创建任务,让依赖管理更简单。
年少时第一次对Gradle总结的微博:Gradle 与 Android的三生三世:是我构建了你,你必将依赖于我
Point
1.闭包和动态配置
【project/ build.gradle文件】
buildscript
ext.kotlin_version = '1.3.41'
repositories
google()
jcenter()
dependencies
classpath 'com.android.tools.build:gradle:3.5.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
allprojects
repositories
google()
jcenter()
【app/ build.gradle文件】
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
- buildscript: 针对于下方dependencies区块中的依赖路径,即插件的仓库配置。
- allprojects: 针对所有project(app、module);
(1)闭包Closure
可以传递的代码区块。
java不能对方法进行引用
void buildscript(Closure configureClosure);
如上所示,buildscript
内的代码区块将传递到buildscript
中,稍后执行。
(2)动态配置
gradle中语法配置,是在runtime而不是build时段check,不同于编码Java,例如在调用某个对象不存在的方法,编译时就会报错,gradle中方法是动态配置的。
buildscript
......
dependencies
classpath 'com.android.tools.build:gradle:3.5.0'
//add语法,等价于上面的写法
add('classpath', 'com.android.tools.build:gradle:3.5.0');
......
来看上述build.gradle
文件中对plugin路径语法配置 的一个例子,很少人知道路径的配置还可以用 add(,)
这种语法,就像调用Java对象方法,传入2个参数,更像是一个万能钥匙,第一个参数是配置key,第二个参数是配置value。
没错,你的确可以这样理解,在上述第一点闭包中讲到,将区块传入void dependencies(Closure configureClosure)
中 稍后执行,再快捷键点击classpath 具体发现是DependencyHandler 接口,此接口具体实现类是DefaultDependencyHandler,类中有个方法叫做MethodMissing(String name, Object args)
,内部遍历配置清单中是否有name方法,找到则内部继续调用create(...)
方法。
可见,Gradle是利用Groovy的特性,把基于Java虚拟机的语言改造成最基本的配置语法。因此,这里建议了解gradle配置规则即可,感兴趣者再去了解其中实现。
拓展
allprojects
repositories
google()
jcenter()
//上下写法等价----------------------
allprojects(new Action<Project>() //很java的感觉
@Override
void execute(Project project)
repositories
google()
jcenter()
)
2.buildType 和 productFlavors
【app/ build.gradle文件】
android
......
buildTypes
release
signingConfig signingConfigs.myConfigs
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable false
debug
signingConfig signingConfigs.myConfigs
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
minifyEnabled false
debuggable true
......
3.compile、 implementation 和 api
- implementation:不会传递依赖;
- compile / api:会传递依赖;api 是 compile 的替代品,效果完全等同。
- 当依赖被传递时,⼆级依赖的改动会导致 0 级项⽬重新编译;
- 当依赖不不传递时,二级依赖的改动 不会导致 0 级项⽬重新编译;
减少传递依赖带来的重复编译应该是implementation 诞生的最大意义了,在往常开发Coder都是一个compile 依赖走天下,单module下的表现不明显,但目前公司项目大部分采用多module项目,例如
主App -----依赖----> 业务module -----依赖----> 工具module
- 主App:一些基本APP信息配置、签名、动态化处理;
- 业务module:业务逻辑/UI处理;
- 工具module:网络请求、自定义控件、工具等;
以上三种是很常见的多module分配,这时使用implementation依赖是可以大大减少重复编译的,因为业务module会依赖 工具module,但主App中无需对工具module使用传递依赖。因此,修改工具module内容时,不会导致主App重新编译。
4.task
./gradlew taskName
task的使用在平时开发过程中也是不可或缺的一部分,特别是用于编写各种插件,例如静态check、打包等需求支持,下面了解一下task重点。
Test1. clean task
首先看个简单的例子,也是project/build.gradle
文件中一个现成的task ------ clean,我们在此基础上加几个Log对比查看下:
println("outside the task: println")
task clean(type: Delete)
println("inside the task: before task")
delete rootProject.buildDir
println("inside the task: after task")
分别在终端terminal输入:
./gradlew
:打印log(如上截图),build文件夹没有删除;./gradlew clean
:打印log(如上截图),build文件夹删除;
为何2个命令都输出了Log,但./gradlew
执行后,文件夹并没有被删除?
在第一点中我们讲解到gradle原理一大特点 ------ 闭包,将代码块传入方法中,内部有自己的处理逻辑。两个命令,Log都打印了,意味着所有语句都执行过了。可./gradlew
命令,delete语句似乎没有起作用?突破口就在这里,点击delete进去,查看源码实现:
package org.gradle.api.tasks;
public class Delete extends ConventionTask implements DeleteSpec
private Set<Object> delete = new LinkedHashSet<Object>();
......
/**
* Sets the files to be deleted by this task.
*
* @param target Any type of object accepted by @link org.gradle.api.Project#files(Object...)
*/
public void setDelete(Object target)
delete.clear();
this.delete.add(target);
......
看到这里真的是非常有意思,在第一点也说了gradle把基于Java虚拟机的语言改造成最基本的配置语法,所以其内部原理实现Java Coder可谓是一目了然,在编写task clean(type: Delete)
,可以直接理解为class clean extends Delete
,这就是个继承嘛。回归到问题本身,可见delete操作内部实则是个添加操作,内部维护着一个Set,在执行 ./gradlew
命令时,只是在配置任务,等到直接执行clean任务时./gradlew clean
时,才会把Set集中的删除任务取出,do it。
以上解释也带出了task的2个重要阶段:
- configuration配置阶段
- execution执行阶段
Test2. task 配置与执行
在上一个例子的基础上加深,task代码块内部新增一个doLast
闭包,输入命令对比结果:
println("outside the task: println")
task clean(type: Delete)
println("inside the task: before task")
delete rootProject.buildDir
println("inside the task: after task")
doLast
println("inside the task: doLast")
分别在终端terminal输入:
./gradlew
:打印log,但是并没有打印出doLast
闭包内的Log,build文件夹没有删除;./gradlew clean
:打印log(如上截图),build文件夹删除;
在上一个Test的基础上,我们得知configuration配置阶段 会将所有配置读取一遍,配备好对应的task,直接执行task时才会真正do it 。而此次试验的doLast
闭包正突出 execution执行阶段 的特点:doLast
里的内容在 task 执⾏过程中才会被执行。
./gradlew
命令还是配置阶段,因此最后输出并没有打印出doLast
闭包中的内容;执行./gradlew clean
直接执行task任务时,才会去执行doLast
闭包中的内容,打印出 > inside the task: doLast
。至此,相信task的2个阶段已经分辨清楚。
Test3. doFirst 和 doLast
在Test2的基础上继续加深,既然在上一点中介绍了doLast
,相应地,doFirst
虽迟但到。上一点中我们点明doFirst
执行在execution阶段,那么doFirst
亦然,这2者的区别似乎通过名字也可了解一二。
下面通过一个更有趣的例子来了解其区别:
task clean(type: Delete)
doFirst
println("inside the task: doFirst")
delete rootProject.buildDir
doLast
println("inside the task: doLast")
clean.doFirst
println("outside the task: doFirst")
clean.doLast
println("outside the task: doLast")
由于这两个区块只在task execution阶段 执行,因此此次试验输入 ./gradlew clean
即可,查看Log输出:
输出结果表明(执行阶段):
- 后面的
doFirst
中的Log输出 先于 前面的输出; - 后面的
doLast
中的Log输出 后于 前面的输出;
首先说明下后续新增的clean.dofirst
这种写法,简直就是Java中调用类的方法,其实在【Test1. clean task】中已经提过:
在编写
task clean(type: Delete)
,可以直接理解为class clean extends Delete
,这就是个继承嘛。
因此,后续新增的这种写法也是没有问题的,重点还是放到doFirst()
、 doLast()
的调用顺序上来,老规矩,查看这2个闭包方法的内部源码实现:
package org.gradle.api.internal;
public abstract class AbstractTask implements TaskInternal, DynamicObjectAware
......
private List<InputChangesAwareTaskAction> actions;
@Override
public Task doFirst(final Closure action)
...
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)
...
taskMutator.mutate("Task.doLast(Closure)", new Runnable()
public void run()
getTaskActions().add(convertClosureToAction(action, "doLast action"));
);
return this;
......
一目了然,一个task中维护了一个Action集合List,而 doFirst
方法每次调用都会向列表头部插入Action,而 doLast
方法每次调用都会向列表尾部插入Action。因此在此次实验中,后面的doFirst
中的Log输出 先于 前面的输出,后面的doLast
中的Log输出 后于 前面的输出。
总结
至此,对于以上三个小实验,做一个简单的总结:
一个标准的task结构
task taskName
初始化代码
doFirst
task 代码
doLast
task 代码
doFirst() 、doLast() 和普通代码段的区别
- **普通代码段:**在 task 创建过程中就会被执行,发生在 configuration阶段;
- **doFirst() 和 doLast():**在 task 执⾏过程中被执行,发生在 execution阶段。如果用户没有 直接或间接 执行 task,那么它的
doLast()
、doFirst()
代码不会被执⾏;doFirst()
和doLast()
都是 task 代码,其中doFirst()
是往队列的前⾯插入代码,doLast()
是往队列的后面插入代码。
拓展 ------ task 的依赖
可以使用 task taskA(dependsOn: b)
的形式来指定依赖。指定依赖后,task 会在⾃己执行前先执⾏依赖的 task。
5.gradle 执⾏的⽣命周期
(1)三个阶段
-
**[Initialization] 初始化阶段:**执行 settings.gradle,确定主 project 和子 project ;
根据项⽬结构来确定项目组成,如下:
-
单 project:确定根目录下的 build.gradle 文件即可;
-
多 project:由配置了多个module的 settings.gradle 文件开始查找 settings 的顺序:
-
当前⽬录
-
兄弟⽬录 master
-
父目录
-
-
-
**[Configuration] 配置阶段:**执行每个 project 的 bulid.gradle,确定出所有 task 所组成的 有向⽆环图;
-
[Execution] 执行阶段:按照上⼀阶段所确定出的有向无环图来执⾏指定的 task;
(2)阶段之间插入代码
- ⼀二阶段之间:settings.gradle 的最后;
- 二三阶段之间:
afterEvaluate
插⼊入代码
Plugin实践
Gradle Plugin到底是什么?
**本质就是将一些独立逻辑的代码封装并抽取出来,加以复用。**但不同于module、library,它所处理的逻辑并非业务性质,而是作为一个项目组织者,更关心各module的配置信息,因此提供了一系列配置、task执行相关API。
一个Plugin的写法:
- 直接写到
/app/build.gradle
配置文件 - 独立封装到项目中的
buildSrc
目录 - 独立一个项目上传到仓库,项目直接引入即可
0. Groovy语法基础要求
- getter / setter
每个 field,Groovy 会⾃自动创建它的 getter
和 setter
方法,从外部可以直接调用,并且在使用 object.fieldA
来获取值或者使用 object.fieldA = newValue
来赋值的时候,实际上会自动转⽽调⽤ object.getFieldA()
和 object.setFieldA(newValue)
。 (跟Kotlin一样)
- 字符中的单双引号
单引号是不带转义的,⽽双引号内的内容可以使⽤ "string1$varstring2"
的⽅式来转义。(跟Vue一样)
1. 配置信息Extension
这一部分配置相当自由,从 “配置类名” 到 “属性”都是自主定义,后续从plugin中获取,就像
/app/build.gradle
中对android编译版本各种配置,以下举个例子:
permissionsCheckList
//明确暂禁用的权限列表
forbiddenPermissions = ['android.permission.GET_ACCOUNTS',
'android.permission.READ_CONTACTS']
2. Plugin实现
(1)直接在/app/build.gradle
实现
【注意:此部分需要写到 apply 引入之前】
要不怎么说Groovy是基于Java虚拟机而制定的DSL,写法部分不同,但是直接写implements实现,“like class”理解。
如下代码,这里实现一个只有print功能的PermissionCheck插件,
- 实现Plugin接口,内部实现
void apply(Project target)
方法. - 可以通过参数target的
target.extensions.create
可以获取到项目配置的Extension信息,根据配置信息实例化创建 XXXExtension类。 - 因此也需要构建相关的XXXExtension类,注意需要定义到Plugin前。
- 通过类的
get/set
方法获取具体属性信息,做自定义题配置逻辑处理。- 逻辑处理几个重点:执行顺序。
class PermissionsCheckListExtension
def forbiddenPermissions = []
class PermissionCheck implements Plugin<Project>
@Override
void apply(Project target)
println 'PermissionCheck apply'
def extension = target.extensions.create('permissionsCheckList', PermissionsCheckListExtension)
println "PermissionCheck (forbiddenPermissions): $extension.forbiddenPermissions"
target.afterEvaluate
println "PermissionCheck afterEvaluate (forbiddenPermissions): $extension.forbiddenPermissions!"
apply plugin: PermissionCheck
permissionsCheckList
//明确暂禁用的权限列表
forbiddenPermissions = ['android.permission.GET_ACCOUNTS',
'android.permission.READ_CONTACTS']
以上,执行gradle配置命令./gradlew
,输出见下图:
输出发现执行到apply plugin: PermissionCheck
时,配置gradle阶段,注意此时还没有读取到项目中permissionsCheckList的配置信息,因此此时输出的forbiddenPermissions为[],也是初始化定义时的值。
而调用 target.afterEvaluate方法内传入闭包内容稍后执行,Point中grad le生命周期有提到,afterEvaluate执行时期是在「配置阶段」之后和「执行阶段」之前,也就是说所有配置结束后,最后一个“配置”来执行闭包里的内容,所以此时自定义配置已经可以获取到了 。而后输出的forbiddenPermissions列表也就是我们后续配置的GET_ACCOUNTS、READ_CONTACTS权限。
(2)project中封装实现到buildSrc 目录下
当然真正实践到项目中,并不会像第一种写法一股脑写在 build.gradle
文件中,即冗杂又缺失服用性,下面介绍第二张实现方式。
创建一个Java Library,修改 /src
里的目录如下图所示,具体如何实现,网上教程太多,这里重点强调几个点,为什么要创建这样的目录。
目录结构
main文件夹下:
-
groovy
文件夹替代初始java
文件及,并创建包名目录,新增Plugin类。 -
创建资源文件
resources/META-INF/gradle-plugins
,其下的*.properties
中的*
代指插件名称,即最终引入插件语句:apply plugin: '*'
。最后,在*.properties
文件中只需要进行一个配置:Plugin的路径地址,具体格式如下,
implementation-class=com.hencoder.plugin_demo.DemoPlugin
下面先做一个小测试,在buildSrc 目录下的build.gradle
配置文件中新增一个print输出,输入./gradlew
命令查看输出结果:
有趣的是buildSrc 目录下的配置文件中的输出语句被执行了两次,为何?之前讲到 gradle生命周期的三个阶段,难道是配置阶段被执行了两次?
并非如此,只是因为buildSrc 目录下的配置内容被执行了两次!buildSrc**,是一个默认的目录,gradle在执行的时候首要Top1优先级就是读取此文件夹下配置。因此如果setting.gradle
中还有buildSrc文件夹的配置信息,,buildSrc中配置内容则会被执行两次。(注:在创建buildSrc 目录时,IDE会自动将此library名称添加到项目根目录下的setting.gradle
配置文件中)
综上,将根目录下的setting.gradle
配置文件中的 :buildSrc
删除,则输出就只有一句了。
buildSrc 目录重点总结
- 这是 gradle 中的⼀个特殊⽬录,此⽬录下的
build.gradle
配置会自动被执行,即使不配置到settings.gradle
- buildSrc 的执⾏早于任何⼀个 project,也早于
settings.gradle
,它是⼀个独立的存在。 - buildSrc 所配置出来的 Plugin 会被 自动添加到编译过程中的每⼀个 project 的 classpath, 因此它们才可以直接使用
apply plugin: 'xxx'
的⽅式来便捷应⽤这些 plugin settings.gradle
中如果配置了了':buildSrc'
,buildSrc ⽬录就会被当做是⼦ Project , 因此会被执⾏两遍。所以在settings.gradle
⾥面应该删掉':buildSrc'
的配置
(3)单独抽成项目发布
3. Transform工具
(1)定义
Android 提供的一个⼯具,在项⽬构建过程中,可以将编译后的⽂件(jar 文件和 class 文件) 添加自定义中间处理过程。
(2)添加依赖
注意:Transform是Android提供的一个工具类,在com.android.build
包下,但是按理说其他module或者library添加时,也不需要特殊考虑build包的依赖,因为在项目本目录下的build.gradle
配置文件中已有 allProject的仓库地址统一配置:
【根目录/build.gradle】
allprojects
repositories
google()
jcenter()
但是在上一点也强调过,buildSrc 的执⾏早于任何⼀个 project,也早于settings.gradle
,它是⼀个独立的存在。因此 settings.gradle
中仓库的配置无效,需要额外在buildSrc目录下的 build.gradle
配置文件中添加库依赖:
// 因为 buildSrc 早于任何一个 project 执⾏行,因此需要⾃己添加仓库
repositories
google()
jcenter()
dependencies
implementation 'com.android.tools.build:gradle:3.1.4'
(3)类方法使用
import com.android.build.api.transform.Transform
......
class CustomTestTransform extends Transform
@Override
String getName()
return "CustomTestTransform"
@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
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException
super.transform(transformInvocation)
Transform实现方法介绍一览
getName()
:对应的task名称。后续打包的时候,会根据task名称生成对应的任务。getInputTypes()
:指定转换结果类型,例如字节码或者资源文件or elsegetScopes()
:指定适用转换范围,例如整个project或者or else。transform(...)
: 自定义转换逻辑。(重点方法,下面细讲)
transform转换方法内部机制
如上演示代码,最基本构建一个CustomTestTransform类,且void transform(TransformInvocation transformInvocation)
方法中空实现,而后将其注册到Plugin。此时安装运行程序会直接报错,如下图:
如上图可见程序安装失败,为何?
寻常思路思考:注册自定义转换类,从父类继承的transform
方法即使空实现(父类也应该会做基础流程过渡的吧),也不应该影响程序正常运行呀。
但其实父类Transform的transform
方法就是空实现!因此这里Android运行逻辑不是说把处理完的结果交给你自定义Transform去加工,而是类似于一种上下游机制,上游将结果传递给自定义Transform,下游在等着数据接收。因此如果自定义子类Transform中的transform
方法是空实现,会使得流程滞留,程序异常。
综上,transform
方法可以先不顾自定义特殊逻辑的实现,但必须需要做的一点是 将从上游接受的数据结果(处理 or 未处理)返回给下游, 即入口接收数据再输送给出口。以下的模版型代码,无任何特殊自定义逻辑,仅做传输作用:
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException
def inputs = transformInvocation.inputs
def outputProvider = transformInvocation.outputProvider
inputs.each
//jarInputs: 各个依赖编译的jar文件
it.jarInputs.each
File dest = outputProvider.getContentLocation(it.name, it.contentTypes, it.scopes, Format.JAR)
println "jarInputs: $it.file"
FileUtils.copyFile(it.file, dest)
//directoryInputs: 本地project编译成的多个class文件
it.directoryInputs.each
File dest = outputProvider.getContentLocation(it.name, it.contentTypes, it.scopes, Format.DIRECTORY)
println "directoryInputs: $it.file"
FileUtils.copyDirectory(it.file, dest)
上述代码逻辑简单一览:
- 从参数transformInvocation 分别获取入口、出口
- 遍历入口数据
- 获取jarInputs ------ 编译依赖的jar文件集合,再遍历,copy输出到出口;
- 获取directoryInputs ------ 本地project编译后的class文件集合,再遍历,copy输出到出口;
为了更好地理解从入口获取的这些class、jar文件集合,print文件路径观察log输出结果,输入./gradlew assembleDebug
打包。
- jarInputs集合路径
jarInputs 集合路径如上,这里只截图了一部分,观察这些jar文件路径不难发现,都是项目编译所依赖的库,且存于 /.gradle/caches/*
缓存文件夹中。(便于各个项目共用这些依赖库)
- directoryInputs集合路径
directoryInputs集合路径如上,都存于项目名称/app/build/*
即本地project编译后的build目录下,而且进一步点进classes目录下,都是R文件。
其实这都是属于各种依赖库的文件,只是AS编译完成项目后,属于此项目project的class文件。
- 自定义Transform路径
如下图可见,这是我们自定义Transform ------ CustomTestTransform的路径: 项目名称/app/build/intermediates/transforms/CustomTestTransform
,而且此目录下的文件就是自定义Transform转换后的jar、class文件。(class文件在图二)
你可以做一个小测试,将build文件夹删除,再把CustomTestTransform 的transform
方法恢复空实现,也就是我们之前解释过的导致dex文件打包失败的**「上下流机制」**,运行./gradlew assembleDebug
:
此时程序安装运行是失败的,见上图CustomTestTransform目录下的文件是空的,没有jar/class文件了,这也是程序为何安装失败的原因:根据「上下流机制」,自定义Transform没有将入口文件运输(处理 or 未处理)到出口,而Android Plugin会将该目录下的所有jar、class文件打包进一个dex文件,因此如果此目录下没有文件,打包后的Dex是一个空壳,届时安装肯定出错。
祭出打包过程神图如下,来源于《Gradle For Android》
(4)Transform落地业务场景
此部分提供的例子CustomTestTransform 只是模版化地将编译完的内容原封不不动搬运到⽬目标位置, 无实际作用,在日常开发中,通常是结合javassist工具(面向切面编程),来修改字节码。
其实在了解Transform提供的功能后,其落地业务场景皆由此为基础拓展,以下介绍几个常见场景。
-
方法耗时统计
通过一个自定义Transform过滤每一个class/jar文件,将所有方法摘出来,插桩计时代码。
-
方法、API搜索
黑名单方法搜索,例如Android系统升级,个别API失效。
以上是关于Gradle再回首之重点归纳的主要内容,如果未能解决你的问题,请参考以下文章