- Prologue
- Installation
- Download Dependencies
- Gradle & Gradle plugin
- Build Types
- Product Flavors
- FlavorDimensions
- Filter variants
- Source Sets
- Declare Dependencies
- SigningConfigs
- Launch app in gradle
- Build specified module
- Epilogue
Prologue
欢迎纠错,欢迎推荐,止转载!
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:
gradle-wrapper.jar是执行下载任务的jar包:
通过执行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
- 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失败:
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的选项:
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>
因为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
.
更多细节就留给大家自行探索了,蛮有意思的. - 使用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生效,亲测).这样一来所有的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报告. - 前面我们说了flavorDimensions内部各dimension之间有优先级,定义在前面的dimension包含的flavor对应的source set会被先使用,定义在后边的dimension因为后使用从而覆盖了前面的dimension相同的文件或者属性,使得优先级低的dimension最终在flavorDimensions间生效.
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来行的代码耗费了近两个小时,几乎所有的坑踩了个遍,诶 就不细说了.
欢迎个人或者商业使用,但请注明出处.@明子,偷懒秘诀哟.
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账号)共同探讨.
另外,鉴于能力所限,若有不妥之处,请不吝指正.