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() {  //很java的感觉
    @Override
    void execute(Project project) {
        repositories {
            google()
            jcenter()

        }
    }
})

2.buildTypeproductFlavors

【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")
}

Gradle再回首之重点归纳_第1张图片

分别在终端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 delete = new LinkedHashSet();
  	......
    /**
     * 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")
    }
}
Gradle再回首之重点归纳_第2张图片

分别在终端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输出:

Gradle再回首之重点归纳_第3张图片

输出结果表明(执行阶段):

  • 后面的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() 和普通代码段的区别

  • **普通代码段:**在 task 创建过程中就会被执行,发生在 configuration阶段
  • **doFirst() 和 doLast():**在 task 执⾏过程中被执行,发生在 execution阶段。如果用户没有 直接或间接 执行 task,那么它的 doLast()doFirst() 代码不会被执⾏;
    • doFirst()doLast() 都是 task 代码,其中 doFirst() 是往队列的前⾯插入代码,doLast() 是往队列的后面插入代码。

拓展 ------ task 的依赖

可以使用 task taskA(dependsOn: b)的形式来指定依赖。指定依赖后,task 会在⾃己执行前先执⾏依赖的 task。


5.gradle 执⾏的⽣命周期

(1)三个阶段

  1. **[Initialization] 初始化阶段:**执行 settings.gradle,确定主 project 和子 project ;

    根据项⽬结构来确定项目组成,如下:

    • 单 project:确定根目录下的 build.gradle 文件即可;

    • 多 project:由配置了多个module的 settings.gradle 文件开始查找 settings 的顺序:

      1. 当前⽬录

      2. 兄弟⽬录 master

      3. 父目录

  2. **[Configuration] 配置阶段:**执行每个 project 的 bulid.gradle,确定出所有 task 所组成的 有向⽆环图

  3. [Execution] 执行阶段:按照上⼀阶段所确定出的有向无环图来执⾏指定的 task;

(2)阶段之间插入代码

  1. ⼀二阶段之间:settings.gradle 的最后;
  2. 二三阶段之间:
afterEvaluate { 
  	插⼊入代码
}



Plugin实践

Gradle Plugin到底是什么?

**本质就是将一些独立逻辑的代码封装并抽取出来,加以复用。**但不同于module、library,它所处理的逻辑并非业务性质,而是作为一个项目组织者,更关心各module的配置信息,因此提供了一系列配置、task执行相关API。

一个Plugin的写法:

  • 直接写到/app/build.gradle 配置文件
  • 独立封装到项目中的buildSrc 目录
  • 独立一个项目上传到仓库,项目直接引入即可

0. Groovy语法基础要求

  • getter / setter

每个 field,Groovy 会⾃自动创建它的 gettersetter 方法,从外部可以直接调用,并且在使用 object.fieldA 来获取值或者使用 object.fieldA = newValue 来赋值的时候,实际上会自动转⽽调⽤ object.getFieldA()object.setFieldA(newValue)。 (跟Kotlin一样)

  • 字符中的单双引号

单引号是不带转义的,⽽双引号内的内容可以使⽤ "string1${var}string2"的⽅式来转义。(跟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插件,

  1. 实现Plugin接口,内部实现void apply(Project target) 方法.
  2. 可以通过参数target的target.extensions.create可以获取到项目配置的Extension信息,根据配置信息实例化创建 XXXExtension类。
  3. 因此也需要构建相关的XXXExtension类,注意需要定义到Plugin前。
  4. 通过类的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权限。

(2)project中封装实现到buildSrc 目录下

当然真正实践到项目中,并不会像第一种写法一股脑写在 build.gradle 文件中,即冗杂又缺失服用性,下面介绍第二张实现方式。

创建一个Java Library,修改 /src 里的目录如下图所示,具体如何实现,网上教程太多,这里重点强调几个点,为什么要创建这样的目录。

目录结构

Gradle再回首之重点归纳_第4张图片

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 命令查看输出结果:

Gradle再回首之重点归纳_第5张图片

有趣的是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 else
  • getScopes():指定适用转换范围,例如整个project或者or else。
  • transform(...): 自定义转换逻辑。(重点方法,下面细讲)

transform转换方法内部机制

如上演示代码,最基本构建一个CustomTestTransform类,且void transform(TransformInvocation transformInvocation) 方法中空实现,而后将其注册到Plugin。此时安装运行程序会直接报错,如下图:

如上图可见程序安装失败,为何?

寻常思路思考:注册自定义转换类,从父类继承的transform方法即使空实现(父类也应该会做基础流程过渡的吧),也不应该影响程序正常运行呀。

但其实父类Transformtransform方法就是空实现!因此这里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)
            }
        }
    }

上述代码逻辑简单一览:

  1. 从参数transformInvocation 分别获取入口、出口
  2. 遍历入口数据
    • 获取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文件。

Gradle再回首之重点归纳_第6张图片

其实这都是属于各种依赖库的文件,只是AS编译完成项目后,属于此项目project的class文件

  • 自定义Transform路径

如下图可见,这是我们自定义Transform ------ CustomTestTransform的路径: 项目名称/app/build/intermediates/transforms/CustomTestTransform ,而且此目录下的文件就是自定义Transform转换后的jar、class文件。(class文件在图二)

Gradle再回首之重点归纳_第7张图片

Gradle再回首之重点归纳_第8张图片

你可以做一个小测试,将build文件夹删除,再把CustomTestTransformtransform 方法恢复空实现,也就是我们之前解释过的导致dex文件打包失败的**「上下流机制」**,运行./gradlew assembleDebug

此时程序安装运行是失败的,见上图CustomTestTransform目录下的文件是空的,没有jar/class文件了,这也是程序为何安装失败的原因:根据「上下流机制」,自定义Transform没有将入口文件运输(处理 or 未处理)到出口,而Android Plugin会将该目录下的所有jar、class文件打包进一个dex文件,因此如果此目录下没有文件,打包后的Dex是一个空壳,届时安装肯定出错。

祭出打包过程神图如下,来源于《Gradle For Android》

Gradle再回首之重点归纳_第9张图片

(4)Transform落地业务场景

此部分提供的例子CustomTestTransform 只是模版化地将编译完的内容原封不不动搬运到⽬目标位置, 无实际作用,在日常开发中,通常是结合javassist工具(面向切面编程),来修改字节码

其实在了解Transform提供的功能后,其落地业务场景皆由此为基础拓展,以下介绍几个常见场景。

  • 方法耗时统计

    通过一个自定义Transform过滤每一个class/jar文件,将所有方法摘出来,插桩计时代码。

  • 方法、API搜索

    黑名单方法搜索,例如Android系统升级,个别API失效。

你可能感兴趣的:(Android,学习笔记)