Gradle

Posted on By ᵇᵒ

Prologue

欢迎纠错,欢迎推荐,:u7981:止转载!

Installation

gradle安装包官网下载地址https://services.gradle.org/distributions/.
安装gradle有两种方式:

  • 手动下载存放本地即可.
  • 通过Android Studio项目里的gradle wrapper.
    gradle wrapper包含几个以下几个重要文件:
    • gradlew (Unix Shell script)
    • gradlew.bat (Windows batch file)
    • gradle/wrapper/gradle-wrapper.jar (Wrapper JAR)
    • gradle/wrapper/gradle-wrapper.properties (Wrapper properties)

    其中文件gradle-wrapper.properties指定要下载的gradle版本地址以及存放路劲:

    #Fri Jul 07 21:03:26 CST 2017
    distributionBase=GRADLE_USER_HOME
    distributionPath=wrapper/dists
    zipStoreBase=GRADLE_USER_HOME
    zipStorePath=wrapper/dists
    distributionUrl=https\://services.gradle.org/distributions/gradle-3.5.1-bin.zip
    
    

    一般来说GRADLE_USER_HOME = ~/.gradle,也就是下载下来的gradle都存放在:~/.gradle/wrapper/dists下面。

    该文件指定的版本号同时对应Project Structure -> Project -> Gradle version:
    /styles/images/gradle/gradle-wrapper-properties.png

    gradle-wrapper.jar是执行下载任务的jar包:
    /styles/images/gradle/gradle-wrapper-jar.png
    通过执行task: gradle wrapper 就可以把指定版本的gradle下载下来,默认存放在本地目录:$USER_HOME/.gradle/wrapper/dists.
    当然也可以命令指定版本号(以版本3.5为例):gradle wrapper --gradle-version 3.5,一般默认下载都是最小发行版bin包,但是Android Studio会下载带src的全包,可以在命令里制定下载类型,只需要添加参数:–distribution-type,甚至还可以指定下载url,使用参数–gradle-distribution-url.

以上两种Gradle安装方式对应着Android Studio -> Settings -> Gradle -> Project-level settings /styles/images/gradle/settings-gradle.png

  • User local gradle distribution: 选择该选项,填入手动下载的gradle存放路劲.
  • Use default gradle wrapper(recommended): 选择该项,Android Studio会自动执行task(gradle wrapper)去下载gradle(如果指定存放路劲下没有).这下知道为什么新建一个项目会卡在初始化界面半天了哇,因为默认就是这个选项.

Download Dependencies

当我们修改了module的build.gradle脚本的时候,Android Studio会自动提示sync,sync的作用就是对我们的修改做出相应变化使之生效.其中最主要做的一件事就是下载依赖包.如果我们在dependencies{ }模块添加了依赖,同时本地对应的gradle路劲又没有该依赖,那么sync的时候gradle就会去网络上下载,下载的地址基于项目一级的build.gradle脚本:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

Ivy、Maven 和 Gradle 管理库和依赖包都是这种方式,公司提前把库存放在网络的仓库里(比如jcenter maven),对于需要依赖包的项目再从网络上下载.
这种工作方式的前提是Android Studio -> Settings -> Gradle -> Global Gradle Settings没有勾选Offline work(具体位置参考前面Project-level settings示例一图),如果勾选了Offline work,指定本地路劲作为Service directory path,则gradle sync的时候不会从网络下载对应的库和依赖包,而是去Offline work指定的路劲找.如果指定的路劲找不到,sync失败.
当然,我也不知道Google给这么个选项在什么情况下勾选使用,谨慎猜测是为了个人使用一些不方便上传网络的私有库,毕竟如果只是为了保密,公司可以选择自己搭个依赖包的私有服务器,然后把build.gradle脚本的jcenter换成自己的服务器地址,服务器保存私有库的同时定期更新jcenter的仓库.
总之,如果不懂不要理会这个选项,默认不勾选就好.勾选了很可能就sync失败:
/styles/images/gradle/gradle-offline-error.png

Gradle & Gradle plugin

继续分析上面project的build.gradle脚本,这个脚本里包含三个模块.
先说最简单的第三个:

task clean(type: Delete) {
    delete rootProject.buildDir
}

一眼就能看出是在设置任务clean的时候删除project根目录下的build文件夹.

再看第一个模块buildscript:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

这个模块用于指定gradle自身build所需的依赖以及下载地址.
有点懵逼哈,其实很好理解,gradle这个软件在build的时候自身也是需要从网络下载依赖包的,这里依赖的是gradle plugin(官方的说法是Android plugin for gradle).

