Gradle 编程模型及 API 实例详解

转: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 函数中

 
  1. def initMinshengGradleEnvironment(){

  2. //属性值从 local.properites 中读取

  3. Properties properties = new Properties()

  4. File propertyFile = new File(rootDir.getAbsolutePath() + "/local.properties")

  5. properties.load(propertyFile.newDataInputStream())

  6. //gradle 就是 gradle 对象。它默认是 Settings 和 Project 的成员变量。可直接获取

  7.  
  8. //ext 前缀,表明操作的是外置属性。api 是一个新的属性名。前面说过,只在

  9. //第一次定义或者设置它的时候需要 ext 前缀

  10. gradle.ext.api = properties.getProperty('sdk.api')

  11.  
  12. println gradle.api //再次存取 api 的时候,就不需要 ext 前缀了

  13. ......

  14. }

再来一个例子强化一下:

我在 utils.gradle 中定义了一些函数,然后想在其他 build.gradle 中调用这些函数。那该怎么做呢?

 
  1. [utils.gradle]

  2. //utils.gradle 中定义了一个获取 AndroidManifests.xml versionName 的函数

  3. def getVersionNameAdvanced(){

  4. 下面这行代码中的 project 是谁?

  5.  
  6. def xmlFile = project.file("AndroidManifest.xml")

  7. def rootManifest = new XmlSlurper().parse(xmlFile)

  8. return rootManifest['@android:versionName']

  9. }

  10. //现在,想把这个 API 输出到各个 Project。由于这个 utils.gradle 会被每一个 Project Apply,所以

  11.  
  12. //我可以把 getVersionNameAdvanced 定义成一个 closure,然后赋值到一个外部属性

  13. 下面的 ext 是谁的 ext?

  14. ext{ //此段花括号中代码是闭包

  15. //除了 ext.xxx=value 这种定义方法外,还可以使用 ext{}这种书写方法。

  16.  
  17. //ext{}不是 ext(Closure)对应的函数调用。但是 ext{}中的{}确实是闭包。

  18. getVersionNameAdvanced = this.&getVersionNameAdvanced

  19. }

上面代码中有两个问题:

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 中都有如下的代码:

 
  1. tasks.getByName("assemble"){

  2. it.doLast{

  3. println "$project.name: After assemble, jar libs are copied to local repository"

  4. copyOutput(true) //copyOutput 是 utils.gradle 输出的 closure

  5. }

  6. }

通过这种方式,我将一些常用的函数放到 utils.gradle 中,然后为加载它的 Project 设置 ext 属性。最后,Project 中就可以调用这种赋值函数了!

注意:此处我研究的还不是很深,而且我个人感觉:

1 在 Java 和 Groovy 中:我们会把常用的函数放到一个辅助类和公共类中,然后在别的地方 import 并调用它们。

