一、前言
上篇我们讲到了patch包的打包是通过dex命令来生成classes.dex之类的dex文件,但是实际项目开发中我们不可能每次都把对应的包、类一一拷贝出来然后自己手动去敲dx命令去打包,所我们的目的是编写任务和插件去自动打patch包,在这个过程中我们首先需要学习的是Gradle Task和Plugin的定制
二、Groovy语法
我们平时使用的Android studio开发就是基于Gradle来构建的,而Gradle是基于Grovvy语言的构建工具,它使用DSL语法,(Domain Specified Language 是领域专用语言,通俗的说叫行话,能达到共识的一种语言)将开发过程中需要的编译、构建、测试、打包以及部署工作,变得非常简单方便和重复使用,所以在学习gradle之前,咱们先大概了解下groovy的基本语法
在Groovy中编写Java代码也是可以的,因为Groovy是一门JVM语言,最终也会被编译成JVM字节码,交给虚拟机去执行
1.变量声明
访问修饰符
def:相当于局部变量
ext:相当于全局变量
声明变量
//groovy是一种规则非常松的语言,语句后面不用写分号
//而且类型也可以不用写
int x = 1
def x = 1
2.字符串
def s1 = 'the x is $x' => 单引号,输出会原样得到 the x is $x
def s2 = "the x is $x" => 双引号,输出的时候会对$符号进行转义 the x is 1
def s3 = '''
this is the first line
this is the second line
this is the third line
'''
=> 三个引号'''xxx'''中的字符串支持随意换行,对$符不能进行转义
3.范围
范围是指值序列的速记,范围由序列中的第一个和最后一个表示,下面是范围写法的例子
- 1..10 =>[1,10]
- 1..<10 =>[1,10)
- ‘a’..’x’ =>[a,x]
- 10..1 =>[10,1]
def test(){
for(i in 1..5){
println i
}
}
4.集合和映射
集合,相当于Java中List对象
//创建空列表
def list = []
//增加一个元素
list.add("Gradle")
//左移添加,返回该列表
list << "Groovy"
//增加多个元素
list.addAll(["Groovy","Gradle"])
//删除一个元素
list.remove("Groovy")
映射,相当于Java中的Map对象
//创建空映射
def map = [:]
//增加值
map = ['name':'shanshan','age':18]
map.put('lastName','xue')
//遍历映射元素
map.each{
println "$it.key : $it.value"
}
map.eachWithIndex{ it, i ->
println "$i : $it"
}
//判断映射是否包含某键
assert map.containsKey('name')
5.函数和闭包
函数,也可以写成和Java中一样的方法,不过groovy可以写的更简单些
//形参类型可以不写
String doSomething(arg1, arg2){
println "method doSomething(arg1, arg2)"
}
//不指定返回类型,必须加def 最后一句不用写return 默认返回最后一句
def doSomething(){
println "method doSomething"
}
闭包(Closure),是一种数据结构,属于一段代码,使用{}包括
//无参数
def closure1 = {
println "hello world"
}
closure1() //执行闭包,输出 hello world
--------------------------------------------------------------
//接收一个参数
def closure2 = {
String str -> println str //箭头前面是参数定义,后面是执行代码,str为外部传入的参数
}
如果只有一个参数可以用it代替,也可以写作:
def closure2 = {
println it
}
closure2("hello world") //执行闭包,输出 hello world
--------------------------------------------------------------
//接收多个参数
def closure3 = {
String str, int n -> println "$str : $n"
}
//也可以省去参数类型,写作:
def closure3 = {
str, n -> println "$str : $n"
}
closure3("hello world",1) //执行闭包,输出 hello world : 1
--------------------------------------------------------------
//使用变量
def var = "hello world"
def closure4 = {
println var
}
closure4() //执行闭包 输出 hello world
--------------------------------------------------------------
//改变上下文
def closure5{
println name //这时name并不存在
}
MyClass m = new MyClass()
closure5.setDelegate(m)
class MyClass{
def name = "hello world"
}
closuere5() //执行闭包,输出 hello world
闭包的优势
比如我们在写一个方法的时候,中间的某项操作可能需要调用者来实现,那么我们一般情况下是通过接口来实现,如果有了闭包,就可以直接使用闭包来实现,比如
interface Callback{
void doCall(int a);
}
def doSomething(Callback callback){
println "start"
callback.doCall(10) //需要通过接口来实现
println "over"
}
//使用闭包
def doSomething(Closure c){
println "start"
c(10) //直接通过闭包来实现,相当于传入了一段代码,也可以指定参数
println "over"
}
//调用doSomething方法,省去了()
doSomething{ int a->
println "a is ${a}"
}
6.类和对象
Groovy和其他面向对象语言一样,也存在类和对象的概念,和java中类的定义是一样的。
跟java的区别就在于Groovy会为每个字段生成getter和setter,我们可以访问字段本身访问setter和getter,比如
class MyClass{
private String name
}
def myClass = new MyClass()
myClass.name = "this is name" //因为groovy动态的为name创建了setter和getter,所以可以直接访问
println myClass.name
7.delegate机制
class Child{
private String name
}
class Parent{
private String name //parent也有一个name属性
Child child = new Child();
void config(Closure c){
c.delegate = clild
c.setResolveStrategy Closure.DELEGATE_FIRST //默认情况是OWNER_FIRST,即它会先查找闭包的owner(这里指parent)
c()
}
}
def parent = new Parent()
parent.config{
name = "this is name"
}
println parent.name //打印结果为null
println parent.child.name //打印结果为this is name
三、Gradle 工程和任务的认识
Gradle是一个框架,它负责定义流程和规则
每一个待编译的工程都叫一个Project
每一个Project在构建的时候都包含一系列Task
当我们新建一个项目,在没有编写任何gradle代码之前,我们可以在Terminal终端输入gradle tasks
命令(需要配置环境变量)查看当前工程中可使用的任务有哪些
在android studio中,我们有Gradle Wrapper,它为Window、OSX、Linus平台都提供了相应的脚本文件,这些脚本使你可在不同平台的不同Gradle版本上正确执行编译打包脚本,不需要配置环境变量,这时候需要看所有任务需要执行的命令是./gradlew tasks
从图中可以看到,Android studio自动为我们添加了一些默认的任务,通过Gradle这些任务我们可以很方便的编译打包工程,而那些任务又是通过添加插件轻松实现的,都是通过build.gradle中apply plugin: 'com.android.application'
这句话来实现的
一、工程和任务的关系
在Gradle的世界中最重要的两个概念就是:工程(Project)和任务(Task),
每一个Gradle项目都会包含一个和多个工程,一个工程可以是一个Module,也可以是一个第三方库
每一个工程又由一个或多个任务组成,一个任务代表了一个工作的最小单元,它可以是一次类的编译,打一个包等,Project为Task提供了执行上下文
二、工程属性
Project properties是Gradle专门为Project定义的属性,对于一个project来说,它有很多默认属性,比如:
- project: Project本身
- name: Project的名字
- path: Project的绝对路径
- description: Project的描述信息
- buildDir: Project构建结果存放目录
- version: Project的版本号
除了默认属性,我们也可以给project添加自定义属性,添加方法为
ext{
versionCode = 1
versionName = 1.0.0
}
使用方法
defaultConfig{
versionCode project.property("versionCode")
versionName project.property("versionName")
}
另外有一个小tip:就是我们可以把一些不想让别人看到的配置放在gradle.properties这个文件里面,比如
USER_NAME = 'user_name'
PASS_WORD = 'password'
//在build.gradle里面可以直接使用 "${USER_NAME}" 拿到
除了在build.gradle中定义属性,我们也可以通过命令 -P
完成定义和赋值,这种方式具有最高优先级
./gradlew -PversionCode=2 -PversionName=2.0.0
另外一种方式,我们可以通过JVM系统参数定义Property 使用命令-D
(需要以“org.gradle.project”为前缀)
./gradlew -Dorg.gradle.project.versionCode=3
-Dorg.gradle.project.versionName=3.0.0
最后一种方式,通过环境变量设置Property (需要以“ORG_ GRADLE_ PROJECT_ ”为前缀)
export ORG_ GRADLE_ PROJECT_versionCode=4
export ORG_ GRADLE_ PROJECT_versionName=4.0.0
三、任务的定义和使用
首先我们定义一个很简单的任务hello,有几种方式
//第一种
task hello{
doLast{
println 'hello World!'
}
}
//上面的任务还可以使用以下写法,不过gradle 5.0已经过时了,可以作为了解
task hello << { //语法糖,相当于doLast
println 'hello World!'
}
//第二种 以字符串形式定义的
task('hello'){
doLast{
println 'hello World!'
}
}
//第三种 以字符串类型定义的 并定义类型
task ('copy', type: Copy){
from (file('srcDir')
into (buildDir)
}
//或者不使用字符串定义 定义类型
task copy(type: Copy){
from (file('srcDir')
into (buildDir)
}
//第四种 使用create创建
tasks.create(name: 'hello'){
doLast{
println 'hello World!'
}
}
//使用create创建并指定类型
tasks.create(name: 'hello',type: Copy){
doLast{
from (file('srcDir')
into (buildDir)
}
}
上面的任务可以在终端中输入 ./gradlew hello
就能输出 hello World! ,这样就很简单的定义了一个任务
task有两个生命周期,配置阶段 和 执行阶段
在配置阶段,Gradle将读取所有的build.gradle文件的所有内容来配置Project和Task等,比如设置Project和Task的Property,处理Task之间的依赖关系等等
task Task1{
def name = "hello" //这段代码是在配置阶段执行的,配置阶段也就是说在task没有执行的时候这段代码就会被扫描到,如果访问了访问不到的字段,就会直接报错
doFirst{ //这段代码在task执行阶段执行,执行阶段是指通过./gradle Task1执行的时候这段代码才会真正执行
println "doFirst $name"
}
doLast{ //这段代码在task执行阶段执行
println "doLast $name"
}
}
四、自定义Task
在Gradle中,我们有三种方法可以自定义Task
- 在build.gradle文件中直接定义
class HelloWorldTask extends DefaultTask{
@Optional //标识可选
String message = "default message"
@TaskAction //标识Task要执行的动作
def hello(){
println $message
}
}
//使用方式
task hello(type: HelloWorldTask)
task hello2(type: HelloWorldTasl){
messge = "this is a new message" //给task参数赋值
}
//对于给参数赋值有几种方式,上面在定义task的时候赋值是一种,还有
方法二
hello2{
message = "xxx" //Gradle会为每个task创建一个同名的方法,该方法接受一个闭包
}
或
hello2.message="xxx" //Gradle会为每个task创建一个同名的property,所以可以将Task当做property来使用
方法三 通过Task的configure()方法完成Property的设置
hello2.configure{
message = "xxx"
}
在当前工程的buildSrc目录下定义
在第一种方式中,在build.gradle中直接定义Task,这样Task的定义和使用混合在一起,在需要定义Task不多时,可以采用这种方法,但是在项目中存在大量自定义Task时,这种方法就非常不合适了,一种改进方法是在另外的一个gradle文件中定义Task,然后使用apply from:到build.gradle中。这里我们使用另一种方法,在
buildSrc
目录下定义,Gradle执行时,会自动查找该目录下所定义的Task类型,并首先编译该目录下的groovy代码以供build.gradle文件使用。具体做法是在当前工程buildSrc/src/main/groovy/com/huli 目录下创建HelloWorldTask.groovy文件,将Task的定义拷贝过来
package com.huli //需要加上包名
class HelloWorldTask extends DefaultTask{
@Optional //标识可选
String message = "default message"
@TaskAction //标识Task要执行的动作
def hello(){
println $message
}
}
//使用方式区别在于 引用Task时,需要指定全名称
task hello(type: com.huli.HelloWorldTask)
- 在单独的项目中定义Task
虽然第二种方式Task的定义和build.gradle分离开了,但是它依然只能应用在当前工程中,如果我们希望所定义的Task能够用在另外的项目中,那么第二种方式就不可行了,此时我们需要将Task的定义放在单独的工程中,然后在要使用该Task的工程中通过声明依赖的方式引入这些Task
这种方式跟后面的自定义Plugin的第三种方式类似,这里不详细介绍,可以看后面。
五、任务的依赖关系
我们可以定义一个任务依赖于某个其他的任务
task build{
doLast{
println "i am build task"
}
}
task release(dependsOn: build){ //通过dependsOn来指定依赖于某个任务
doLast{
println "i am release task"
}
}
//如果上面两个任务是本来就有的而不是自己新写的,那么可以通过下面的方式来指定依赖
release.dependsOn build
在终端运行 ./gradlew release
将会得到两行结果
i am build task
i am release task
六、对现有任务添加动作行为
比如要对上面定义的hello添加动作行为
//第一种方法
//在doFirst中添加
hello.doFirst{
println "hello doFirst"
}
//在doLast中添加
hello.doLast{
println "hello doLast1"
}
//第二种方法
hello{
doLast{
println "hello doLast2"
}
}
在终端执行得到结果为:
hello doFirst
hello World!
hello doLast1
hello doLast2
七、给任务设置属性并访问
task myTask{
//ext是固定写法
ext.myProperty = "myValue"
//可设置其他属性
...
}
task printTaskProperties{
doLast{
println myTask.myProperty
}
}
八、增量式构建
如果我们将Gradle的Task看作一个黑盒子,那么我们便可以抽象出输入和输出的概念,一个Task对输入进行操作,然后产生输出。比如,在使用java插件编译源代码时,即输入为Java源文件,输出则为class文件。如果多次执行一个Task的输入和输出是一样的,那么我们便可以认为这样的Task是没有必要重复执行的。此时,反复执行相同的Task是冗余的,并且是耗时的。
为了解决这样的问题,Gradle引入增量式构建的概念。在增量式构建中,我们为每一个Task定义输入(inputs)和输出(outputs),如果在执行一个Task时,他的输入和输出与前一次执行时没有发生变化,那么Gradle便会认为该Task是最新的(UP-TO-DATE),因此Gradle将不予执行。一个Task的inputs和outputs可以是一个或多个文件,可以是文件夹,还可以是Project的某个Property,甚至可以是某个闭包所定义的条件。
每个Task都拥有inuts和outputs属性,他们的类型分别为TaskInputs和TaskOutputs,在下面的例子中,我们展示了这么一种场景:名为combineFileContent的Task从sourceDir目录中读取所有的文件,然后将每个文件的内容合并到destination.txt中。让我们先来看看没有定义Task输入和输出的情况:
task combineFileContentNonIncremental {
def sources = fileTree('sourceDir')
def destination = file('destination.txt')
doLast {
destination.withPrintWriter { writer ->
sources.each {source ->
writer.println source.text
}
}
}
}
多次执行./gradlew combineFileContentNonIncremental
时,整个Task都会反复执行,即便在第一次执行已经得到了所需的结果,如果该task是一个非常繁重的task,那么多次重复执行势必造成没必要的时间浪费
这时,我们可以将sources声明为该Task的inputs,而将destination声明为outputs,重新创建task如下:
task combineFileContentIncremental {
def sources = fileTree('sourceDir')
def destination = file('destination.txt')
//相比之下,只多了这两行代码
inputs.dir sources
outputs.file destination
doLast {
destination.withPrintWriter { writer ->
sources.each {source ->
writer.println source.text
}
}
}
}
当首次执行combineFileContentIncremental时,Gradle会完整的执行该Task,但是紧接着再执行一次,命令就会显示
:combineFileContentIncremental UP-TO-DATE
BUILD SUCCESSFUL
Total time: 2.104 secs
可以看到,combineFileContentIncremental被标记为UP-TO-DATE,表示该Task是最新的,Gradle将不予执行,在实际应用中,你将遇到很多这样的情况,因为Gradle的很多插件都引入了增量式的构建机制
如果我们修改了inputs(即sourceDir文件夹)中的任何一个文件或删掉了destination.txt,当调用该任务时,Gradle又会重新执行,因为此时的Task已经不再是最新的了
九、Gradle目录结构分析(番外话题,作为了解)
在默认情况下,Gradle采用了与Maven相同的Java项目目录结构(src/main/java src/test/java之类的),在采用Maven目录结构的同时,还融入了自己的一些概念,即source set,Gradle为我们创建了2个source set,一个名为main,一个名为test
请注意,这里的source set的名字main与目录结构中的main文件夹并无必然的联系,只是在默认情况下,Gradle为了source set概念到文件系统目录结构映射方便,才采用了相同的名字,对于test也是如此。我们完全可以在build.gradle文件中重新配置这些source set所对应的目录结构,同时,我们还可以创建新的source set
从本质上讲,Gradle的每个source set都包含有一个名字,并包含有一个名为java和另一个名为resources的Property,他们分别用于表示该source set所包含的java源文件集合和资源文件集合,在实际应用时,我们可以将他们设置成任何目录值,比如
sourceSets {
main {
java {
srcDir 'java-sources'
}
resources {
srcDir 'resources'
}
}
}
另外我们还可以创建新的sourceSet,比如:
sourceSet{
api
}
默认情况下,该api对应的Java源文件目录被Gradle设置为src/api/java,而资源文件目录则被设置成了src/api/resources,我们也可以像上面的main一样重新对api的目录结构进行配置
Gradle默认会自动为每一个创建的source set创建相应的task,创建规律为:对于名为A的sourceSet,Gradle将为其创建compile\Java、process\Resources和\Classes这3个Task,对于上面的api而言,Gradle会为其创建compileApiJava、processApiResources和apiClasses Task
你可能会注意到,对于main而言,Gradle并没有相应的compileMainJava,原因在于:由于main是Gradle默认创建的sourceSet,并且又是极其重要的,便省略掉了其中的Main,而是直接使用了compileJava作为main的编译Task,对于test来说,Gradle依然采用了compileTestJava
通常情况是,我们自己创建的名为api的source set会被其他source set所依赖,比如main中的类需要实现api中的某个接口等,此时,我们需要做两件事,第一,我们需要在编译main之前对api进行编译,即编译main中的Java源文件的Task应该依赖于api中的Task
Classes.dependsOn apiClasses
第二,在编译main时,我们需要将api编译生成的class文件放在main的classpath下
sourceSets{
main{
compileClasspath = compileClassPath + files(api.output.classesDir)
}
test{
runtimeClasspath = runtimeClasspath + files(api.output.classesDir)
}
}
四、项目构建过程中任务分析
对于我们一个Android项目来说,在你点了Android Studio上面的运行按钮的时候,等编译完成项目就能安装到手机上去了,但你知道这中间发生什么事情了吗?下面我们就需要分析这个过程大概都做了什么
我们主module app的build.gradle一般都是以 apply plugin: 'com.android.application'
开始的,这是一个Android studio自带的插件,该插件内部包含许多的Android相关的task,我们一般主要使用的是assemble
、clean
、build
,而其中assemble就是我们用于打包apk所用的task,而这个task的依赖关系特别复杂,下面这个图表示了assembleDebug
的依赖关系
看上面的图着实很让人难以看懂其中的顺序,所以下面给出了相应task的执行顺序
:app:preBuild
:app:preDebugBuild
:app:checkDebugManifest
:app:prepareDebugDependencies
:app:compileDebugAidl
:app:compileDebugRenderscript
:app:generateDebugBuildConfig //generated/source文件夹下生成buildConfig文件夹,根据不同的flavor和buildType生成对应文件夹
:app:generateDebugAssets
:app:mergeDebugAssets
:app:generateDebugResValues //gennerated/res文件夹下,生成resValues文件夹,根据不同的flavor和buildType生成对应文件夹
:app:generateDebugResources
:app:mergeDebugResources
:app:processDebugManifest
:app:processDebugResources
:app:generateDebugSources //在gennerated文件夹下生成对应的R.java文件
:app:compileDebugJavaWithJavac //开始使用javac将.java编译成.class文件 intermediates下生成classes文件夹,所以我们代码注入一般放在这个步骤之后,因为代码注入是基于.class文件来做的
:app:compileDebugNdk
:app:compileDebugSources
:app:transformClassesWithDexForDebug
:app:mergeDebugJniLibFolders
:app:transformNative_libsWithMergeJniLibsForDebug
:app:processDebugJavaRes
:app:transformResourcesWithMergeJavaResForDebug
:app:validateDebugSigning
:app:packageDebug
:app:zipalignDebug
:app:assembleDebug
以上task都是属于app的module中的,若有多个module,gradle会为每个module执行一次该task链
有两个Task我们需要留意:
:app:compileDebugJavaWithJavac ————>编译.java文件到.class
:app: transformClassesWithDexForDebug ————>将.class文件打成dex包
根据以上依赖关系,如果我们有比如打patch包,或者代码注入等的需求(这些需求都需要在.java文件编译成.class文件后、打包成dex之前进行操作),就可以定义task然后进行依赖的插入,或者使用doFirst
、doLast
等相关操作来实现打包过程中,插入自己的业务需求
五、自定义Plugin
在Plugin中,我们可以向Project中加入新的Task,定义configurations和property等。在Gradle中创建自定义插件,有三种方式(跟Task定义的三种方式类似):
在build.gradle脚本中直接使用
在buildSrc中使用
buildSrc是Gradle提供的在项目中配置自定义插件的默认目录在独立Module中使用
每一个自定义的Plugin都需要实现Plugin接口,上面三种方式只是plugin放置的地方不一样(放置位置跟上面task的一样),其实Plugin的定义是一样的,我们重点讲第三种,下面是步骤分析
创建AndroidLibrary 取名为hotfix-patch,删除除build.gradle之外的其他文件
修改插件的build.gradle
apply plugin: 'groovy' //使用groovy插件
apply plugin: 'maven' //要上传maven仓库,所以也要用maven插件
repositories {
mavenCentral()
google()
jcenter()
}
dependencies {
//gradle sdk
compile gradleApi()
//groovy sdk
compile localGroovy()
}
uploadArchives {
repositories {
mavenDeployer {
//本地maven地址
repository(url: uri('../repo-hotfix-patch'))
pom.groupId = 'com.huli.plugin'
pom.artifactId = "hotfix-patch" //插件名
pom.version = '1.0.0'
}
}
}
我们gradle的包管理都是存储在maven仓库中的,我们这里定义一个本地的maven仓库,我们代码完成后需要先打包代码到本地的maven仓库,然后在主项目中依赖我们打好的包
按照以下目录创建项目
此处有坑,注意下面的META-INF和gradle-plugins是父子目录关系,不是一个目录名称,Android Studio可能会给你优化到一个文件夹,导致你插件无法apply,请分开成父子目录写
首先src目录下创建groovy和resources目录
- groovy包用来存储gradle代码
- resources目录存放的是gradle插件名称,在外界apply需要根据定义的文件名进行apply,这里我命名hotfix.patch,那么在外界就使用apply plugin: ‘hotfix.plugin’
hotfix.plugin.properties文件内容如下,指定插件的实现是哪个类
implementation-class=com.huli.plugin.HotfixPatchPlugin
其次解释groovy包下的代码
下面是两个类,一个是HotfixExtension类,可以用来进行参数的传递,代码为
package com.huli.plugin
class HotfixExtension {
String name //打patch包的task所依赖的task
}
然后是插件的代码
package com.huli.plugin
public class HotfixPatchPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.extensions.create('hotfixExt', HotfixExtension)
println "this is a custom plugin"
//在插件中定义task
project.task('hello'){
doLast{
println "hello " + project.hotfixExt.name
}
}
}
}
在plugin中我们只需要实现apply方法,方法传入的是引用该plugin的project,project中存储着各种参数
把插件打包到本地仓库
点击下面按钮就能将插件打包上传到本地maven,之后会在刚才创建的maven目录下生成对应的jar
生成的目录如图,每次对插件代码的修改都需要再次上传
应用我们的插件
在根目录的build.gradle下配置
buildscript {
repositories {
maven { url uri('repo-hotfix-patch') }
}
dependencies {
classpath 'com.huli.plugin:hotfix-patch:1.0.0'
}
}
在app的build.gradle中引用插件
apply plugin: 'hotfix.patch'
hotfixExt {
name = "xiaoming"
}
在gradle中执行命令 ./gradlew hello
就能成功输出
hello xiaoming
六、patch打包任务分析
根据以上知识点,我们为了将这个打包任务抽成第一个第三方library,方便多个项目使用,我们需要使用第三种方式自定义plugin来实现
1. 新建Android Library工程 配置build.gradle
2. 创建名为HotfixPatchPlugin的插件,实现Plugin接口
3. 创建HotfixExtension实体类,用于传递参数
4. 创建PatchTask
5. 重写apply(Project project)方法
6. 使用自定义的Plugin
7. 配置需要打入patch包的类
8. 通过命令进行patch打包
./gradlew patchMe -PpatchName=shanshan
最后在根目录下生成对应的包含dex的jar包