同样,这个被依赖的gradle plugin也会被下载到本地,保存在$ADNROID_STUDIO_HOME/gradle/m2repository/com/android/tools/build/gradle/目录下.
每一个Android studio目录下都会默认自带一个gradle文件夹,该文件夹下包含一个默认版本的gradle以及m2repository,后续编译中用到的gradle plugin就保存在m2repository里,所以该文件夹不可以随意移动.

而module的build.gradle脚本里配置的依赖库下载存放位置:~/.gradle/caches/modules-2/files-2.1

每一版本gradle依赖的gradle plugin版本号都是有范围的,或者说gradle plugin的每一次版本升级都只会适配较新版本的gradle,两者具体的版本匹配关系可参考:
https://developer.android.google.cn/studio/releases/gradle-plugin.html#updating-plugin.
http://tools.android.com/build/gradleplugin

第二个模块:

allprojects {
    repositories {
        jcenter()
    }
}

这里就指定了所有项目所需依赖的下载地址(具体需要的依赖包声明在了module的build.gradle脚本里),如果下载依赖使用私有的库服务器,修改这里的地址.
另外,这里不一定需要对所有项目设置,也可以只针对当前项目设置.

这两个模块同时也对应Project Structure -> Project的选项:
/styles/images/gradle/module-build-gradle.png

Build Types

当创建一个新的module的时候,Android Studio会在module的build.gradle里自动创建debug和release build types.debug build type没有显示出来,默认包含属性debuggable true,采用android通用的debug keystore(位于home/.android/目录)签名,我们也可以显示地把它写出来并加以修改.

android {
    ...
    defaultConfig {...}
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }

        debug {
            applicationIdSuffix ".debug"
        }

        jnidebug {

            // This copies the debuggable attribute and debug signing configurations.
            initWith debug

            applicationIdSuffix ".jnidebug"
            jniDebuggable true
        }
    }
}

以上是一个build type配置示例,注意这里的”initWith”属性允许我们复制其他build type的配置,然后配置想改变的设置(当然也可以新增),这样就不用从头开始写了.借鉴了java的继承思想.
更多build type属性请参考官方文档:
http://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.BuildType.html

Product Flavors

配置flavor和配置build types有点类似,所有的配置项都添加在productFlavors模块,product flavors支持所有的defaultConfig属性(因为所有的procut flavors都默认继承了defaultConfig),所以我们可以在defaultConfig模块里配置所有flavor的基础属性,然后在flavor各自模块里进行修改和新增.

android {
    ...
    defaultConfig {...}
    buildTypes {...}
    productFlavors {
        demo {
            applicationIdSuffix ".demo"
            versionNameSuffix "-demo"
        }
        full {
            applicationIdSuffix ".full"
            versionNameSuffix "-full"
        }
    }
}

以上是一个productFlavors的配置示例(这里列举了两个flavor:demo和full),更多flavor属性请参考:
http://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.ProductFlavor.html

创建好flavor并sync,Android Studio会基于build types和flavor自动创建build variant(默认位于Android Studio的左侧边栏,也可以通过上方菜单栏的Build -> Select Build Variant调出),命名方式为<product-flavor><Build-Type>
/styles/images/gradle/gradle-build-variant.png
因为Android Studio的原因,build variant并不能下拉显示的时候截图,这里的下拉菜单就包含了所有组成的build variants,其实就是flavor和build types的所有交叉组合.
使用IDE编译apk的时候,需要先选择build variant(Android Studio一次只能编译一个build variants).
当然,我们也可以使用gradle命令编译apk,
gradle [clean] <assemble,install><product-flavor><Build-Type>, 比如gradle assembleDemoDebug.
如果省略flavor,则会一次性编译该build type下所有flavor组合的build variant.比如gradle assembleDebug则会把上图中的DemoDebug和FullDebug(build variant不区分大小写)全编译出来.
另外,gradle指令还支持驼峰命名缩写,比如gradle assembleDemoDebug指令可以简写为gradle aDD.

  • gradle assembleDebug指令很特殊,不可以缩写为gradle aD.而gradle assembleRelease可以缩写为gradle aR,甚至还可以写为gradle assembleR.
    如果两个flavor的首字母都一样,那么使用驼峰缩写会怎样呢?
    答案是这个维度的flavor必须用全称,而其他维度可以区分的flavor依然可以继续缩写首字母(包括build type),只是我一直没搞懂assembleDebug的缩写到底和什么冲突了导致不能缩写.
    又如果flavor名字和build type名字一样,会怎样呢(估计一般人不会这么吃撑了没事儿干)?
    答案是肯定行不通的,编译会报错ProductFlavor names cannot collide with BuildType names.
    更多细节就留给大家自行探索了,蛮有意思的:smile:.
  • 使用gradle与gradlew(w指wrapper)并没有本质区别,只不过gradle wrapper自动为不同项目选择gradle版本.如果是管理多个项目,且每个项目使用的gradle版本不一样,建议选一个都兼容的gradle版本设为环境变量,使用gradle指令.当然,遇上一些历史项目非得使用老旧的gradle版本,那还是gradlew方便.
    gradlew指令使用方式举例(Windows直接使用gradlew):./gradlew clean,以防权限问题更稳妥的方式是sh gradlew clean.

