如果像写代码一样写 Gradle 脚本 - 简书

1,687 阅读6分钟
原文链接: www.jianshu.com

很早前写过一篇Gradle入门 的文章,然后就没有后续。后来工作需要,陆续重写了X盟分析、反馈、分享SDK的打包脚本,之前的打包脚本都是基于ant的,动辄写上几百上千行XML标记,但是同样的事情用Gradle来做,不仅语法更容易理解而且代码量也少了2/3还多。

掌握一门打包脚本并能够像使用一门机器语言一样熟练是非常重要的。写Java的同学可以轻松实现产品需求,是因为他已经透彻java知道该怎么做,写打包脚本同样如此。

Gradle 打包过程

Gradle在执行脚本的时候分两个阶段

比如如下的脚本

task hello() {    
       doFirst {        
              println 'world1'    
       }    
       println 'hello'    
       doLast {        
              println 'world2'    
       }
}

会打印如下结果:

> gradle hello
hello
:hello
world1
world2

BUILD SUCCESSFUL

为什么会先打印 "hello" 而不是world1呢,是因为Gradle在载入这个脚本的时候会先运行脚本(Groovy)配置任务和一些属性,所以脚本在这个阶段只是像普通的Groovy代码一样在运行。执行到 println 'hello'的时候自然就打印了这句话。但是 doFirstdoLast里面的东西是在任务执行的时候调用的,所以后打印出来了。

了解这个过程非常关键,因为大部分时候我们需要配置任务的执行顺序,但是如果任务还没有配置好,那么是无法执行的。比如这样

task hello(dependsOn:world) {    
       println( 'hello' )
}
task world(){    
       println( 'world' )
}

会报如下的错误

>gradle hello
...
Could not find property 'world'

理解这个错误,需要上面提到的知识,脚本第一次逐行运行的时候是做配置工作的,当它运行到 task hello(dependsOn:world) {这句的时候发现一个变量 world 之前从来没有见到,脚本无法继续执行,报错。

解决这个问题可以把 task world的定义放到task hello之前,或者可以使用另外一个技巧。

task hello(dependsOn:'world'){    
       println( 'hello' )
}
task world(){    
       println( 'world' )
}

这样的写法只是给world加上了引号,但是这时候Gradle再次执行到 task hello(dependsOn:'world') 会把“world”当做任务的名字而不是任务本身来配置。而在真正执行任务的时候,已经配置完(知道了task world这个任务的存在),直接找到这个任务就行了。

对于打包任务来说,其实就是组织一些现成的任务来完成工作。所以知道有哪些任务可以调用,并规划他们的先后顺序非常重要。

Gradle 提供的基本任务

Gradle 官网提供了详细的 DSL文档 有好多可以使用和配置的东西。最常用的莫过于这些

  • Copy 复制任务把文件从A复制到B
  • Delete 删除文件,目录
  • Zip 把制定文件压缩成 .zip 包

Copy
下面是一个复制文档的任务,把文档从 'src/main/doc' 这个地方复制到 'build/target/doc' 过滤条件为只复制.md结尾的文件,并且不复制空文件夹。

task copyDocs(type: Copy) { 
     includeEmptyDirs = false
     from( 'src/main/doc') {
            include '**.md'
            into ('build/target/doc')
     }
}

Delete
删除文件夹 "uglyFolder" 和 "uglyFile" ,用起来更简单。

task makePretty(type: Delete) { 
       delete 'uglyFolder', 'uglyFile'
}

Zip
这段代码把"build/sdk"下面的文件复制到'build/hehe'里面,然后把"misc"里面的"config.json"文件复制到根目录下面。注意"from"来的文件默认是打包脚本所在目录。"into" 的根目录是 "destinationDir" 属性设置的地方。

task dabao( type:Zip){    
    destinationDir = file('build')    
    archiveName =  'abc.zip'    

    from('build/sdk') {        
          into( 'hehe' )   
    }    
    from('misc'){        
          include('config.json')        
          into('.')    
    }
}

有了上面这些任务,基本的文件增删改查都已经没问题了。所以熟悉上面三个任务的基本配置很重要。

任务调度

其实在最开始的已经见到了

task hello(dependsOn:'world'){    
       println( 'hello' )
}
task world(){    
       println( 'world' )
}

在 Gradle 中调度任务其实就是通过一系列的 dependsOn 操作来实现的,这样每个任务的执行都依赖另外一个任务,任务的顺序就建立起来了。

上面的例子意思是 task hello 依赖于 task world 如果要执行 task hello 那么要先执行 task world。但是有时候会出现这样的情况

task hello(dependsOn:['b_world','a_world']){    
       println( 'hello' )
}
task a_world(){    
       println( 'a world' )
}
task b_world(){    
       println( 'b world' )
}

对于这种情况 task hello 肯定会后执行的,那么 task a_worldtask b_world 谁会先执行呢,这个问题Gradle的开发小组也在研究,默认的情况是按照字母顺序,现在也没有给出太好的解决方案。

➜  gradle hello
hello
a world
b world   //以上是配置阶段的打印
:a_world UP-TO-DATE // a_world 任务执行
:b_world UP-TO-DATE // b_world 任务执行
:hello UP-TO-DATE     // hello 任务执行

BUILD SUCCESSFUL

PS:如果搜索这个问题可以看到很多方案,但是最简单直接的还是加个字母前缀。

特殊的任务!!插件

举个例子,我们要把一堆java文件打包成jar包怎么办?这里面有个非常的核心的任务,我们暂时是无法完成的,把java代码编译成.class文件.

假如已经有了一个Task叫"build"可以把.java文件编译成 .class文件,那么剩下的事情就简单了。只要再把编译好的.class文件打包成.jar包即可(zip包)。

再或者我们要把一堆.java文件和资源文件打包成.apk文件,那该怎么办 ?

所幸Gradle的设计可以很方便的支持插件来完成这些特殊的需求(自己也可以写插件)。比如打Android的apk文件,只需要这样

apply plugin: 'com.android.application'

android {
    compileSdkVersion 22
    buildToolsVersion "22.0.1"

    defaultConfig {
        applicationId "com.example.ntop.myapplication"
        minSdkVersion 15
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
        }
    }
}