2 但是在 Gradle,更正规的方法是在 xxx.gradle 中定义插件。然后通过添加 Task 的方式来完成工作。gradle 的 user guide 有详细介绍如何实现自己的插件。

  1. 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 包到我自己设计的产出物目录中。

 
  1. //Library 工程必须加载此插件。注意,加载了 Android 插件就不要加载 Java 插件了。因为 Android

  2. //插件本身就是拓展了 Java 插件

  3. apply plugin: 'com.android.library'

  4. //android 的编译,增加了一种新类型的 Script Block-->android

  5. android {

  6. //你看,我在 local.properties 中设置的 API 版本号,就可以一次设置,多个 Project 使用了

  7.  
  8. //借助我特意设计的 gradle.ext.api 属性

  9.  
  10. compileSdkVersion = gradle.api //这两个红色的参数必须设置

  11.  
  12. buildToolsVersion = "22.0.1"

  13. sourceSets{ //配置源码路径。这个 sourceSets 是 Java 插件引入的

  14.  
  15. main{ //main:Android 也用了

  16.  
  17. manifest.srcFile 'AndroidManifest.xml' //这是一个函数,设置 manifest.srcFile

  18. aidl.srcDirs=['src'] //设置 aidl 文件的目录

  19.  
  20. java.srcDirs=['src'] //设置 java 文件的目录

  21.  
  22. }

  23. }

  24. dependencies { //配置依赖关系

  25.  
  26. //compile 表示编译和运行时候需要的 jar 包,fileTree 是一个函数,

  27.  
  28. //dir:'libs',表示搜索目录的名称是 libs。include:['*.jar'],表示搜索目录下满足*.jar 名字的 jar

  29. //包都作为依赖 jar 文件

  30.  
  31. compile fileTree(dir: 'libs', include: ['*.jar'])

  32. }

  33. } //android SB 配置完了

  34.  
  35. //clean 是一个 Task 的名字,这个 Task 好像是 Java 插件(这里是 Android 插件)引入的。

  36. //dependsOn 是一个函数,下面这句话的意思是 clean 任务依赖 cposCleanTask 任务。所以

  37. //当你 gradle clean 以执行 clean Task 的时候,cposCleanTask 也会执行

  38. clean.dependsOn 'cposCleanTask'

  39. //创建一个 Task,

  40. task cposCleanTask() <<{

  41. cleanOutput(true) //cleanOutput 是 utils.gradle 中通过 extra 属性设置的 Closure

  42. }

  43. //前面说了,我要把 jar 包拷贝到指定的目录。对于 Android 编译,我一般指定 gradle assemble

  44. //它默认编译 debug 和 release 两种输出。所以,下面这个段代码表示:

  45. //tasks 代表一个 Projects 中的所有 Task,是一个容器。getByName 表示找到指定名称的任务。

  46. //我这里要找的 assemble 任务,然后我通过 doLast 添加了一个 Action。这个 Action 就是 copy

  47. //产出物到我设置的目标目录中去

  48. tasks.getByName("assemble"){

  49. it.doLast{

  50. println "$project.name: After assemble, jar libs are copied to local repository"

  51. copyOutput(true)

  52. }

  53. }

  54. /*

  55. 因为我的项目只提供最终的 release 编译出来的 Jar 包给其他人,所以不需要编译 debug 版的东西

  56.  
  57. 当 Project 创建完所有任务的有向图后,我通过 afterEvaluate 函数设置一个回调 Closure。在这个回调

  58.  
  59. Closure 里,我 disable 了所有 Debug 的 Task

  60. */

  61. project.afterEvaluate{

  62. disableDebugBuild()

  63. }

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 的依赖关系是最新的。

 
  1. [build.gradle]

  2. apply plugin: 'com.android.application' //APK 编译必须加载这个插件

  3. android {

  4. compileSdkVersion gradle.api

  5. buildToolsVersion "22.0.1"

  6. sourceSets{ //差不多的设置

  7.  
  8. main{

  9. manifest.srcFile 'AndroidManifest.xml'

  10. //通过设置 jni 目录为空,我们可不使用 apk 插件的 jni 编译功能。为什么?因为据说

  11.  
  12. //APK 插件的 jni 功能好像不是很好使....晕菜

  13.  
  14. jni.srcDirs = []

  15. jniLibs.srcDir 'libs'

  16. aidl.srcDirs=['src']

  17. java.srcDirs=['src']

  18. res.srcDirs=['res']

  19. }

  20. }//main 结束

  21.  
  22. signingConfigs { //设置签名信息配置

  23.  
  24. debug { //如果我们在 local.properties 设置使用特殊的 keystore,则使用它

  25.  
  26. //下面这些设置,无非是函数调用....请务必阅读 API 文档

  27.  
  28. if(project.gradle.debugKeystore != null){

  29. storeFile file("file://${project.gradle.debugKeystore}")

  30. storePassword "android"

  31. keyAlias "androiddebugkey"

  32. keyPassword "android"

  33. }

  34. }

  35. }//signingConfigs 结束

  36.  
  37. buildTypes {

  38. debug {

  39. signingConfig signingConfigs.debug

  40. jniDebuggable false

  41. }

  42. }//buildTypes 结束

  43.  
  44. dependencies {

  45. //compile:project 函数可指定依赖 multi-project 中的某个子 project

  46. compile project(':CPosDeviceSdk')

  47. compile fileTree(dir: 'libs', include: ['*.jar'])

  48. } //dependices 结束

  49.  
  50. repositories {

  51. flatDir { //flatDir:告诉 gradle,编译中依赖的 jar 包存储在 dirs 指定的目录

  52.  
  53. name "minsheng-gradle-local-repository"

  54. dirs gradle.LOCAL_JAR_OUT //LOCAL_JAR_OUT 是我存放编译出来的 jar 包的位置

  55.  
  56. }

  57. }//repositories 结束

  58.  
  59. }//android 结束

  60.  
  61. /*

  62. 创建一个 Task,类型是 Exec,这表明它会执行一个命令。我这里让他执行 ndk 的

  63.  
  64. ndk-build 命令,用于编译 ndk。关于 Exec 类型的 Task,请自行脑补 Gradle 的 API

  65. */

  66. //注意此处创建 task 的方法,是直接{}喔,那么它后面的 tasks.withType(JavaCompile)

  67. //设置的依赖关系,还有意义吗?Think!如果你能想明白,gradle 掌握也就差不多了

  68.  
  69. task buildNative(type: Exec, description: 'Compile JNI source via NDK') {

  70. if(project.gradle.ndkDir == null) //看看有没有指定 ndk.dir 路径

  71.  
  72. println "CANNOT Build NDK"

  73. else{

  74. commandLine "/${project.gradle.ndkDir}/ndk-build",

  75. '-C', file('jni').absolutePath,

  76. '-j', Runtime.runtime.availableProcessors(),

  77. 'all', 'NDK_DEBUG=0'

  78. }

  79. }

  80. tasks.withType(JavaCompile) {

  81. compileTask -> compileTask.dependsOn buildNative

  82. }

  83. ......

  84. //对于 APK,除了拷贝 APK 文件到指定目录外,我还特意为它们加上了自动版本命名的功能

  85.  
  86. tasks.getByName("assemble"){

  87. it.doLast{

  88. println "$project.name: After assemble, jar libs are copied to local repository"

  89. project.ext.versionName = android.defaultConfig.versionName

  90. println "\t versionName = $versionName"

  91. copyOutput(false)

  92. }

  93. }

  1. 结果展示