FlavorDimensions

对大多数人或者说大多数公司来说,flavor+build type完全可以满足不同版本编译需求,但不排除极个别奇葩,在这基础上都还不够,于是FlavorDimensions诞生了(作为开发者来说,程序严谨很重要,不管用不用得到,各种极限情况都得考虑).
其实build type也可以近似看作一种维度,这就是为什么前面我们说flavor配置和build type配置很类似,build variants其实就是各个不同维度之间按维度顺序交叉组合而已,只不过build type这个类似维度有一些特殊的地方,它没有applicationId属性等.

android {
  ...
  buildTypes {
    debug {...}
    release {...}
  }

  flavorDimensions "api", "mode"

  productFlavors {
    demo {
      // Assigns this product flavor to the "mode" flavor dimension.
      dimension "mode"
      ...
    }

    full {
      dimension "mode"
      ...
    }

    minApi24 {
      dimension "api"
      minSdkVersion '24'
      versionCode 30000 + android.defaultConfig.versionCode
      versionNameSuffix "-minApi24"
      ...
    }

    minApi23 {
      dimension "api"
      minSdkVersion '23'
      versionCode 20000  + android.defaultConfig.versionCode
      versionNameSuffix "-minApi23"
      ...
    }

    minApi21 {
      dimension "api"
      minSdkVersion '21'
      versionCode 10000  + android.defaultConfig.versionCode
      versionNameSuffix "-minApi21"
      ...
    }
  }
}
...

这是一个FlavorDimensions的示例,我们通过flavorDimensions定义了两个dimension:”api”和”mode”.
注意,这里定义的两个dimension是有优先级区分的,优先级正比于他们相互间的先后顺序(这一点非常非常重要).
productFlavors模块里的所有flavor必须至少配置一项dimension属性(理论上可以同时配置多个dimension,但只有最高优先级的dimension生效,亲测:smile:).这样一来所有的flavor按dimension区分为不同维度,不同dimension之间按dimension优先级先后顺序交叉组合成build variant(这里为了便于理解,我把build type也近似看做dimension,且永远处于最后,优先级最低):
<productFlavor1><productFlavor2><build type>
例如gradle assembleMinApi24DemoDebug
如果我们在定义flavorDimensions时把mode和api顺序对调,则应该是gradle assembleDemoMinApi24Debug,这就是优先级的重要性,当然优先级的重要性远不止于此,在merge source的时候会有特别影响.

Filter variants

flavor各维度交叉组合build variant讲完了,但是这里有个问题,很多时候我们编译并非全都需要这些build variant,对于某些组合的build variant可能根本就用不上,这对于使用gradle assemble这种指令一次性全编译来说是一种浪费(每多编译一个build variant会增加相应的编译时间,同时生成出来的apk也占空间,不需要的还必须手动清理).
这个时候我们可以使用variantFilter来自动过滤掉那些不需要编译的build variant.

android {
  ...
  buildTypes {...}

  flavorDimensions "api", "mode"
  productFlavors {
    demo {...}
    full {...}
    minApi24 {...}
    minApi23 {...}
    minApi21 {...}
  }

  variantFilter { variant ->
      def names = variant.flavors*.name
      // To check for a certain build type, use variant.buildType.name == "<buildType>"
      // if (variant.buildType.name == "debug"){ setIgnore(true) }
      // if (variant.buildType.name.equals('debug')){ setIgnore(true) }
      if (names.contains("minApi21") && names.contains("demo")) {
          // Gradle ignores any variants that satisfy the conditions above.
          setIgnore(true)
      }
  }
}
...

