转:http://wiki.jikexueyuan.com/project/deep-android-gradle/four-four.html
希望你在进入此节之前,一定花时间把前面内容看一遍!!!
https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html。加载插件是调用它的 apply 函数.apply 其实是 Project 实现的 PluginAware 接口定义的:
来看代码:
[apply 函数的用法] apply 是一个函数,此处调用的是图 30 中最后一个 apply 函数。注意,Groovy 支持
函数调用的时候通过 参数名 1:参数值 2,参数名 2:参数值 2 的方式来传递参数
apply plugin: 'com.android.library' <==如果是编译 Library,则加载此插件
apply plugin: 'com.android.application' <==如果是编译 Android APP,则加载此插件
除了加载二进制的插件(上面的插件其实都是下载了对应的 jar 包,这也是通常意义上我们所理解的插件),还可以加载一个 gradle 文件。为什么要加载 gradle 文件呢?
其实这和代码的模块划分有关。一般而言,我会把一些通用的函数放到一个名叫 utils.gradle 文件里。然后在其他工程的 build.gradle 来加载这个 utils.gradle。这样,通过一些处理,我就可以调用 utils.gradle 中定义的函数了。
加载 utils.gradle 插件的代码如下:
utils.gradle 是我封装的一个 gradle 脚本,里边定义了一些方便函数,比如读取 AndroidManifest.xml 中
的 versionName,或者是 copy jar 包/APK 包到指定的目录
apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"
也是使用 apply 的最后一个函数。那么,apply 最后一个函数到底支持哪些参数呢?还是得看图 31 中的 API 说明:
我这里不遗余力的列出 API 图片,就是希望大家在写脚本的时候,碰到不会的,一定要去查看 API 文档!
2.设置属性
如果是单个脚本,则不需要考虑属性的跨脚本传播,但是 Gradle 往往包含不止一个 build.gradle 文件,比如我设置的 utils.gradle,settings.gradle。如何在多个脚本中设置属性呢?
Gradle 提供了一种名为 extra property 的方法。extra property 是额外属性的意思,在第一次定义该属性的时候需要通过 ext 前缀来标示它是一个额外的属性。定义好之后,后面的存取就不需要 ext 前缀了。ext 属性支持 Project 和 Gradle 对象。即 Project 和 Gradle 对象都可以设置 ext 属性
举个例子:
我在 settings.gradle 中想为 Gradle 对象设置一些外置属性,所以在 initMinshengGradleEnvironment 函数中
-
def initMinshengGradleEnvironment(){ -
//属性值从 local.properites 中读取 -
Properties properties = new Properties() -
File propertyFile = new File(rootDir.getAbsolutePath() + "/local.properties") -
properties.load(propertyFile.newDataInputStream()) -
//gradle 就是 gradle 对象。它默认是 Settings 和 Project 的成员变量。可直接获取 -
//ext 前缀,表明操作的是外置属性。api 是一个新的属性名。前面说过,只在 -
//第一次定义或者设置它的时候需要 ext 前缀 -
gradle.ext.api = properties.getProperty('sdk.api') -
println gradle.api //再次存取 api 的时候,就不需要 ext 前缀了 -
...... -
}
再来一个例子强化一下:
我在 utils.gradle 中定义了一些函数,然后想在其他 build.gradle 中调用这些函数。那该怎么做呢?
-
[utils.gradle] -
//utils.gradle 中定义了一个获取 AndroidManifests.xml versionName 的函数 -
def getVersionNameAdvanced(){ -
下面这行代码中的 project 是谁? -
def xmlFile = project.file("AndroidManifest.xml") -
def rootManifest = new XmlSlurper().parse(xmlFile) -
return rootManifest['@android:versionName'] -
} -
//现在,想把这个 API 输出到各个 Project。由于这个 utils.gradle 会被每一个 Project Apply,所以 -
//我可以把 getVersionNameAdvanced 定义成一个 closure,然后赋值到一个外部属性 -
下面的 ext 是谁的 ext? -
ext{ //此段花括号中代码是闭包 -
//除了 ext.xxx=value 这种定义方法外,还可以使用 ext{}这种书写方法。 -
//ext{}不是 ext(Closure)对应的函数调用。但是 ext{}中的{}确实是闭包。 -
getVersionNameAdvanced = this.&getVersionNameAdvanced -
}
上面代码中有两个问题:
project 是谁?
ext 是谁的 ext?
上面两个问题比较关键,我也是花了很长时间才搞清楚。这两个问题归结到一起,其实就是:
加载 utils.gradle 的 Project 对象和 utils.gradle 本身所代表的 Script 对象到底有什么关系?
我们在 Groovy 中也讲过怎么在一个 Script 中 import 另外一个 Script 中定义的类或者函数(见 3.5 脚本类、文件 I/O 和 XML 操作一节)。在 Gradle 中,这一块的处理比 Groovy 要复杂,具体怎么搞我还没完全弄清楚,但是 Project 和 utils.gradle 对于的 Script 的对象的关系是:
-
当一个 Project apply 一个 gradle 文件的时候,这个 gradle 文件会转换成一个 Script 对象。这个,相信大家都已经知道了。
-
Script 中有一个 delegate 对象,这个 delegate 默认是加载(即调用 apply)它的 Project 对象。但是,在 apply 函数中,有一个 from 参数,还有一个 to 参数(参考图 31)。通过 to 参数,你可以把 delegate 对象指定为别的东西。
- delegate 对象是什么意思?当你在 Script 中操作一些不是 Script 自己定义的变量,或者函数时候,gradle 会到 Script 的 delegate 对象去找,看看有没有定义这些变量或函数。
现在你知道问题 1,2 和答案了:
问题 1:project 就是加载 utils.gradle 的 project。由于 posdevice 有 5 个 project,所以 utils.gradle 会分别加载到 5 个 project 中。所以,getVersionNameAdvanced 才不用区分到底是哪个 project。反正一个 project 有一个 utils.gradle 对应的 Script。
问题 2:ext:自然就是 Project 对应的 ext 了。此处为 Project 添加了一些 closure。那么,在 Project 中就可以调用 getVersionNameAdvanced 函数了
比如:我在 posdevice 每个 build.gradle 中都有如下的代码:
-
tasks.getByName("assemble"){ -
it.doLast{ -
println "$project.name: After assemble, jar libs are copied to local repository" -
copyOutput(true) //copyOutput 是 utils.gradle 输出的 closure -
} -
}
通过这种方式,我将一些常用的函数放到 utils.gradle 中,然后为加载它的 Project 设置 ext 属性。最后,Project 中就可以调用这种赋值函数了!
注意:此处我研究的还不是很深,而且我个人感觉:
1 在 Java 和 Groovy 中:我们会把常用的函数放到一个辅助类和公共类中,然后在别的地方 import 并调用它们。
2 但是在 Gradle,更正规的方法是在 xxx.gradle 中定义插件。然后通过添加 Task 的方式来完成工作。gradle 的 user guide 有详细介绍如何实现自己的插件。
- Task 介绍
Task 是 Gradle 中的一种数据类型,它代表了一些要执行或者要干的工作。不同的插件可以添加不同的 Task。每一个 Task 都需要和一个 Project 关联。
Task 的 API 文档位于 https://docs.gradle.org/current/javadoc/,选择 Index 这一项,然后 ctrl+f,输入图 34 中任何一个 Block,你都会找到对应的函数。比如我替你找了几个 API,如图 35 所示:
特别提示:当你下次看到一个不认识的 SB 的时候,就去看 API 吧。
下面来解释代码中的各个 SB:
-
subprojects:它会遍历 posdevice 中的每个子 Project。在它的 Closure 中,默认参数是子 Project 对应的 Project 对象。由于其他 SB 都在 subprojects 花括号中,所以相当于对每个 Project 都配置了一些信息。
-
buildscript:它的 closure 是在一个类型为 ScriptHandler 的对象上执行的。主意用来所依赖的 classpath 等信息。通过查看 ScriptHandler API 可知,在 buildscript SB 中,你可以调用 ScriptHandler 提供的 repositories(Closure )、dependencies(Closure)函数。这也是为什么 repositories 和 dependencies 两个 SB 为什么要放在 buildscript 的花括号中的原因。明白了?这就是所谓的行话,得知道规矩。不知道规矩你就乱了。记不住规矩,又不知道查 SDK,那么就彻底抓瞎,只能到网上到处找答案了!
- 关于 repositories 和 dependencies,大家直接看 API 吧。后面碰到了具体代码我们再来介绍
4.CPosDeviceSdk build.gradle
CPosDeviceSdk 是一个 Android Library。按 Google 的想法,Android Library 编译出来的应该是一个 AAR 文件。但是我的项目有些特殊,我需要发布 CPosDeviceSdk.jar 包给其他人使用。jar 在编译过程中会生成,但是它不属于 Android Library 的标准输出。在这种情况下,我需要在编译完成后,主动 copy jar 包到我自己设计的产出物目录中。
-
//Library 工程必须加载此插件。注意,加载了 Android 插件就不要加载 Java 插件了。因为 Android -
//插件本身就是拓展了 Java 插件 -
apply plugin: 'com.android.library' -
//android 的编译,增加了一种新类型的 Script Block-->android -
android { -
//你看,我在 local.properties 中设置的 API 版本号,就可以一次设置,多个 Project 使用了 -
//借助我特意设计的 gradle.ext.api 属性 -
compileSdkVersion = gradle.api //这两个红色的参数必须设置 -
buildToolsVersion = "22.0.1" -
sourceSets{ //配置源码路径。这个 sourceSets 是 Java 插件引入的 -
main{ //main:Android 也用了 -
manifest.srcFile 'AndroidManifest.xml' //这是一个函数,设置 manifest.srcFile -
aidl.srcDirs=['src'] //设置 aidl 文件的目录 -
java.srcDirs=['src'] //设置 java 文件的目录 -
} -
} -
dependencies { //配置依赖关系 -
//compile 表示编译和运行时候需要的 jar 包,fileTree 是一个函数, -
//dir:'libs',表示搜索目录的名称是 libs。include:['*.jar'],表示搜索目录下满足*.jar 名字的 jar -
//包都作为依赖 jar 文件 -
compile fileTree(dir: 'libs', include: ['*.jar']) -
} -
} //android SB 配置完了 -
//clean 是一个 Task 的名字,这个 Task 好像是 Java 插件(这里是 Android 插件)引入的。 -
//dependsOn 是一个函数,下面这句话的意思是 clean 任务依赖 cposCleanTask 任务。所以 -
//当你 gradle clean 以执行 clean Task 的时候,cposCleanTask 也会执行 -
clean.dependsOn 'cposCleanTask' -
//创建一个 Task, -
task cposCleanTask() <<{ -
cleanOutput(true) //cleanOutput 是 utils.gradle 中通过 extra 属性设置的 Closure -
} -
//前面说了,我要把 jar 包拷贝到指定的目录。对于 Android 编译,我一般指定 gradle assemble -
//它默认编译 debug 和 release 两种输出。所以,下面这个段代码表示: -
//tasks 代表一个 Projects 中的所有 Task,是一个容器。getByName 表示找到指定名称的任务。 -
//我这里要找的 assemble 任务,然后我通过 doLast 添加了一个 Action。这个 Action 就是 copy -
//产出物到我设置的目标目录中去 -
tasks.getByName("assemble"){ -
it.doLast{ -
println "$project.name: After assemble, jar libs are copied to local repository" -
copyOutput(true) -
} -
} -
/* -
因为我的项目只提供最终的 release 编译出来的 Jar 包给其他人,所以不需要编译 debug 版的东西 -
当 Project 创建完所有任务的有向图后,我通过 afterEvaluate 函数设置一个回调 Closure。在这个回调 -
Closure 里,我 disable 了所有 Debug 的 Task -
*/ -
project.afterEvaluate{ -
disableDebugBuild() -
}
Android 自己定义了好多 ScriptBlock。Android 定义的 DSL 参考文档在
<a rel="nofollow" href="https://developer.android.com/tools/building/plugin-for-gradle.html" "="" style="padding: 0px; margin: 0px; background-color: transparent; color: rgb(45, 133, 202);">https://developer.android.com/tools/building/plugin-for-gradle.html 下载。注意,它居然没有提供在线文档。
图 36 所示为 Android 的 DSL 参考信息。
图 37 为 buildToolsVersion 和 compileSdkVersion 的说明:
从图 37 可知,这两个变量是必须要设置的.....
5.CPosDeviceServerApk build.gradle
再来看一个 APK 的 build,它包含 NDK 的编译,并且还要签名。根据项目的需求,我们只能签 debug 版的,而 release 版的签名得发布 unsigned 包给领导签名。另外,CPosDeviceServerAPK 依赖 CPosDeviceSdk。
虽然我可以先编译 CPosDeviceSdk,得到对应的 jar 包,然后设置 CPosDeviceServerApk 直接依赖这个 jar 包就好。但是我更希望 CPosDeviceServerApk 能直接依赖于 CPosDeviceSdk 这个工程。这样,整个 posdevice 可以做到这几个 Project 的依赖关系是最新的。
-
[build.gradle] -
apply plugin: 'com.android.application' //APK 编译必须加载这个插件 -
android { -
compileSdkVersion gradle.api -
buildToolsVersion "22.0.1" -
sourceSets{ //差不多的设置 -
main{ -
manifest.srcFile 'AndroidManifest.xml' -
//通过设置 jni 目录为空,我们可不使用 apk 插件的 jni 编译功能。为什么?因为据说 -
//APK 插件的 jni 功能好像不是很好使....晕菜 -
jni.srcDirs = [] -
jniLibs.srcDir 'libs' -
aidl.srcDirs=['src'] -
java.srcDirs=['src'] -
res.srcDirs=['res'] -
} -
}//main 结束 -
signingConfigs { //设置签名信息配置 -
debug { //如果我们在 local.properties 设置使用特殊的 keystore,则使用它 -
//下面这些设置,无非是函数调用....请务必阅读 API 文档 -
if(project.gradle.debugKeystore != null){ -
storeFile file("file://${project.gradle.debugKeystore}") -
storePassword "android" -
keyAlias "androiddebugkey" -
keyPassword "android" -
} -
} -
}//signingConfigs 结束 -
buildTypes { -
debug { -
signingConfig signingConfigs.debug -
jniDebuggable false -
} -
}//buildTypes 结束 -
dependencies { -
//compile:project 函数可指定依赖 multi-project 中的某个子 project -
compile project(':CPosDeviceSdk') -
compile fileTree(dir: 'libs', include: ['*.jar']) -
} //dependices 结束 -
repositories { -
flatDir { //flatDir:告诉 gradle,编译中依赖的 jar 包存储在 dirs 指定的目录 -
name "minsheng-gradle-local-repository" -
dirs gradle.LOCAL_JAR_OUT //LOCAL_JAR_OUT 是我存放编译出来的 jar 包的位置 -
} -
}//repositories 结束 -
}//android 结束 -
/* -
创建一个 Task,类型是 Exec,这表明它会执行一个命令。我这里让他执行 ndk 的 -
ndk-build 命令,用于编译 ndk。关于 Exec 类型的 Task,请自行脑补 Gradle 的 API -
*/ -
//注意此处创建 task 的方法,是直接{}喔,那么它后面的 tasks.withType(JavaCompile) -
//设置的依赖关系,还有意义吗?Think!如果你能想明白,gradle 掌握也就差不多了 -
task buildNative(type: Exec, description: 'Compile JNI source via NDK') { -
if(project.gradle.ndkDir == null) //看看有没有指定 ndk.dir 路径 -
println "CANNOT Build NDK" -
else{ -
commandLine "/${project.gradle.ndkDir}/ndk-build", -
'-C', file('jni').absolutePath, -
'-j', Runtime.runtime.availableProcessors(), -
'all', 'NDK_DEBUG=0' -
} -
} -
tasks.withType(JavaCompile) { -
compileTask -> compileTask.dependsOn buildNative -
} -
...... -
//对于 APK,除了拷贝 APK 文件到指定目录外,我还特意为它们加上了自动版本命名的功能 -
tasks.getByName("assemble"){ -
it.doLast{ -
println "$project.name: After assemble, jar libs are copied to local repository" -
project.ext.versionName = android.defaultConfig.versionName -
println "\t versionName = $versionName" -
copyOutput(false) -
} -
}
- 结果展示
在 posdevice 下执行 gradle assemble 命令,最终的输出文件都会拷贝到我指定的目录,结果如图 38 所示:
图 38 所示为 posdevice gradle assemble 的执行结果:
-
library 包都编译 release 版的,copy 到 xxx/javaLib 目录下
-
apk 编译 debug 和 release-unsigned 版的,copy 到 apps 目录下
- 所有产出物都自动从 AndroidManifest.xml 中提取 versionName。
实例 2
下面这个实例也是来自一个实际的 APP。这个 APP 对应的是一个单独的 Project。但是根据我前面的建议,我会把它改造成支持 Multi-Projects Build 的样子。即在工程目录下放一个 settings.build。
另外,这个 app 有一个特点。它有三个版本,分别是 debug、release 和 demo。这三个版本对应的代码都完全一样,但是在运行的时候需要从 assets/runtime_config 文件中读取参数。参数不同,则运行的时候会跳转到 debug、release 或者 demo 的逻辑上。
注意:我知道 assets/runtime_config 这种做法不 decent,但,这是一个既有项目,我们只能做小范围的适配,而不是伤筋动骨改用更好的方法。另外,从未来的需求来看,暂时也没有大改的必要。
引入 gradle 后,我们该如何处理呢?
解决方法是:在编译 build、release 和 demo 版本前,在 build.gradle 中自动设置 runtime_config 的内容。代码如下所示:
-
[build.gradle] -
apply plugin: 'com.android.application' //加载 APP 插件 -
//加载 utils.gradle -
apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle" -
//buildscript 设置 android app 插件的位置 -
buildscript { -
repositories { jcenter() } -
dependencies { classpath 'com.android.tools.build:gradle:1.2.3' } -
} -
//android ScriptBlock -
android { -
compileSdkVersion gradle.api -
buildToolsVersion "22.0.1" -
sourceSets{ //源码设置 SB -
main{ -
manifest.srcFile 'AndroidManifest.xml' -
jni.srcDirs = [] -
jniLibs.srcDir 'libs' -
aidl.srcDirs=['src'] -
java.srcDirs=['src'] -
res.srcDirs=['res'] -
assets.srcDirs = ['assets'] //多了一个 assets 目录 -
} -
} -
signingConfigs {//签名设置 -
debug { //debug 对应的 SB。注意 -
if(project.gradle.debugKeystore != null){ -
storeFile file("file://${project.gradle.debugKeystore}") -
storePassword "android" -
keyAlias "androiddebugkey" -
keyPassword "android" -
} -
} -
} -
/* -
最关键的内容来了: buildTypes ScriptBlock. -
buildTypes 和上面的 signingConfigs,当我们在 build.gradle 中通过{}配置它的时候, -
其背后的所代表的对象是 NamedDomainObjectContainer<BuildType> 和 -
NamedDomainObjectContainer<SigningConfig> -
注意,NamedDomainObjectContainer<BuildType/或者 SigningConfig>是一种容器, -
容器的元素是 BuildType 或者 SigningConfig。我们在 debug{}要填充 BuildType 或者 -
SigningConfig 所包的元素,比如 storePassword 就是 SigningConfig 类的成员。而 proguardFile 等 -
是 BuildType 的成员。 -
那么,为什么要使用 NamedDomainObjectContainer 这种数据结构呢?因为往这种容器里 -
添加元素可以采用这样的方法: 比如 signingConfig 为例 -
signingConfig{//这是一个 NamedDomainObjectContainer<SigningConfig> -
test1{//新建一个名为 test1 的 SigningConfig 元素,然后添加到容器里 -
//在这个花括号中设置 SigningConfig 的成员变量的值 -
} -
test2{//新建一个名为 test2 的 SigningConfig 元素,然后添加到容器里 -
//在这个花括号中设置 SigningConfig 的成员变量的值 -
} -
} -
在 buildTypes 中,Android 默认为这几个 NamedDomainObjectContainer 添加了 -
debug 和 release 对应的对象。如果我们再添加别的名字的东西,那么 gradle assemble 的时候 -
也会编译这个名字的 apk 出来。比如,我添加一个名为 test 的 buildTypes,那么 gradle assemble -
就会编译一个 xxx-test-yy.apk。在此,test 就好像 debug、release 一样。 -
*/ -
buildTypes{ -
debug{ //修改 debug 的 signingConfig 为 signingConfig.debug 配置 -
signingConfig signingConfigs.debug -
} -
demo{ //demo 版需要混淆 -
proguardFile 'proguard-project.txt' -
signingConfig signingConfigs.debug -
} -
//release 版没有设置,所以默认没有签名,没有混淆 -
} -
......//其他和 posdevice 类似的处理。来看如何动态生成 runtime_config 文件 -
def runtime_config_file = 'assets/runtime_config' -
/* -
我们在 gradle 解析完整个任务之后,找到对应的 Task,然后在里边添加一个 doFirst Action -
这样能确保编译开始的时候,我们就把 runtime_config 文件准备好了。 -
注意,必须在 afterEvaluate 里边才能做,否则 gradle 没有建立完任务有向图,你是找不到 -
什么 preDebugBuild 之类的任务的 -
*/ -
project.afterEvaluate{ -
//找到 preDebugBuild 任务,然后添加一个 Action -
tasks.getByName("preDebugBuild"){ -
it.doFirst{ -
println "generate debug configuration for ${project.name}" -
def configFile = new File(runtime_config_file) -
configFile.withOutputStream{os-> -
os << I am Debug\n' //往配置文件里写 I am Debug -
} -
} -
} -
//找到 preReleaseBuild 任务 -
tasks.getByName("preReleaseBuild"){ -
it.doFirst{ -
println "generate release configuration for ${project.name}" -
def configFile = new File(runtime_config_file) -
configFile.withOutputStream{os-> -
os << I am release\n' -
} -
} -
} -
//找到 preDemoBuild。这个任务明显是因为我们在 buildType 里添加了一个 demo 的元素 -
//所以 Android APP 插件自动为我们生成的 -
tasks.getByName("preDemoBuild"){ -
it.doFirst{ -
println "generate offlinedemo configuration for ${project.name}" -
def configFile = new File(runtime_config_file) -
configFile.withOutputStream{os-> -
os << I am Demo\n' -
} -
} -
} -
} -
} -
.....//copyOutput
最终的结果如图 39 所示:
几个问题,为什么我知道有 preXXXBuild 这样的任务?
答案:gradle tasks --all 查看所有任务。然后,多尝试几次,直到成功