在 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 的内容。代码如下所示:

 
  1. [build.gradle]

  2. apply plugin: 'com.android.application' //加载 APP 插件

  3.  
  4. //加载 utils.gradle

  5. apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"

  6. //buildscript 设置 android app 插件的位置

  7.  
  8. buildscript {

  9. repositories { jcenter() }

  10. dependencies { classpath 'com.android.tools.build:gradle:1.2.3' }

  11. }

  12. //android ScriptBlock

  13. android {

  14. compileSdkVersion gradle.api

  15. buildToolsVersion "22.0.1"

  16. sourceSets{ //源码设置 SB

  17. main{

  18. manifest.srcFile 'AndroidManifest.xml'

  19. jni.srcDirs = []

  20. jniLibs.srcDir 'libs'

  21. aidl.srcDirs=['src']

  22. java.srcDirs=['src']

  23. res.srcDirs=['res']

  24. assets.srcDirs = ['assets'] //多了一个 assets 目录

  25.  
  26. }

  27. }

  28. signingConfigs {//签名设置

  29.  
  30. debug { //debug 对应的 SB。注意

  31.  
  32. if(project.gradle.debugKeystore != null){

  33. storeFile file("file://${project.gradle.debugKeystore}")

  34. storePassword "android"

  35. keyAlias "androiddebugkey"

  36. keyPassword "android"

  37. }

  38. }

  39. }

  40. /*

  41. 最关键的内容来了: buildTypes ScriptBlock.

  42. buildTypes 和上面的 signingConfigs,当我们在 build.gradle 中通过{}配置它的时候,

  43.  
  44. 其背后的所代表的对象是 NamedDomainObjectContainer<BuildType> 和

  45.  
  46. NamedDomainObjectContainer<SigningConfig>

  47. 注意,NamedDomainObjectContainer<BuildType/或者 SigningConfig>是一种容器,

  48.  
  49. 容器的元素是 BuildType 或者 SigningConfig。我们在 debug{}要填充 BuildType 或者

  50.  
  51. SigningConfig 所包的元素,比如 storePassword 就是 SigningConfig 类的成员。而 proguardFile 等

  52.  
  53. 是 BuildType 的成员。

  54.  
  55. 那么,为什么要使用 NamedDomainObjectContainer 这种数据结构呢?因为往这种容器里

  56.  
  57. 添加元素可以采用这样的方法: 比如 signingConfig 为例

  58.  
  59. signingConfig{//这是一个 NamedDomainObjectContainer<SigningConfig>

  60. test1{//新建一个名为 test1 的 SigningConfig 元素,然后添加到容器里

  61.  
  62. //在这个花括号中设置 SigningConfig 的成员变量的值

  63.  
  64. }

  65. test2{//新建一个名为 test2 的 SigningConfig 元素,然后添加到容器里

  66.  
  67. //在这个花括号中设置 SigningConfig 的成员变量的值

  68.  
  69. }

  70. }

  71. 在 buildTypes 中,Android 默认为这几个 NamedDomainObjectContainer 添加了

  72.  
  73. debug 和 release 对应的对象。如果我们再添加别的名字的东西,那么 gradle assemble 的时候

  74.  
  75. 也会编译这个名字的 apk 出来。比如,我添加一个名为 test 的 buildTypes,那么 gradle assemble

  76. 就会编译一个 xxx-test-yy.apk。在此,test 就好像 debug、release 一样。

  77.  
  78. */

  79. buildTypes{

  80. debug{ //修改 debug 的 signingConfig 为 signingConfig.debug 配置

  81.  
  82. signingConfig signingConfigs.debug

  83. }

  84. demo{ //demo 版需要混淆

  85.  
  86. proguardFile 'proguard-project.txt'

  87. signingConfig signingConfigs.debug

  88. }

  89. //release 版没有设置,所以默认没有签名,没有混淆

  90.  
  91. }

  92. ......//其他和 posdevice 类似的处理。来看如何动态生成 runtime_config 文件

  93.  
  94. def runtime_config_file = 'assets/runtime_config'

  95. /*

  96. 我们在 gradle 解析完整个任务之后,找到对应的 Task,然后在里边添加一个 doFirst Action

  97. 这样能确保编译开始的时候,我们就把 runtime_config 文件准备好了。

  98.  
  99. 注意,必须在 afterEvaluate 里边才能做,否则 gradle 没有建立完任务有向图,你是找不到

  100.  
  101. 什么 preDebugBuild 之类的任务的

  102.  
  103. */

  104. project.afterEvaluate{

  105. //找到 preDebugBuild 任务,然后添加一个 Action

  106. tasks.getByName("preDebugBuild"){

  107. it.doFirst{

  108. println "generate debug configuration for ${project.name}"

  109. def configFile = new File(runtime_config_file)

  110. configFile.withOutputStream{os->

  111. os << I am Debug\n' //往配置文件里写 I am Debug

  112. }

  113. }

  114. }

  115. //找到 preReleaseBuild 任务

  116.  
  117. tasks.getByName("preReleaseBuild"){

  118. it.doFirst{

  119. println "generate release configuration for ${project.name}"

  120. def configFile = new File(runtime_config_file)

  121. configFile.withOutputStream{os->

  122. os << I am release\n'

  123. }

  124. }

  125. }

  126. //找到 preDemoBuild。这个任务明显是因为我们在 buildType 里添加了一个 demo 的元素

  127.  
  128. //所以 Android APP 插件自动为我们生成的

  129.  
  130. tasks.getByName("preDemoBuild"){

  131. it.doFirst{

  132. println "generate offlinedemo configuration for ${project.name}"

  133. def configFile = new File(runtime_config_file)

  134. configFile.withOutputStream{os->

  135. os << I am Demo\n'

  136. }

  137. }

  138. }

  139. }

  140. }

  141. .....//copyOutput

  142.  

最终的结果如图 39 所示:

几个问题,为什么我知道有 preXXXBuild 这样的任务?

答案:gradle tasks --all 查看所有任务。然后,多尝试几次,直到成功

猜你喜欢

转载自blog.csdn.net/qq_36838191/article/details/81094250