// groovy支持类型推断,def可以不写,但是规范一点还是带上
def a = 5;
def b = "groovy"
函数比较简单,就不赘述了。
下面的示例演示了函数的定义与调用:
def add(var1, var2){
//函数的最后一行作为返回值,与kotlin一致,可以省略return
var1+var2
}
// 传统的调用函数方式
println "test1: ${add(1, 2)}"
// 不用括号调用函数
println "test2: ${add 'a', 'b'}"
下面演示了利用groovy执行命令行的方法:
def gitVersion(){
if(System.getenv('CI_BUILD')){
return 1;
}
def cmd ='git rev-list HEAD --first-parent --count'
cmd.execute().text.trim().toInteger()
}
android {
defaultConfig {
versionCode gitVersion()
}
......
}
闭包是groovy的重要特性之一,详细的使用方法可以参考官网的相关介绍。闭包的定义及基本使用:
// 闭包的定义及调用
def closure = { String param ->
println "param.len = ${param.length()}"
}
closure("abc")
closure.call("abc")
闭包作为函数入参:
def func(Closure closure){
closure()
}
func {
println 'closure as parameter'
}
闭包有this、owner、delegate三个属性,在闭包内调用方法时,由他们来确定使用哪个对象来处理。来看一个委托模式的示例:
class Person {
String personName = '李四'
int personAge = 18
def printPerson(){
println "name is ${personName},age is ${personAge}"
}
}
def person(Closure<Person> closure){
Person p = new Person();
// 将闭包与代理对象绑定,这一步是必须的
closure.delegate = p
// 代理模式:
// 0. Closure.OWNER_FIRST:自身优先,默认
// 1. Closure.DELEGATE_FIRST:代理优先
// 2. Closure.OWNER_ONLY:仅自身
// 3. Closure.DELEGATE_ONLY:仅代理
closure.setResolveStrategy(Closure.DELEGATE_FIRST);
return closure
}
def closure = person {
personName = '张三'
personAge = 20
printPerson()
}
task configClosure {
doLast {
closure()
}
}
将ResolveStrategy分别设置为4种类型,运行:./gradlew configClosure
将得到下面的运行结果:
Closure.OWNER_FIRST | Closure.DELEGATE_FIRST | Closure.OWNER_ONLY | Closure.DELEGATE_ONLY |
---|---|---|---|
> Task :configClosure name is 张三,age is 20 | > Task :configClosure name is 张三,age is 20 | Execution failed for task ':configClosure'. > Could not set unknown property 'personName' of type org.gradle.api.Project. | > Task :configClosure name is 张三,age is 20 |
可见除了Closure.OWNER_ONLY
其余结果都是我们希望得到的,如果想打印默认的“李四”该怎么办呢?很简单不要重写对应的属性即可:
def closure = person {
// personName = '张三'
// personAge = 20
printPerson()
}
相信到这里你应该理解了闭包与代理对象间的相互关系了,Gradle中大量使用的Extension正是利用了这一特性,比如APG常见的:
com.android.application
uses {@link AppExtension} com.android.library
uses {@link LibraryExtension} com.android.test
uses {@link TestExtension} com.android.atom
uses {@link AtomExtension} com.android.instantapp
uses {@link InstantAppExtension} 上述XXXExtension都继承自BaseExtension,我们覆写相关Extension的属性达到更新配置的目的。
闭包的写法也很有特色,现有如下接口原型:PublicationContainer#create(String name, Class type, Action super U> configuration)
,下面两种写法都是可以的:
publishing {
publications{ PublicationContainer it->
libName(MavenPublication) {
version "$libVersion"
artifactId "$libArtifactId"
groupId "$libGroupId"
}
}
publications.create("$libArtifactId", MavenPublication) {
version "$libVersion"
artifactId "$libArtifactId"
groupId "$libGroupId"
}
}
操作符重载
參考:http://www.blogjava.net/johnnyjian/archive/2010/03/19/315962.html
Metaclass就是Class的Class,Class定義了該類實例的行為,Metaclass則定義了該類及其實例的行為(http://en.wikipedia.org/wiki/Metaclass))。Groovy通過Metaclass使程序可以在運行時修改/添加類的方法、屬性等(動態性)。
在Groovy中,每個Class都有一個對應的Metaclass,通過這個Metaclass可以給這個Class添加方法或屬性:
示例:為String類添加新方法
String.metaClass.capitalize = { delegate[0].toUpperCase() + delegate[1..-1] }
println("very".capitalize()) // 首字母大寫
// 获取指定task的帮助说明
gradle help --task "taskName"
// 获取所有task
gradle tasks --all
// 默认对rootProject执行命令
gradle assembleRelease
// 指定subProject执行命令
gradle :app:assembleRelease
// gradle支持驼峰缩写执行命令:等同于执行gradle assembleRelease
gradle aR
来源:经验:Android批量打包APK并批量安装
./gradlew :B:assembleRelease
./gradlew :C:assembleDebug
./gradlew :D:assembleAndroidTest
// 三个任务可以合并为下面
./gradlew clean :B:assembleRelease :C:assembleRelease :D:assembleAndroidTest
Initialization
setting.gradle
创建Project
对象,默认至少有个rootProject
,可以添加更多子项目,如:// 创建3个子项目
include ':app', ':a', ':b'
Configuration
Project
的build.gradle
脚本,完成脚本的构建流程。该阶段执行完毕后,整个脚本有多少个task以及task之间的依赖关系就确定了。需要注意的是该阶段不会执行task的执行内容(doFirst {}/ doLast {}),但是会执行task的构建内容。Execution
例如下build.gradle文件:
task printString {
group 'test'
description 'this is a task grouped as test.'
println "block run during configuration."
doLast {
println "doLast group:$group, description:$description"
}
doFirst {
println "doFirst group:$group, description:$description"
}
}
我们在命令行执行:gradle printString
,结果如下:
Starting a Gradle Daemon (subsequent builds will be faster)
> Configure project :
block run during configuration.
> Task :printString
doFirst group:test, description:this is a task grouped by test
doLast group:test, description:this is a task grouped by test
BUILD SUCCESSFUL in 8s
1 actionable task: 1 executed
- gradle的执行入口文件默认是
build.gradle
文件,也称为构建脚本。执行gradle命令时从当前目录下寻找build.gradle
文件来执行构建。- project中的全局变量可以通过在rootProject的build.gradle中定义 ext{…}来配置,需要引用的地方直接
$varName
即可。
前面提到了在Configuration
执行完毕后才建立起task graph,因此如果我们想做一些依赖于非gradle自带task的操作时,就要等待Configuration
执行完毕,并且是自动执行。gradle为我们提供了这样的hook入口:
在上面的示例build.gradle
中继续增加:
afterEvaluate { Project project ->
println "afterEvaluate ${project.name}"
}
task hook {
afterEvaluate { Project project ->
println "afterEvaluate2 ${project.name}"
}
}
gradle.projectsEvaluated {
println "projectsEvaluated"
tasks.findByName("printString").doFirst {
println 'hook printString from projectsEvaluated'
}
}
然后执行gradle printString
,输出结果为:
> Configure project :
block run during configuration.
afterEvaluate test1
afterEvaluate2 test1
projectsEvaluated
> Task :printString
hook printString from projectsEvaluated
doFirst group:test, description:this is a task grouped by test
doLast group:test, description:this is a task grouped by test
BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
在gradle执行的时候,会将三种不同的脚本文件转换成对应的三个对象:
gradle插件分为:
.gradle
为后缀的脚本文件,定义一些函数或执行一些逻辑,使用方法:eg. apply from: ‘xxx.gradle’。Plugin
接口,重写其@Override void apply(Project project) {…}方法,使用方法:eg. apply plugin: PluginFullClassname或者PluginID 。gradle插件本质上为一系列task的集合,通过插件的形式实现gradle task的复用和传播,相当于开发中的lib。例如作为安卓研发,谷歌为我们开发的com.android.tools.build:gradle:version
插件(gradle-plugin与gradle版本关系)就包含了com.android.application
和com.android.library
两大功能,涵盖了安卓开发汇总涉及到的各种task,熟练掌握这些task将获得很多我们“一般认知”之外的开发能力,可以查阅文末参考资料进一步学习。
在gradle官方用户文档中,特别强调了要熟悉核心插件的使用。java和android插件的通用task体包括:
这里有几点需要注意:
java
插件用于构建任何java项目,提供了’implementation’, 'testImplementation’等依赖方法。
官方文档:
The Java Plugin
引用方式:
apply plugin:‘java’
项目结构:
java-library
插件此插件是java插件的拓展,用于构建java库文件。
官方文档:
The Java Library Plugin
引用方式:
apply plugin:‘java-library’
groovy
插件扩展了java
插件,提供了对groovy开发的支持。
官方文档
The Groovy Plugin
引用方式
apply plugin:'groovy'
maven
插件用于发布成果物到maven仓库
官方文档
引用方式
apply plugin:'maven'
AGP需要读取Android SDK的路径以调用相关工具,因此需要相关配置,可以通过以下几种方式之一进行:
ANDROID_HOME
环境变量;需要用到NDK时同样需要通过下述方式指定NDK路径:
【参考Gradle官方说明】。由于AGP不是gradle自带的插件,需要在buildScript.dependencies中引入:classpath 'com.android.tools.build:gradle:4.1.0'
。
除此之外还应当熟悉安卓的工程结构:
com.android.application
插件用于构建安卓apk等安装包。
apply plugin:'com.android.application'
android {
// 指定构建工具版本,有默认值
buildToolsVersion 'version'
defaultConfig{
applicationId '**.**.**'
applicationIdSuffix '.two'
minSdkVersion 14
targetSdkVersion 25
versionCode 1
versionName "1.0"
versionNameSuffix ".0"
//用于Library中,可以将混淆文件输出到aar中,供Application混淆时使用。
consumerProguardFiles 'proguard-rules.pro'
//给渠道一个分组加维度的概念,比如现在有三个渠道包,分成免费和收费两种类型,可以添加一个dimension,
//打渠道包的时候会自动打出6个包,而不需要添加6个渠道,详细的说明可见 https://developer.android.com/studio/build/build-variants.html#flavor-dimensions。
dimensions 'api', 'fee'
//ndk的配置,AS2.2之后推荐切换到cmake的方式进行编译。
externalNativeBuild {
cmake {
cppFlags "-frtti -fexceptions"
arguments "-DANDROID_ARM_NEON=TRUE"
buildStagingDirectory "./outputs/cmake"
path "CMakeLists.txt"
version "3.7.1"
}
ndkBuild {
path "Android.mk"
buildStagingDirectory "./outputs/ndk-build"
}
}
ndk{
//只保留特定的api输出到apk文件中。
abiFilters 'x86', 'x86_64', 'armeabi'
}
javaCompileOptions{
annotationProcessorOptions { //注解的配置。
includeCompileClasspath true //需要使用注解功能。
arguments = [ eventBusIndex : 'org.greenrobot.eventbusperf.MyEventBusIndex' ] //AbstractProcessor中可以读取到该参数。
classNames
}
}
splits {
abi {
enable true //开启abi分包
universalApk true //是否创建一个包含所有有效动态库的apk
reset() //清空defaultConfig配置
include 'x86','armeabi' //打出包含的包 这个是和defaultConfig累加的
exclude 'mips' //排除指定的cpu架构
}
density{
enable true //开启density分包
reset() //清空所有默认值
include 'xhdpi','xxhdpi' //打出包含的包 这个是和默认值累加的
exclude 'mdpi' //排除指定
}
language {
enable true //开启language分包
include 'en','cn' // 指定语言
}
}
//manifest占位符 定义参数给manifest调用,如不同的渠道id。
manifestPlaceholders = [key:'value']
//开启multiDex
multiDexEnabled true
//手动拆包,将具体的类放在主DEX。
multiDexKeepFile file('multiDexKeep.txt')
//支持Proguard语法,进行一些模糊匹配。
multiDexKeepProguard file('multiDexKeep.pro')
//混淆文件的列表,如默认的android混淆文件及本地proguard文件,
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
//代码中可以通过BuildConfig.IS_RELEASE 调用。
buildConfigField('boolean','IS_RELEASE','false')
//在res/value 中添加demo 。
resValue('string','appname','demo')
//指定特定资源,可以结合productFlavors实现不同渠道的最小的apk包。
resConfigs "cn", "hdpi"
// 给每个模块指定一个特定的资源前缀,可以避免多模块使用相同的文件命名后合并冲突,在build.gradle中指定了这个配置后,AS会检查不合法的资源命名并报错
resourcePrefix 'xxx_'
}
buildTypes{
debug {
applicationIdSuffix '.debug'
versionNameSuffix '.1'
//生成的apk是否可以调试 debug默认是true release默认false
debuggable true
//是否可以调试NDK代码 使用lldb进行c和c++代码调试
jniDebuggable true
//是否开启png优化,会对png图片做一次最优压缩,影响编译速度,debug默认是false,release默认true
crunchPngs true
//Android Wear的支持
embedMicroApp true
//是否开启混淆
minifyEnabled true
//是否开启渲染脚本
renderscriptDebuggable false
//渲染脚本等级 默认是5
renderscriptOptimLevel 5
//是否zip对齐优化 默认就是true
zipAlignEnabled true
}
release {......}
}
productFlavors{
// 配置所有风味,可以覆盖defaultConfig的参数配置
free {
dimension 'fee'
......
}
vip {
dimension 'fee'
......
}
}
sourceSets{
main {
res.srcDirs 'src/main/res'
jniLibs.srcDirs = ['libs']
aidl.srcDirs 'src/main/aidl'
assets.srcDirs 'src/main/assets'
java.srcDirs 'src/main/java'
jni.srcDirs 'src/main/jni'
renderscript.srcDirs 'src/main/renderscript'
resources.srcDirs 'src/main/resources'
manifest.srcFile 'src/main/AndroidManifest.xml'
}
free { //除了main,也可以给不同的风味指定不同的配置
}
}
signingConfigs{
//签名文件的路径
storeFile file('debug.keystore')
//签名文件密码
storePassword 'android'
//别名
keyAlias 'androiddebygkey'
//key的密码
keyPassword 'android'
}
compileOptions{
//java源文件的编码格式 默认UTF-8
encoding 'UTF-8'
//java编译是否使用gradle新的增量模式
incremental true
//java源文件编译的jdk版本
sourceCompatibility JavaVersion.VERSION_1_7
//编译出的class的版本
targetCompatibility JavaVersion.VERSION_1_7
}
lintOptions{
// 设置为 true时lint将不报告分析的进度
quiet true
// 如果为 true,则当lint发现错误时停止 gradle构建
abortOnError false
// 如果为 true,则只报告错误
ignoreWarnings true
// 如果为 true,则当有错误时会显示文件的全路径或绝对路径
absolutePaths true
// 如果为 true,则检查所有的问题,包括默认不检查问题
checkAllWarnings true
// 如果为 true,则将所有警告视为错误
warningsAsErrors true
// 不检查给定的问题id
disable 'TypographyFractions','TypographyQuotes'
// 检查给定的问题 id
enable 'RtlHardcoded','RtlCompat', 'RtlEnabled'
// 仅检查给定的问题 id
check 'NewApi', 'InlinedApi'
// 如果为true,则在错误报告的输出中不包括源代码行
noLines true
// 如果为 true,则对一个错误的问题显示它所在的所有地方,而不会截短列表,等等。
showAll true
// 重置 lint 配置(使用默认的严重性等设置)。
lintConfig file("default-lint.xml")
// 如果为 true,生成一个问题的纯文本报告(默认为false)
textReport true
// 配置写入输出结果的位置;它可以是一个文件或 “stdout”(标准输出)
textOutput 'stdout'
// 如果为真,会生成一个XML报告,以给Jenkins之类的使用
xmlReport false
// 用于写入报告的文件(如果不指定,默认为lint-results.xml)
xmlOutput file("lint-report.xml")
// 如果为真,会生成一个HTML报告(包括问题的解释,存在此问题的源码,等等)
htmlReport true
// 写入报告的路径,它是可选的(默认为构建目录下的 lint-results.html )
htmlOutput file("lint-report.html")
// 设置为 true, 将使所有release 构建都以issus的严重性级别为fatal(severity=false)的设置来运行lint,并且,如果发现了致命(fatal)的问题,将会中止构建(由上面提到的 abortOnError 控制.
checkReleaseBuilds true
//设置给定问题的严重级别(severity)为fatal (这意味着他们将会在release构建的期间检查 (即使 lint 要检查的问题没有包含在代码中)
fatal 'NewApi', 'InlineApi'
// 设置给定问题的严重级别为error
error 'Wakelock', 'TextViewEdits'
// 设置给定问题的严重级别为warning
warning 'ResourceAsColor'
// 设置给定问题的严重级别(severity)为ignore(和不检查这个问题一样)
ignore 'TypographyQuotes'
}
testOptions{ }
aaptOptions{
//aapt执行时的额外参数
additionalParameters '--rename-manifest-package',
'cct.cn.gradle.lsn13','-S','src/main/res2','--auto-add-overlay'
//对png进行优化检查,buildTypes中配置也可
cruncherEnabled true
//对res目录下的资源文件进行排除 把res文件夹下面的所有.jpg格式的文件打包到apk中
ignoreAssets '*.jpg'
//对所有.jpg文件不进行压缩
noCompress '.jpg'
}
dexOptions{
//dx命令附加参数
additionalParameters '--minimal-main-dex','--set-max-idx-number=10000'
//执行dx时java虚拟机可用的最大内存大小
javaMaxHeapSize '2048m'
//开启大模式,所有的class打到一个dex中,可以忽略65535方法数的限制,低于14版本不可运行
jumboMode true
//在dex中是否保留Runtime注解 默认是true
keepRuntimeAnnotatedClasses true
//默认dex中的进程数 默认是4
maxProcessCount 4
//默认的线程数
threadCount 4
//对library预编译 提高编译效率 但是clean的时候比较慢 默认开启的
preDexLibraries true
}
packagingOptions{
//pickFirsts做用是 当有重复文件时 打包会报错 这样配置会使用第一个匹配的文件打包进入apk
pickFirsts = ['META-INF/LICENSE']
//重复文件会合并打包入apk
merge 'META-INF/LICENSE'
//打包时排除匹配文件
exclude 'META-INF/LICENSE'
}
//指定自定义的adb路径,可以配置在环境变量中
adbExecutable '/xx/yyy'
adbOptions {
//调用adb install命令时默认传递的参数
installOptions '-r' '-d'
//执行adb命令的超时时间
timeOutInMs 1000
}
dataBinding {
enabled = true //开启databinding
version = "1.0"
addDefaultAdapters = true
}
// applicationVariants是AppExtension继承自BaseExtension唯一拓展的成员变量
// 其参数类型是DefaultDomainObjectSet,是不同buildType及Flavor的集合
// applicationVariants最常用的是它的all方法
applicationVariants.all { v ->
if (v.buildType.name == 'release') {
v.outputs.all {
outputFileName = "app_V${v.versionName}+${v.versionCode}_${buildTime()}_${v.flavorName}.apk"
}
}
}
// flavor*buildType会构建出大量的apk,可以动态设置忽略一些产出
variantFilter { variant ->
def buildTypeName = variant.buildType.name
def flavorName = variant.flavors.name
if (flavorName.contains("360") && buildTypeName.contains("debug")) {
// Tells Gradle to ignore each variant that satisfies the conditions above.
setIgnore(true)
}
}
}
com.android.library
插件用于构建安卓依赖库(aar),大部分功能与com.android.application
重合,使用方法参见上面。
引用方式:
classpath ‘com.android.tools.build:gradle:4.1.0’ # 由于不是gradle自带,所以需要在构建工具的classpath中额外引入
apply plugin:‘com.android.library’
独有属性:
android.libraryVariants.all { variant ->
def mergedFlavor = variant.getMergedFlavor()
// Defines the value of a build variable you can use in the manifest.
mergedFlavor.manifestPlaceholders = [hostName:"www.example.com"]
}
可以使用Android Studio或者IDEA进行开发,不同的是IDEA支持创建Gradle Plugin类型的工程,而AS完全需要手工引入相关依赖,因此需要对Gradle Plugin工程目录结构有所了解。以AS开发为例,其
补充说明:插件的编码语言不一定必须为groovy,也可以是kotlin、java
关键步骤截图:
名称 | 截图 |
---|---|
module工程目录结构 | |
build.gradle | 得也可以自定义sourceSets 等属性 |
插件properties文件 | 注意文件名规则:pluginId.properties,pluginId可以与包名不一致 |
直接在rootProject build.gradle编写下编写自定义Plugin,然后在脚本头部引用:
// 演示一个最简单的Gradle插件
class SimpleTestExt {
String msg
String code
}
class SimpleTestPlugin implements Plugin<Project> {
void apply(Project project){
def extension = project.extensions.create('simpleTest', SimpleTestExt)
project.task('showSimpleTest'){
doFirst {
println "${extension.msg} from ${extension.code}"
}
}
}
}
// 在gradle脚本中引入插件
apply plugin: SimpleTestPlugin
simpleTest {
msg = 'Hi,Gradle'
code = '0'
}
如上面【使用AS开发】章节所示的方式,创建一个java module,添加groovy依赖进行开发,添加maven插件结合resource资源文件实现插件的发布和使用。这是一种方式,便于插件的传播与共享,有时我们仅希望在当前工程使用一些定制功能,此时可以使用下面buildSrc的方式。
同样创建一个java module,但名称必须是buildSrc
,同样修改java目录为groovy,具体流程如下:
与一般的创建插件工程的方式相比,区别是:
补充说明:buildSrc也可以创建pluginId.properties,然后通过apply plugin: pluginId方式进行导入,区别是不需要发布到仓库。其实不难发现,buildSrc的本质就是把这个module作为rootProject的构建时的依赖的源码,所以相关代码可以使用。
关键操作截图:
名称 | 截图 |
---|---|
工程结构 |
出了log日志这种被动方式,还是使用debug进行调试。开启方法为:
./gradlew --no-daemon -Dorg.gradle.debug=true :app:assembleRelease
用于在gradle脚本中定义参数。
定义Extension实体:
class FileSizer {
boolean includeCode = true
boolean includeResource = true
boolean enableBuildLog = false
@Override
String toString() {
return "FileSizer{" +
"includeCode=" + includeCode +
", includeResource=" + includeResource +
", enableBuildLog=" + enableBuildLog +
'}';
}
}
gradle中设置实体:
apply plugin: 'me.plugin.filesizer'
fileSizer {
includeResource true
includeCode false
enableBuildLog false
}
插件中读取实体:
def fileSizer = project.extensions.create('fileSizer', FileSizer)
在org.gradle.api.Project
中定义了task方法,返回一个Task对象。共有5个方法的重载,如下图所示:
最复杂的是入参含有Map参数的重构方法(术语为:map-style notation),Map参数包括:
Option | Description | Default Value |
---|---|---|
“name” | The name of the task to create. | None. Must be specified. |
“type” | The class of the task to create. | DefaultTask |
“action” | The closure or Action to execute when the task executes. See Task.doFirst(Action). | null |
“overwrite” | Replace an existing task? | false |
“dependsOn” | The dependencies of the task. See here for more details. | [] |
“group” | The group of the task. | null |
“description” | The description of the task. | null |
“constructorArgs” | The arguments to pass to the task class constructor. | null |
从上可知,task支持如下形式进行创建:
// 只传一个name参数
task myTask
task(name: myTask) //调用map重载方法
// name + closure
task myTask { /* logic */}
// name+action
task(name: myTask, action: new Action<Task>() {
@Override
void execute(Task task) {
/* logic */
}
})
task myTask {
doFirst {
/* logic */
}
}
task myTask << { /* logic */ }
操作符
<<
在Gradle 4.x中被弃用(deprecated),并且在Gradle 5.0 被移除(removed),使用会报:Could not find method leftShift() for arguments
.
/**
* 第一步:自定义类继承DefaultTask,用@TaskAction标记task任务实体
*/
class MyTask extends DefaultTask {
String msg='hello from myTask'
List<String> list = new ArrayList<>()
/**
方法名可以随意取,重点是执行入口需要用@TaskAction进行注解
TaskAction在doFirst之后,doLast之前执行
*/
@TaskAction
def doWork(){
println msg
list.forEach{ e-> println e}
}
def add(String... args){
list.addAll(args)
}
}
/**
* 第二步:向gradle注册我们自定义的task,这样我们可以从AS的gradle面板中看到这个task
*/
task callMyTask(type: MyTask){
group 'test'
msg = 'hello task' //重写了msg属性
add 'a','b','c' //调用了add方法
}
Task的一个不可忽视的属性就是其依赖关系,是一种有向无环图接口。通过上面构造参数中的dependsOn
指定所需依赖的任务:
task A << {println 'Hello from A'}
task B << {println 'Hello from B'}
task C << {println 'Hello from C'}
B.dependsOn A
C.dependsOn B
我们可以通过下面的方式查看指定task的依赖:
afterEvaluate {
project.tasks.findByName('assembleDebug').taskDependencies.getDependencies().each {
println "dependOn: ${it.name}"
it.taskDependencies.getDependencies().each {
println " : ${it.name}"
}
}
}
前面提到了使用
task.doFirst{...}
来hook目标任务,这里又多了一种方式,即:taskA.dependsOn taskB
在Gradle中,每一个task都有inputs和outputs,如果在执行一个Task时,如果它的输入和输出与前一次执行时没有发生变化,那么Gradle便会认为该Task是最新的,因此Gradle将不予执行而是在控制台打印[UP-TO-DATE],这就是增量构建的概念。
一个task的inputs和outputs可以是一个或多个文件,可以是文件夹,还可以是project的某个property,甚至可以是某个闭包所定义的条件。自定义task默认每次执行,但通过指定inputs和outputs,可以达到增量构建的效果。
示例:查看dependencies
任务的输入输出
def dp = tasks.findByName("dependencies")
println "dp.inputs = ${dp.inputs}"
println "dp.outputs = ${dp.outputs}"
打印结果:
> Configure project :app
dp.inputs = org.gradle.api.internal.tasks.DefaultTaskInputs@2d417d27
dp.outputs = org.gradle.api.internal.tasks.DefaultTaskOutputs@6cddd0c3
可以使用附加参数
-- rerun-taks
来强制执行task,忽略增量编译
// 创建一个独立的task
task copyTask(type: Copy) {
from 'src/main/resources' //必填,原型:CopySpec.from(java.lang.Object[])
into 'build/config' //必填,原型:CopySpec.into(java.lang.Object)
include '**/*.html' //可选,仅拷贝符合规则的文件
include '**/*.jsp' //可选
exclude { details -> //可选,剔除符合规则的文件
details.file.name.endsWith('.html') &&
details.file.text.contains('staging')
}
}
// 更常用的做法是直接调用Project.copy(org.gradle.api.Action)函数去复制文件
copy {
from 'src/main/resources'
into 'build/config'
rename { String oldName-> //配合重命名函数使用
return "newName"
}
// 使用正则表达示来映射文件名
rename '(.+)-staging-(.+)', '$1$2'
rename(/(.+)-staging-(.+)/, '$1$2')
}
// 嵌套形式的拷贝
task nestedSpecs(type: Copy) {
into 'build/explodedWar'
exclude '**/*staging*'
from('src/dist') {
eachFile {
println(it)
}
include '**/*.html'
}
// 将运行时的依赖库拷贝到libs目录下
into('libs') {
from configurations.runtime
}
}
from()
方法接受的参数和文件集合时files()
一样:
from 'src/main/webapp'
;from 'src/staging/index.html'
;from zipTree('src/main/assets.zip')
。同步拷贝:在拷贝的时候,把原文件拷贝到目标目录时,会把目标目录下之前的全部清除,这种方式很适合项目的依赖库拷贝
task libs(type: Sync) {
from configurations.runtime
// 拷贝之前会把$buildDir/libs目录下所有的清除
into "$buildDir/libs"
}
// 删除文件夹
task myClean(type: Delete) {
delete buildDir
}
// 删除符合规则的文件
task cleanTempFiles(type: Delete) {
delete fileTree("src").matching {
include "**/*.tmp"
}
}
apply plugin: 'java'
task zip(type: Zip) {
from 'src/dist'
// 方式一:通过into指定文件目录
into('libs') {
from configurations.runtime
}
// 方式二:通过指定文件目录和文件名指定
baseName = 'myGame' // 设置文件名称,效果:myGame-1.0.zip
archiveName = "my-distribution.zip" // 完整文件名
destinationDir = file("${buildDir}/dist") // 文件目录
}
解压缩用的是Copy:
task unpackFiles(type: Copy) {
from zipTree("${buildDir}/dist/my-distribution.zip")
into "${buildDir}/resources"
}
plugins {
id 'java'
}
tasks.jar{
manifest {
attributes( "Main-Class": "gradle.test.App")
}
}
不借助插件:
task buildJar {
mkdir("jar")
copy {
from(layout.buildDirectory.dir("classes/java/main"))
into("./jar")
}
Files.write(Paths.get(layout.projectDirectory.file("jar/MANIFEST.MF").asFile.absolutePath),"Main-Class: gradle.test.App\r".getBytes())
copy {
from(layout.projectDirectory.file("MANIFEST.MF"))
into(layout.projectDirectory.file("app/jar"))
}
this.exec {
workingDir("./jar")
executable("jar")
args("-cvfe" ,"test.jar", "gradle.test.App" ,"./")
}
}
task taskCompressPngs(type: Exec) {
def compressPngs = 1
// 不同task传入不同的入参
if("task_2" in gradle.startParameter.taskNames){
compressPngs = 0
}
// 命令主体
commandLine "myshell.sh $compressPngs".tokenize()
// 命令参数
// args compressPngs
}
class FileSizerTask extends DefaultTask {
private Set<File> paths
private final List<RecordItem> codeRecordItems = new ArrayList<>()
private final List<RecordItem> resRecordItems = new ArrayList<>()
private final isWindows = org.gradle.internal.os.OperatingSystem.current().isWindows()
void addPaths(Set<File> paths) {
this.paths = paths
}
@TaskAction
def start() {
codeRecordItems.clear()
resRecordItems.clear()
if (paths != null && !paths.isEmpty()) {
paths.forEach { f ->
calcFileSizeInDir(f)
}
}
printResult()
}
void printResult() {
File dstFile = new File("${project.buildDir.path}/${FileSizerPlugin.GROUP}/filesizer.json")
if (!dstFile.exists()) {
if (!dstFile.parentFile.exists()) {
dstFile.parentFile.mkdirs()
}
dstFile.createNewFile()
}
JsonObject json = new JsonObject()
if (!codeRecordItems.isEmpty()) {
def array = new JsonArray()
for (RecordItem item : codeRecordItems) {
array.add(item.toJson())
}
json.add("codeRecordItems", array)
}
if (!resRecordItems.isEmpty()) {
def array = new JsonArray()
for (RecordItem item : resRecordItems) {
array.add(item.toJson())
}
json.add("resRecordItems", array)
}
BufferedWriter writer = null
try {
writer = new BufferedWriter(new FileWriter(dstFile))
writer.write(json.toString())
} catch (IOException e) {
e.printStackTrace()
} finally {
if (writer != null)
writer.close()
}
}
void calcFileSizeInDir(File dir) {
def files = dir.listFiles()
for (File file : files) {
if (file.isFile()) {
if (isResource(file)) {
resRecordItems.add(new RecordItem(file.length(), file.path))
} else {
codeRecordItems.add(new RecordItem(file.length(), file.path))
}
} else {
calcFileSizeInDir(file)
}
}
}
boolean isResource(File file) {
return isWindows?file.path.contains('main\\res'):file.path.contains('main/res')
}
}
插件中注册task:
project.afterEvaluate {
project.task("fileSizer", type: FileSizerTask) {
group GROUP
description 'statistic files size including resource and code.'
project.logger.println fileSizer
addPaths resolveFilePaths(project, fileSizer)
}
if (fileSizer.enableBuildLog){
BuildCycleListener listener = new BuildCycleListener()
project.gradle.addListener(listener)
}
}
android gradle plugin V3.x 之后,每个 flavor 必须对应一个 dimension。Variants 共有三种类型:
这三种变量的公有属性【参考资料】:
属性名 | 属性类型 | 说明 |
---|---|---|
name | String | Variant的名字,必须是唯一的。 |
description | String | Variant的描述说明。 |
dirName | String | Variant的子文件夹名,必须也是唯一的。可能也会有不止一个子文件夹,例如“debug/flavor1” |
baseName | String | Variant输出的基础名字,必须唯一。 |
outputFile | File | Variant的输出,这是一个可读可写的属性。 |
processManifest ProcessManifest | 处理Manifest的task。 | vaidlCompile AidlCompile 编译AIDL文件的task。 |
renderscriptCompile | RenderscriptCompile | 编译Renderscript文件的task。 |
mergeResources | MergeResources | 混合资源文件的task。 |
mergeAssets | MergeAssets | 混合asset的task。 |
processResources | ProcessAndroidResources | 处理并编译资源文件的task。 |
generateBuildConfig | GenerateBuildConfig | 生成BuildConfig类的task。 |
javaCompile | JavaCompile | 编译Java源代码的task。 |
processJavaResources | Copy | 处理Java资源的task。 |
assemble | DefaultTask | Variant的标志性assemble task。 |
ApplicationVariant类还有以下附加属性:
属性名 | 属性类型 | 说明 |
---|---|---|
buildType | BuildType | Variant的BuildType。 |
productFlavors | List | Variant的ProductFlavor。一般不为空但也允许空值。 |
mergedFlavor | ProductFlavor | android.defaultConfig和variant.productFlavors的合并。 |
signingConfig | SigningConfig | Variant使用的SigningConfig对象。 |
isSigningReady | boolean | 如果是true则表明这个Variant已经具备了所有需要签名的信息。 |
testVariant | BuildVariant | 将会测试这个Variant的TestVariant。 |
dex | Dex | 将代码打包成dex的task。如果这个Variant是个库,这个值可以为空。 |
packageApplication | PackageApplication | 打包最终APK的task。如果这个Variant是个库,这个值可以为空。 |
zipAlign | ZipAlign | zip压缩APK的task。如果这个Variant是个库或者APK不能被签名,这个值可以为空。 |
install | DefaultTask | 负责安装的task,不能为空。 |
uninstall | DefaultTask | 负责卸载的task。 |
LibraryVariant类还有以下附加属性:
属性名 | 属性类型 | 说明 |
---|---|---|
buildType | BuildType | Variant的BuildType. |
mergedFlavor | ProductFlavor | The defaultConfig values |
testVariant | BuildVariant | 用于测试这个Variant。 |
packageLibrary | Zip | 用于打包库项目的AAR文件。如果是个库项目,这个值不能为空。 |
TestVariant类还有以下属性:
属性名 | 属性类型 | 说明 |
---|---|---|
buildType | BuildType | Variant的Build Type。 |
productFlavors | List | Variant的ProductFlavor。一般不为空但也允许空值。 |
mergedFlavor | ProductFlavor | android.defaultConfig和variant.productFlavors的合并。 |
signingConfig | SigningConfig | Variant使用的SigningConfig对象。 |
isSigningReady | boolean | 如果是true则表明这个Variant已经具备了所有需要签名的信息。 |
testedVariant | BaseVariant | TestVariant测试的BaseVariant |
dex | Dex | 将代码打包成dex的task。如果这个Variant是个库,这个值可以为空。 |
packageApplication | PackageApplication | 打包最终APK的task。如果这个Variant是个库,这个值可以为空。 |
zipAlign | ZipAlign | zip压缩APK的task。如果这个Variant是个库或者APK不能被签名,这个值可以为空。 |
install | DefaultTask | 负责安装的task,不能为空。 |
uninstall | DefaultTask | 负责卸载的task。 |
connectedAndroidTest | DefaultTask | 在连接设备上行执行Android测试的task。 |
providerAndroidTest | DefaultTask | 使用扩展API执行Android测试的task。 |
示例:让“demo”版应用仅支持 API 级别 23 及更高级别,使用 variantFilter
代码块过滤掉所有将“minApi21”和“demo”产品变种组合在一起的构建变体配置:
android {
...
buildTypes {...}
flavorDimensions "api", "mode"
productFlavors {
demo {
dimension "mode"
matchingFallbacks = ["debug", "release"]
}
full {...}
minApi24 {...}
minApi23 {...}
minApi21 {...}
}
variantFilter { variant ->
def names = variant.flavors*.name
// To check for a certain build type, use variant.buildType.name == ""
if (names.contains("minApi21") && names.contains("demo")) {
// Gradle ignores any variants that satisfy the conditions above.
setIgnore(true)
}
}
}
...
Variant常见用法:
this.afterEvaluate {
// 评估完成后可以获取到所有的变体
// 当然这里applicationVariants也可以是上述的另外两种
this.android.applicationVariants.all { variant ->
def name = variant.name //比如:douyinDebug
def baseName = variant.baseName //对应:douyin-debug
variant.outputs.each {
// 由于我们当前的变体是 application 类型的,所以
// 这个 output 就是我们 APK 文件的输出路径,我们
// 可以通过重命名这个文件来修改我们最终输出的 APK 文件
outputFileName = "app-${variant.baseName}-${variant.versionName}.apk"
println outputFileName
}
// 获取变体的task,taskName支持拼接,所以非常灵活
def task = variant.checkManifest
println task.name
checkTask.doFirst {
// hook目标task,做一些特殊操作
def bt = variant.buildType.name
if (bt == 'qa' || bt == 'preview'
|| bt == 'release') {
update_plugin(bt)
}
}
}
}
Transform是Android Gradle V1.5.0 版本以后提供的API,此类用于在class文件被转化为dex文件之前去修改字节码以实现插桩需求。Transform最终会在AGP中被转化成对应的TransformTask,被TaskManager管理。
自定义Transform需要依赖implementation 'com.android.tools.build:gradle:1.5.0+'
。
注册Transform:
/* registerTransform必须在评估完成前进行注册,afterEvaluate阶段注册是无效的 */
// 方式一:
// def android = project.extensions.findByType(AppExtension);
// android.registerTransform(new TransformerTransform(project))
// 方式二:也可以简写如下,但是就没有代码提示了
project.android.registerTransform(new TransformerTransform(project))
Transform的5个重写方法:
重点是transform方法 ,在这个方法中去修改class文件注入我们的逻辑,通常有两种方式:Javassist
和ASM
,其中Javassist更接近java编码习惯,因此比较容易入门,ASM则比较复杂。
构建过程中处理代码另一个重要的操作对象就是文件了,参考Working With Files学习如何使用gradle task体系复制、创建、移动、删除文件(夹),以及压缩/解压文件、构建jar包等。
文件遍历:
// 浅层遍历
file("/home/HouXinLin/test/").listFiles().each{
println(it)
}
// 深层遍历
Files.walkFileTree(Paths.get("/home/HouXinLin/test/"),new SimpleFileVisitor<java.nio.file.Path>(){
@Override
FileVisitResult visitFile(java.nio.file.Path file, BasicFileAttributes attrs) throws IOException {
println(file)
return super.visitFile(file, attrs)
}
})
FileCollection collection = project.files(
'src/test1.txt',
new File('src/test2.txt'),
['src/test3.txt', 'src/test4.txt'])
// 遍历所有集合
collection.each { File file ->
println file.name
}
// 把文件集合转换为Set类型
Set set1 = collection.files
Set set2 = collection as Set
// 把文件集合转换为List类型
List list = collection as List
// 把文件集合转换为String类型
String path = collection.asPath
// 把文件集合转换为File类型
File file1 = collection.singleFile
File file2 = collection as File
// 添加或者删除一个集合
def union = collection + files('src/test5.txt')
def different = collection - files('src/test3.txt')
注意:文件集合中的文件对象都是延迟操作的,也就是说你可以创建一个 FileCollection 对象,它所包含的所有文件都是在未来被其它任务调用时才会真正的创建。
创建文件树:
// 指定目录创建文件树对象
FileTree tree = fileTree(dir: 'src/main')
// 给文件树对象添加包含指定文件
tree.include '**/*.java'
// 给文件树对象添加排除指定文件
tree.exclude '**/Abstract*'
// 使用路径创建文件树对象,同时指定包含的文件
tree = fileTree('src').include('**/*.java')
// 通过闭包创建文件树
tree = fileTree('src') {
include '**/*.java'
}
// 通过map创建文件树
tree = fileTree(dir: 'src', include: '**/*.java')
tree = fileTree(dir: 'src', includes: ['**/*.java', '**/*.xml'])
tree = fileTree(dir: 'src', include: '**/*.java', exclude: '**/*test*/**')
操作文件树:
// 遍历文件树的所有文件
tree.each {File file ->
println file
}
// 过虑生成新的文件树对象
FileTree filtered = tree.matching {
include 'org/gradle/api/**'
}
// 使用“+”号合并两个文件树,同文件集合的“+”操作一样
FileTree sum = tree + fileTree(dir: 'src/test')
// 访问文件树中各项内容
tree.visit {element ->
println "$element.relativePath => $element.file"
}
def file = file('./test.txt')
def string = file.text //读取全文
def input = file.newDataInputStream()
def output = file.newDataOutputStream()
output << input //IO读写语法糖
input.close()
output.close()
Groovy Xml 操作
gradle依赖:
implementation 'org.javassist:javassist:3.28.0-GA'
使用教程和示例参见:https://github.com/jboss-javassist/javassist
gradle依赖:
implementation "org.ow2.asm:asm:6.0"
implementation "org.ow2.asm:asm-util:6.0"
implementation "org.ow2.asm:asm-commons:6.0"
使用教程和示例参见:https://asm.ow2.io/。 由于classpath中引入了classpath 'com.android.tools.build:gradle:version'
,而gradle依赖gradle-core,gradle-core依赖lint,lint依赖lint-checks,lint-checks最后依赖到了asm,所以如果其他build.gradle
中要引用asm相关的类,不用设置classpath,直接import就可以了。但是其他other.gradle
不享有这种传递性,需要自己配置buildscript:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:$version$'
}
}
- ASM的语法虽然比较难以读懂,但是仍然有一些技巧,比如可以先用Java写出你想要实现的插桩后的代码,然后使用
Asm bytecode viewer
、ASM Bytecode Outline
等插件查看其对应的ASM字节码,然后对照着最终结果反过去写实现过程。然后在build\intermedates\transforms\yourTransform
去找到修改后的class文件,使用jd-gui.jar
等工具查看生成的代码是否正确。- 目前的这些AS插件都是支持Java转ASM bytecode,而不支持kotlin转ASM bytecode,因此如果想要对kotlin进行插桩,可以先将kotlin编译成class,然后使用
asm-all.jar
工具将class转为ASM bytecode。
aspectJ同样可以在JavaCompile结束后对生成的字节码二次编辑修改从而实现AOP。