这个例子又一次验证了build type可近似看作一种维度的思想.

Source Sets

  • src/main/
    所有build variant共有的公共代码和资源.
  • src/buildType/
    特定的build type独有的代码和资源.
  • src/productFlavor/
    特定的product flavor独有的代码和资源.
    提示: 如果配置了flavorDimensions,可以为每一种flavor维度的组合创建一份独有的代码和资源:src/productFlavor1ProductFlavor2/.
  • src/productFlavorBuildType/
    特定的build variant独有的代码和资源.

    每一个source set都可以包含下面所示的部分或者全部文件(这里以debug build type为例):

    Java sources: [app/src/debug/java]
    Manifest file: app/src/debug/AndroidManifest.xml
    Android resources: [app/src/debug/res]
    Assets: [app/src/debug/assets]
    AIDL sources: [app/src/debug/aidl]
    RenderScript sources: [app/src/debug/rs]
    JNI sources: [app/src/debug/jni]
    JNI libraries: [app/src/debug/jniLibs]
    Java-style resources: [app/src/debug/resources]
    

    如果不同的source set含有相同文件的不同版本,gradle会按照如下优先级决定使用哪个文件版本(优先级高→低从左到右,高的覆盖低的):
    build variant > build type > product flavor > main source set > library dependencies

    • 前面我们说了flavorDimensions内部各dimension之间有优先级,定义在前面的dimension包含的flavor对应的source set会被先使用,定义在后边的dimension因为后使用从而覆盖了前面的dimension相同的文件或者属性,使得优先级低的dimension最终在flavorDimensions间生效.
      其实整个source set的优先级也是按使用的先后顺序来确定优先级的,后使用的覆盖替换先使用的,从而拥有source set的高优先级.
    • applicationIdSuffix versionNameSuffix这种属性有点特殊,它属于追加类型,不会被覆盖替换,所以所有source set有定义这类属性的地方都会生效,追加的先后按source set使用的先后顺序.

    source set还有另外一种创建方法,不一定要名字对应build type或者flavor name,可以取别名,然后在sourceSets block里显示声明这些source set包含的文件(或者路劲).
    因为我们编译apk是基于build variant,所以最终目的是编译器为每一个build variant根据规则生成好一份source set就行了.这个生成的结果可以通过task查看,点击Android Studio右侧边栏的Gradle > MyApplication > Tasks > android,双击sourceSets,执行该task,完成后会在IDE底部栏的Gradle Console生成一份所有build variant的source set报告.

Declare Dependencies

可以通过build variant加”compile”关键字为特定的build variant声明依赖.类似地,也可以为testing source set声明特定的依赖.
Gradle 4.0新增了依赖 api指令和implemention指令:

  • “api”完全等同于原来的“compile”。
  • “implemention”的特点是,项目A中有使用implemention指令编译的依赖库a,项目B又依赖于项目A,则项目B将无法访问a中的任何程序,目的就是将项目A的依赖库a隐藏在项目内部。
    这样处理的好处很多,参见What’s the difference between implementation and compile in gradle
    现在一般推荐将“Compile”变更为“implemention”,除非需要将库a暴露给项目B(请使用api指令,“compile”指令已经过时)。
    dependencies {
      // Adds the local "mylibrary" module as a dependency to the "demo" flavor.
      demoCompile project(":mylibrary")
    
      // Adds specific library module dependencies as compile time dependencies
      // to the fullRelease and fullDebug build variants.
      fullReleaseCompile project(path: ':library', configuration: 'release')
      fullDebugCompile project(path: ':library', configuration: 'debug')
    
      // Adds a remote binary dependency only for local tests.
      testCompile 'junit:junit:4.12'
    
      // Adds a remote binary dependency only for the instrumented test APK.
      androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
    }
    

dependecies的语法:

implementation 'com.android.support:appcompat-v7:27.1.1'  
# or
implementation "com.android.support:appcompat-v7:27.1.1"

单引号和双引号皆可;但是如果路径中含有变量,则必须使用双引号,且变量前面添加$符号:

def support = '27.1.1'
implementation "com.android.support:appcompat-v7:$support"  

SigningConfigs

...
android {
    ...
    defaultConfig {...}
    signingConfigs {
        release {
            storeFile file("myreleasekey.keystore")
            storePassword "password"
            keyAlias "MyReleaseKey"
            keyPassword "password"
        }
    }
    buildTypes {
        release {
            ...
            signingConfig signingConfigs.release
        }
    }
}

