Android应用的构建过程是一个复杂的过程,涉及到很多工具。首先所有的资源文件都会被编译,并且在一个R文件中引用,然后Java代码被编译,通过dex工具转换成dalvik字节码。最后这些文件都会被打包成一个APK文件,此应用被最终安装到设备之前,APK会被一个debug或者release的key文件签名。
一句话定义Gradle
Gradle是一种构建工具,其构建基于Groovy(DSL) ------ 一种基于JVM的动态语言,用于申明构建和创建任务,让依赖管理更简单。
年少时第一次对Gradle总结的微博:Gradle 与 Android的三生三世:是我构建了你,你必将依赖于我
【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'
可以传递的代码区块。
java不能对方法进行引用
void buildscript(Closure configureClosure);
如上所示,buildscript
内的代码区块将传递到buildscript
中,稍后执行。
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() { //很java的感觉
@Override
void execute(Project project) {
repositories {
google()
jcenter()
}
}
})
【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
}
}
......
}
减少传递依赖带来的重复编译应该是implementation 诞生的最大意义了,在往常开发Coder都是一个compile 依赖走天下,单module下的表现不明显,但目前公司项目大部分采用多module项目,例如
主App -----依赖----> 业务module -----依赖----> 工具module
以上三种是很常见的多module分配,这时使用implementation依赖是可以大大减少重复编译的,因为业务module会依赖 工具module,但主App中无需对工具module使用传递依赖。因此,修改工具module内容时,不会导致主App重新编译。
./gradlew taskName
task的使用在平时开发过程中也是不可或缺的一部分,特别是用于编写各种插件,例如静态check、打包等需求支持,下面了解一下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
看到这里真的是非常有意思,在第一点也说了gradle把基于Java虚拟机的语言改造成最基本的配置语法,所以其内部原理实现Java Coder可谓是一目了然,在编写task clean(type: Delete)
,可以直接理解为class clean extends Delete
,这就是个继承嘛。回归到问题本身,可见delete操作内部实则是个添加操作,内部维护着一个Set,在执行 ./gradlew
命令时,只是在配置任务,等到直接执行clean任务时./gradlew clean
时,才会把Set集中的删除任务取出,do it。
以上解释也带出了task的2个重要阶段:
在上一个例子的基础上加深,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个阶段已经分辨清楚。
在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 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() 和普通代码段的区别
doLast()
、doFirst()
代码不会被执⾏;
doFirst()
和 doLast()
都是 task 代码,其中 doFirst()
是往队列的前⾯插入代码,doLast()
是往队列的后面插入代码。拓展 ------ task 的依赖
可以使用 task taskA(dependsOn: b)
的形式来指定依赖。指定依赖后,task 会在⾃己执行前先执⾏依赖的 task。
**[Initialization] 初始化阶段:**执行 settings.gradle,确定主 project 和子 project ;
根据项⽬结构来确定项目组成,如下:
单 project:确定根目录下的 build.gradle 文件即可;
多 project:由配置了多个module的 settings.gradle 文件开始查找 settings 的顺序:
当前⽬录
兄弟⽬录 master
父目录
**[Configuration] 配置阶段:**执行每个 project 的 bulid.gradle,确定出所有 task 所组成的 有向⽆环图;
[Execution] 执行阶段:按照上⼀阶段所确定出的有向无环图来执⾏指定的 task;
afterEvaluate {
插⼊入代码
}
Gradle Plugin到底是什么?
**本质就是将一些独立逻辑的代码封装并抽取出来,加以复用。**但不同于module、library,它所处理的逻辑并非业务性质,而是作为一个项目组织者,更关心各module的配置信息,因此提供了一系列配置、task执行相关API。
一个Plugin的写法:
/app/build.gradle
配置文件buildSrc
目录每个 field,Groovy 会⾃自动创建它的 getter
和 setter
方法,从外部可以直接调用,并且在使用 object.fieldA
来获取值或者使用 object.fieldA = newValue
来赋值的时候,实际上会自动转⽽调⽤ object.getFieldA()
和 object.setFieldA(newValue)
。 (跟Kotlin一样)
单引号是不带转义的,⽽双引号内的内容可以使⽤ "string1${var}string2"
的⽅式来转义。(跟Vue一样)
这一部分配置相当自由,从 “配置类名” 到 “属性”都是自主定义,后续从plugin中获取,就像
/app/build.gradle
中对android编译版本各种配置,以下举个例子:
permissionsCheckList {
//明确暂禁用的权限列表
forbiddenPermissions = ['android.permission.GET_ACCOUNTS',
'android.permission.READ_CONTACTS']
}
/app/build.gradle
实现【注意:此部分需要写到 apply 引入之前】
要不怎么说Groovy是基于Java虚拟机而制定的DSL,写法部分不同,但是直接写implements实现,“like class”理解。
如下代码,这里实现一个只有print功能的PermissionCheck插件,
void apply(Project target)
方法.target.extensions.create
可以获取到项目配置的Extension信息,根据配置信息实例化创建 XXXExtension类。get/set
方法获取具体属性信息,做自定义题配置逻辑处理。
class PermissionsCheckListExtension {
def forbiddenPermissions = []
}
class PermissionCheck implements Plugin {
@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权限。
当然真正实践到项目中,并不会像第一种写法一股脑写在 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 目录重点总结
build.gradle
配置会自动被执行,即使不配置到settings.gradle
settings.gradle
,它是⼀个独立的存在。apply plugin: 'xxx'
的⽅式来便捷应⽤这些 pluginsettings.gradle
中如果配置了了 ':buildSrc'
,buildSrc ⽬录就会被当做是⼦ Project , 因此会被执⾏两遍。所以在settings.gradle
⾥面应该删掉 ':buildSrc'
的配置Android 提供的一个⼯具,在项⽬构建过程中,可以将编译后的⽂件(jar 文件和 class 文件) 添加自定义中间处理过程。
注意: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'
}
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)
}
}
}
上述代码逻辑简单一览:
为了更好地理解从入口获取的这些class、jar文件集合,print文件路径观察log输出结果,输入./gradlew assembleDebug
打包。
jarInputs 集合路径如上,这里只截图了一部分,观察这些jar文件路径不难发现,都是项目编译所依赖的库,且存于 /.gradle/caches/*
缓存文件夹中。(便于各个项目共用这些依赖库)
directoryInputs集合路径如上,都存于项目名称/app/build/*
即本地project编译后的build目录下,而且进一步点进classes目录下,都是R文件。
其实这都是属于各种依赖库的文件,只是AS编译完成项目后,属于此项目project的class文件。
如下图可见,这是我们自定义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》
此部分提供的例子CustomTestTransform 只是模版化地将编译完的内容原封不不动搬运到⽬目标位置, 无实际作用,在日常开发中,通常是结合javassist工具(面向切面编程),来修改字节码。
其实在了解Transform提供的功能后,其落地业务场景皆由此为基础拓展,以下介绍几个常见场景。
方法耗时统计
通过一个自定义Transform过滤每一个class/jar文件,将所有方法摘出来,插桩计时代码。
方法、API搜索
黑名单方法搜索,例如Android系统升级,个别API失效。