简单配置一下,就可以用Gradle的Android插件来打包Apk了。apply plugin: 'com.android.application' 这句话是配置Android插件。android {..} 闭包中配置是Android插件要求的必须得配置。 而我们如果有自己的自定义行为,可以查看插件提供的任务,自己调度就可以了。

查看已经存在的任务执行 gradle tasks 即可,在AndroidStudio上有个窗口可以直接查看目前工程中包含的任务。


as_tasks_Image.png

SDK打包实践

这里 可以下载到反馈组件的SDK,大概结构如下:

➜  tree -L 2
.
├── example
│   ├── build.gradle
│   ├── example.iml
│   ├── proguard-rules.pro
│   └── src
├── libs
│   ├── armeabi
│   ├── armeabi-v7a
│   ├── com.umeng.fb.5.3.0.jar
│   ├── mips
│   └── x86
├── releasenote.txt
└── res
    ├── anim
    ├── drawable
    ├── drawable-xhdpi
    ├── layout
    ├── values
    └── values-zh

主要是三个目录example目录提供Demo程序,libs目录提供开发者需要的jar包,res目录提供资源文件。设定流程如下:

  1. 编译工程(源代码)导出jar文件
  2. 复制"example"工程到"outputs/example"目录
  3. 复制jar文件到"outputs/libs"目录
  4. 复制资源文件到"outputs/res"目录
  5. 把 "outputs" 目录压缩成 zip 文件

关键的一步是如何导出jar文件,在研究完 Gradle 的 Android 插件之后发现我们需要的是 build 任务,执行这个任务时,Android会生成apk(对于lib工程会生成aar文件,可以从aar文件中解析拿到jar文件)。

这样代码就清晰了

//Caution: gradle clean dabao
task dabao(type:Zip, dependsOn:build) { 
    destinationDir = file('outputs') 
    duplicatesStrategy = 'exclude' 
    archiveName = 'com.umeng.fb.' + android.defaultConfig.versionName + '.zip' 

    from('/'){ 
        include 'releasenote.txt' 
    } 
    into('example') { 
        from '../example' exclude '**/build/**' 
    } 
    from('src/main/res'){ 
        into 'res' 
    } 
    from('libs'){ 
        into 'libs' 
    } 
    from(zipTree('build/outputs/aar/sdk-release.aar')){ 
       include 'classes.jar' 
       rename 'classes.jar','com.umeng.fb.' + android.defaultConfig.versionName + '.jar' 
       into 'libs' 
    }
}

如你所见,只需要20行左右的代码就可以完成打包了。(也可以从"build/intermediates/bundles/release"下面找到jar包 )