如果顾虑隐私,可以不用在脚本里记录密码明文,而采用从环境变量获取的方式:

storePassword System.getenv("KSTOREPWD")
keyPassword System.getenv("KEYPWD")

或者每次手动从命令行输入的密码的方式:

storePassword System.console().readLine("\nKeystore password: ")
keyPassword System.console().readLine("\nKey password: ")

手动输入密码的方式可能会出现空指针异常

Cannot invoke method readLine() on null object

这是Android studio默认设置gradle为deamon方式,关于这个bug参考:
https://discuss.gradle.org/t/system-console-when-using-the-daemon/2306

Launch app in gradle

在掌握了gradle的使用之后,你也许会像明子和我一样,不再喜欢使用Android Studio来编译apk.
但是使用gradle命令来编译apk有两个问题:

  • 无法debug,因为debugger是IDE才有的,gradle本身并不自带,所以这个无解,debug就老老实实用IDE吧.
  • 无法启动应用,gradle没有启动应用的指令,最多只能到install,但是我们可以在task install的最后执行adb shell am start来启动应用.上代码:
/**
 * Jul 09, 2017, code by Bob.
 * Any question plz be free contact with me(40080007@qq.com).
 * Android Studio version 2.3.3
 * Gradle version 3.5.1
 * Gradle plugin version 2.3.3
 */
android.applicationVariants.all { variant ->

    // launch after install
    if (variant.install != null) {
        variant.install.doLast {

            // Make the applicationId of last flavor dimension as final applicationId,
            // if it doesn't exist, replace with the applicationId of defaultConfig block.
            // Ignore buildType block cos property applicationId is forbidden inside it.
            def appId = ((variant.productFlavors.applicationId.size > 0) && variant.productFlavors.applicationId[-1]) ? variant.productFlavors.applicationId[-1] : android.defaultConfig.applicationId
            //  def appId = variant.productFlavors.applicationId.size > 0 ? (variant.productFlavors.applicationId[-1] ?: android.defaultConfig.applicationId) : android.defaultConfig.applicationId
            println 'appId:'+appId


            // applicationIdSuffix of defaultConfig block
            def defaultAppIdSuffix = android.defaultConfig.applicationIdSuffix ?: ''

            // applicationIdSuffix of flavor block
            // Combine applicationIdSuffix of all the flavor dimensions in order
            def flavorAppIdSuffix = ''
            variant.productFlavors.applicationIdSuffix.each { suffix ->
                flavorAppIdSuffix += suffix ?: ''
            }

            // applicationIdSuffix of buildType block
            def buildTypeAppIdSuffix = variant.buildType.applicationIdSuffix ?: ''

            // Combine applicationIdSuffix of all these three blocks in order,
            // and we get the final applicationIdSuffix.
            def appIdSuffix = defaultAppIdSuffix + flavorAppIdSuffix + buildTypeAppIdSuffix

            // Combine applicationId with applicationIdSuffix in order,
            // finally we get the package name.
            def packageName = appId + appIdSuffix
            println 'packageName:'+packageName

            // replace launch activity here with yours!!!
            // In addition, you can define this as a variant in defaultConfig block.
            def startupClass = packageName + File.separator + 'com.example.bob.demo.MainActivity'

            exec {
                executable = 'adb'
                args = ['shell', 'am', 'start', '-c', 'android.intent.category.LAUNCHER', '-n', startupClass]
            }
        }
    }
}

这段20来行的代码耗费了近两个小时,几乎所有的坑踩了个遍:sweat_smile:,诶 就不细说了.
欢迎个人或者商业使用,但请注明出处.@明子,偷懒秘诀哟.

Build specified module

对于多module的project,如何指定编译某特定的module(当然 你得保证该module的依赖能编过),有两种方法:

  • 进入该module根目录,再使用gradle assembleRelease之类的编译命令.
    理论上来说使用Android Studio右侧的Gradle task栏也可以找到对应的module task,但是不知道为什么我在Android Studio 3.0.1上测试时其他module还是会一起编译。
  • 在task前指定module名字,如:
    gradle :app:assembleDebug
    gradle :app:clean :app:aR
    ./gradlew :app:assembleDebug
    module名字就是project的settings.gradle文件里include的名字,而不是module文件夹的名字

Epilogue

关于gradle,就到此结束了,如果你还有什么感兴趣的,可以留言(需要登录Github账号)共同探讨.
另外,鉴于能力所限,若有不妥之处,请不吝指正.