Gradle 基础必知必会,一篇带你搞定
Gradle 爬坑指南 – 导论
百度App技术 - Gradle 与 Android 构建入门
Android Gradle (一)Gradle的Android插件入门
Android Gradle (二)签名配置和依赖管理
补齐Android技能树「Gradle篇」专栏
Gradle是一个框架,作为框架,它负责定义流程和规则。而具体的编译工作则是通过插件的方式来完成的。比如编译Java有Java插件,编译Groovy有Groovy插件,编译Android APP有Android APP插件,编译Android Library有Android Library插件。Gradle中每一个待编译的工程都是一个Project,一个具体的编译过程是由一个一个的Task来定义和执行的。一个Project到底包含多少个Task,其实是由编译脚本指定的插件决定。插件是什么呢?插件就是用来定义Task,并具体执行这些Task的东西
参与项目构建的第三方代码叫 --> 插件
参与代码逻辑的第三方代码叫 --> 依赖
Gradle 构建工具在不同场景下会分别使用3个 JVM 进程:
1.client 进程
client 进程是个轻量级进程,每次构建开始都会创建这个进程,构建结束会销毁这个进程。client 进程的任务是查找并和 Daemon 进程通信:
像 gradle.properties 里面设置的参数,全局 init.gradle 初始化脚本的任务这些都需要 client 进程传递给 Daemon 进程
2.Daemon 进程
gradle --status
命令可以查看已启动的 daemon 进程情况:
➜ ~ jps
39554 KotlinCompileDaemon
39509 GradleDaemon
39608
39675 Jps
➜ ~ gradle --status
PID STATUS INFO
39509 IDLE 6.6.1
// INFO 是 gradle 版本号
// Kotlin 语言编写的 Gradle 脚本需要一个新的 daemon 进程出来
wrapper 进程
wrapper 进程啥也不干,不参与项目构建,唯一任务就是负责下载管理 Gradle 版本。我们导入 Gradle 项目进来,client 进程发现所需版本的 Gradle 本机没有,那么就会启动 wrapper 进程,根据 gradle.properties 里面的参数去自行 gradle-wrapper.jar 里面的下载程序去下载 Gradle 文件
Gradle中,每一个待编译的工程都叫一个Project。每一个Project在构建的时候都包含一系列的Task。比如一个Android APK的编译可能包含:Java源码编译Task、资源编译Task、JNI编译Task、lint检查Task、打包生成APK的Task、签名Task等。一个Project到底包含多少个Task,其实是由编译脚本指定的插件决定。插件是什么呢?插件就是用来定义Task,并具体执行这些Task的东西
导入插件
插入插件,我们先要导入仓库,也就声明从哪些仓库查找插件和远程依赖,然后再导入插件。只要我们在根项目的 build.gradle 构建脚本中声明导入的插件,那么所有子项目就都可以使用该插件了
根项目 build.gradle 导入插件
buildscript {
repositories {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
}
}
allprojects {
repositories {
google()
jcenter()
}
}
子项目使用插件
app build.gradle -->
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
....
defaultConfig {
....
}
buildTypes {
....
}
}
dependencies {
....
}
其中几个 {…} 闭包解释下:
apply 就是查找插件中的 apply 方法并调用他就是这么简单,gradle 会帮你实例化这么个类,并调用他的方法
buildscript {
repositories {
mavenCentral()
google()
}
dependencies {
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
}
}
apply plugin: 'com.jakewharton.butterknife'
apply plugin ‘com.jakewharton.butterknife’ 其实就是调用
com.jakewharton:butterknife-gradle-plugin:10.2.3
包中的com.jakewharton.butterknife
类的apply方法。
如下所示 自定义gradle插件
class GreetingPlugin implements Plugin<Project> {
void apply(Project project) {
project.task('hello') {
doLast {
println 'Hello from the GreetingPlugin'
}
}
}
}
// Apply the plugin
apply plugin: GreetingPlugin
获取构建信息的命令
// 1、按自顶向下的结构列出子项目的名称列表
./gradlew projects
// 2、分类列出项目中所有的任务
./gradlew tasks
// 3、列出项目的依赖列表
./gradlew dependencies
查看Android Gradle 依赖树
./gradlew :app:dependencies > dependencies.txt
./gradlew app:dependencies --configuration releaseCompileClasspath > dependencies.txt //显示release模式下编译过程中的依赖树
依赖分类
Gradle 依赖分:直接依赖
,项目依赖
,本地 jar
、aar 依赖
,传递依赖
,这几个总规要分清楚的,多少还是有些差别的
implementation 'androidx.core:core-ktx:1.3.2'
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
--> 内部依赖了
implementation "io.reactivex.rxjava2:rxjava:x.x.x"
jar: 只有class文件及manifest,不包含资源文件;
arr:包含jar包跟资源文件,生成需要mave插件的支持;
model:完整的AS项目;
jar
添加依赖的方式一般就是这2种了,基本上大家都是直接放到 libs 里的,一个个写太费事
implementation files('hibernate.jar', 'libs/spring.jar')
implementation fileTree(dir: 'libs', include: ['*.jar'])
对于 Gradle Android Plugin 来说,会把 jar 看成一种本地代码资源,implementation fileTree() 是声明本地 java 资源路径。
so 文件
.so 文件和 .java 一样,会被 Gradle 看成一种本地代码资源,只要设置 .so 资源的路径即可
一般有2种方式:
一个是按照 android 插件默认的文件路径放置 .so 文件,这样 android 插件会按照默认的路径查找,引入 so 文件
一个是我们手动设置 .so 资源路径
// 设置 .so 资源路径
android{
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
需要注意的是,so 文件需要放到具体的 ABI 目录下,不能直接放 libs 目录下,一般都是这样的
x86/x86_64/armeabi-v7a/arm64-v8a 这些叫 ABI,每一种都对应一种 CPU 指令集 ,在 defaultConfig{…} 中设置使用哪些 ABI
android{
defaultConfig {
ndk {
abiFilters "armeabi", "armeabi-v7a", "x86", "mips"
}
}
}
远程依赖
Gradle 没有自己的远程仓库,用的是 Maven,Jcenter,Jvy 这些库,所以添加远程依赖,要先声明使用哪个远程仓库
每个 Gradle 脚本都要声明使用的远程仓库,所以根目录脚本才会用 allprojects{…} 给每个子项目都声明远程仓库地址
buildscript {
ext.kotlin_version = "1.4.10"
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
allprojects {
repositories {
google()
jcenter()
}
}
aar 文件
Gradle 把 aar 文件一样视为远程依赖,所以同样要在 repositories{…} 里声明 aar 文件仓库地址,不过这个仓库的设置就有些麻烦、复杂了
若是 app module 依赖本地 aar,这么写就行了
android{
...
}
// 声明本地 aar 文件地址
repositories {
flatDir {
dirs 'libs'
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
// 添加本地 aar 文件依赖
implementation(name: 'easeui', ext: 'aar')
但要是 lib 这样的,给别的 module 提供依赖的 module,依赖 aar 文件的话,必须在整个依赖链条上所有的 module 都写上这个 aar 文件所在的仓库地址,一般这种情况,我们都是在根目录脚本 allprojects{…} 里添加 aar 仓库,注意这里 aar 仓库的地址淂写 aar 文件所在 module 这一层级的地址
allprojects {
repositories {
google()
jcenter()
flatDir {
dirs '../animal/libs'
}
}
}
implementation(name: 'easeui', ext: 'aar')
图示如下:app 依赖 lib module --> lib module 依赖 animal module --> animal module 中有 aar 依赖 easeui.arr
或者呢你要是嫌麻烦,可以在项目根目录下建一个 libs 文件夹,所有的 aar 都放到里面,然后在跟脚本统一设置下
compileClasspath --> 中包含的代码、类库,是我们在编写代码时能使用到的类库。这个 path 里要是没有我们想要调的类库,即便我们把远程依赖代码下下来也没用
runtimeClasspath --> 中包含的代码、类库,是 app 跑起来之后能找到的类库。即便我们把远程依赖代码下下来并写到 compileClasspath 里了,我们写代码时可以调用对应的类库,但只要没写到 runtimeClasspath 里面去,app 跑起来一样还是会找不到类库,会 crash 的
api会传递依赖
举例:A依赖B、B依赖C,A、B、C 都是项目 module
1.若 A implementation B,B implementation C,
那么 在 B 中 C 的类库可以随便用
在 A 中就不能使用 C 的类库,IED 提示找不到对应的类
这是因为 implementation 引入的依赖,会把 C 加入 B 的 compileClasspath 和 runtimeClasspath,会把 C 加入 A 的 runtimeClasspath
因为没有加入 A 的 compileClasspath,所以在 A 项目中使用不了对应的代码,但是因为加入了 A 的 runtimeClasspath,则会把 C 的类库打包进 APK 中
2.若 A implementation B,B api C,那么
若 B compileOnly C,则 A 无法调用 C 的代码,且 C 的代码不会被打包到 APK 中
若 B runtimeOnly C,则 A、B 都无法调用 C 的代码,但 C 的代码会被打包到 APK 中
依赖决议是指 “在编译过程中, 如果存在某个依赖的多个版本, 构建系统应该选择哪个进行构建的问题”
不过 Gradle 自有其处理方法,不考虑其他强制设置版本号的手段,就说默认。默认下,Gradle 会使用最新版本的远程依赖。
isFoce 标记会强制使用该标记版本的依赖
dependencies {
implementation("io.reactivex.rxjava2:rxjava:2.2.6") {
isForce = true
}
implementation("io.reactivex.rxjava2:rxjava:2.2.10")
}
// 依赖决议使用 2.2.6 版本
注意:
isForce 出现的非常早,实际使用起来问题很多,因书写不当很容易造成构建失败,现在建议不要再使用了
isForce 最大的问题是这个:
force 的版本不能比 app 主模块的版本低,要不就报错。比如 app 模块依赖于 lib, 如果 app 中引入了 rxjava:2.2.10, lib 却 force 了 rxjava:2.2.6 则会发生编译错误
遇到这个问题:
strictly 是一种强力依赖版本约束,官方现在推荐使用这个,可以用 !!
简写,这样就会强制使用 2.2.0 版本的依赖了
dependencies {
implementation("io.reactivex.rxjava2:rxjava") {
version {
strictly("2.2.0")
}
}
// 等价于
implementation("io.reactivex.rxjava2:rxjava:2.2.0!!")
}
constraints 是 strictly 新版本的替代,东西和 strictly 是一样的
dependencies {
implementation("io.reactivex.rxjava2:rxjava:2.2.6!!")
constraints {
implementation("io.reactivex.rxjava2:rxjava:2.2.6!!")
}
}
但是同样也有问题,constraints 的版本不能比其他的低,要不也会报错
上面说过 .jar 文件会以代码的方式添加到最终构建产物中,aar 会直接包含 .jar 中的代码。要是本地项目和远程依赖都引入了同一个 .jar 的话会冲突的,会提示有相同包名+类名冲突的资源,这里的话可以在方便的位置用 compileOnly 代替 implementation
Gradle 在 implementation{…} 中提供了 exclude 设置,可以忽略指定的依赖,被忽略的依赖将被视为从来没有依赖过
先熟悉下 group、module、version 指的都是哪部分
implementation("io.reactivex.rxjava2:rxandroid:2.1.1")
group = io.reactivex.rxjava2
module = rxandroid
version = 2.1.1
一般我们可以这么写
implementation('org.hibernate:hibernate:3.1') {
exclude module: 'cglib'
exclude group: 'org.jmock'
exclude group: 'org.unwanted', module: 'iAmBuggy'
}
远程依赖冲突一般我们通过 exclude、force、transitive 解决冲突,其中 force 已经不推荐使用了,下面提供一些范例:
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
def requested = details.requested
if (requested.group == 'com.android.support') {
if (!requested.name.startsWith("multidex")) {
details.useVersion '26.1.0'
}
}
}
}
前文我用 implementation、api 来讲解 compileClasspath、runtimeClasspath,A依赖B、B依赖C,C作为依赖会传递给A。Gradle 就是这么一层层、逐级的合并 path,才能最终知道哪些代码应该打进 APK 文件中,即便这会带来依赖冲突、性能损耗的问题,但是正是这种机制,才能使得代码齐全,不会缺少
我们 implementation 一个远程依赖,依赖传递默认是启动的。当然依赖传递也有操作余地,系统提供了 transitive 让我们自己选择是不是要把依赖传递下去
导入远程依赖,transitive 默认是 true 的,就像下面一样
implementation("io.reactivex.rxjava2:rxandroid:2.1.1"){
transitive(true)
}
现在我们把 transitive 写成 false 看看
例:app implementation libs、libs api rxandroid,这样我们在 app 中也能够使用 rxandroid 的 API,我们给 libs api rxandroid 设置 transitive(false) 后,app 中就找不到 rxandroid 的类库了,因为依赖无法被传递了
api("io.reactivex.rxjava2:rxandroid:2.1.1"){
transitive(false)
}
Gradle 中声明的 manifestPlaceholders 参数,在 AndroidManifest.xml 中可以通过 $hostName 的方式引用到
manifestPlaceholders 可以写在 Gradle 脚本 android{…} DSL 中,常见的应用位置:
defaultConfig {...}、productFlavors{...}、buildTypes{...}
android {
defaultConfig {
manifestPlaceholders(["hostName":"www.example.com"])
}
}
可以在配置文件中的任何位置使用
<?xml .....>
<manifest ....>
<application ....>
<activity ....>
<intent-filter>
<data android:scheme="http" android:host="${hostName}"/>
</intent-filter>
</...>
</...>
</...>
buildConfigField 方法会在生成 BuildConfig.java 时, 向其中插入该属性。日志模块我们就是依托这个参数,来统一控制 release 版本不打印日志
android {
defaultConfig {
buildConfigField("int", "i", "1234")
buildConfigField("String", "str", "\"some text\"")
buildConfigField("boolean", "isRelease", "true")
}
}
可以看到 build 后,BuildConfig 中有这个参数了
resValue 比大家可能见的少,用来向 res/values/strings.xml 中添加数据
android {
defaultConfig {
resValue("string", "app_name_test", "some text")
}
}
defaultConfig {
// 资源打包过滤
resConfigs("en", "ldltr")
}
resConfigs(…) 是系统资源过滤,同一类型的资源,只有 resConfigs{…} 声明的配置才能打入 APK 中,可以用在:defaultConfig {…}、productFlavors{…}、buildTypes{…} 中
debugImplementation ('me.ele:uetool:1.0.15'){
exclude group: 'com.android.support', module: 'support-v7'
}
debugImplementation ('me.ele:uetool:1.0.15'){
exclude group: 'com.android.support'
}
更加方便的方式
有时候,乱七八糟的依赖过多,可以使用如下方案:
在 app的module中:
android{
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
def requested = details.requested
if (requested.group == 'com.android.support') {
if (requested.name.startsWith("appcompat-v7")) {
details.useVersion '25.3.0'
}
if (requested.name.startsWith("appcompat-v4")) {
details.useVersion '25.3.0'
}
if (requested.name.startsWith("recyclerview-v7")) {
details.useVersion '25.3.0'
}
}
}
}
}
这样可以强制使用某个版本,不用再一个个去过滤了。
ext{…} 这个 DSL 代码段也是 Project 对象提供的方法。ext{…} 大家都不陌生吧,都是用来做全局参数、依赖的配置。ext{…} 是 Gradle 提供的、让我们定义所需全局变量的代码块,简称:扩展属性
一般我们在根项目脚本中写 ext{…}
// project build.gradle
ext {
tag = "BB"
age = 2
}
大家注意这2种使用方式自动提示的环节,很多人抱怨没有代码提示好难用
// app build.gradle
// 方式1:直接使用,sync 之后出现自动提示
println( "name = $tag" )
println( "age = $age" )
// 方式1:借助 rootProject 对象,rebuild 之后出现自动提示
println( "name = ${rootProject.ext.tag}" )
println( "age = ${rootProject.ext.age}" )
1 – 抽象公共配置脚本
这个好理解,ext{…} 写在根目录脚本里有的人说应该拆分、专人专事,于是我们专门写一个 ext{…} 的脚本出来,该脚本一般都叫:config.gradle,然后 apple from 导入本目录脚本就能用了
ext{…} 编译过后是 Project 对象中的一个成员属性,ext{…} 中一般我们都是写 Map 来配置一些属性
// config.gradle
ext {
// 不同的 DSL 配置块,推荐专门写一个 map 出来,这样方便查找
android = [
compileSdkVersion: 28,
buildToolsVersion: "29.0.1",
applicationId : "com.example.androidstudydemo",
minSdkVersion : 21,
targetSdkVersion : 29,
versionCode : 1,
versionName : "1.0"
]
}
// root build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply from: 'config.gradle'
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
// app build.gradle
android {
compileSdkVersion rootProject.ext.android.compileSdkVersion
defaultConfig {
applicationId rootProject.ext.android.applicationId
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode rootProject.ext.android.versionCode
versionName rootProject.ext.android.versionName
}
}
这个也不难理解,我们的项目要是有多个 子项目 ,每个子项目中 重复的脚本配置 写起来也是让人讨厌的事,尤其是休休改改的情况下很讨厌的,我们应该延续 java 相面对象中的思路:一处修改,处处使用
这里我们需创建脚本基类,该脚本一般都叫:base_build.gradle,也是通过 apple from 导入子项目脚本就可以了
// base_build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion rootProject.ext.android.compileSdkVersion
defaultConfig {
applicationId rootProject.ext.android.applicationId
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode rootProject.ext.android.versionCode
versionName rootProject.ext.android.versionName
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:support-v4:28.0.0'
implementation 'com.android.support:appcompat-v7:28.0.0'
}
// app build.gradle
apply from: rootProject.file("base_build.gradle")
dependencies {
implementation 'com.android.volley:volley:1.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.alibaba:fastjson:1.2.60'
implementation 'org.greenrobot:eventbus:3.1.1'
implementation 'com.squareup.okhttp3:okhttp:3.8.0'
implementation 'com.squareup.retrofit2:retrofit:parent-2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.2'
implementation 'io.reactivex:rxjava:1.2.1'
implementation 'io.reactivex:rxandroid:1.0.1'
implementation 'com.jakewharton.rxbinding:rxbinding:0.4.0'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:2.2'
implementation 'cat.ereza:customactivityoncrash:2.2.0'
implementation 'com.github.bumptech.glide:glide:3.7.0'
}
Android Studio项目版本号的统一管理
Android studio中的BuildConfig类
开发中常用的包有jar包 arr包,还有model,区别如下:
jar: 只有class文件及manifest,不包含资源文件;
arr:包含jar包跟资源文件,生成需要mave插件的支持;
model:完整的AS项目;
allprojects {
repositories {
jcenter()
maven { url 'https://maven.google.com' }
// OR
google()
}
}
google()存储库是Google maven存储库的快捷方式.它是在Gradle 4.x中引入的.使用的实际存储库URL是https://dl.google.com/dl/android/maven2/
,和 https://maven.google.com
实际指向同一存储库.
Android多渠道productFlavors同时开发两个类似的app
Android 中实现差异化打包权威指南
Android Maven 打包不同的Flavors 的aar文件
maven gradle插件如何上传多渠道library
pom文件
productFlavors {
all {
...
}
sdk {
...
}
}
apply plugin: 'maven'
import org.gradle.api.internal.artifacts.publish.DefaultPublishArtifact
//导入类
configurations {
sdkArchives
AllArchives
}
artifacts {
AllArchives new DefaultPublishArtifact("all", "aar", "aar", null, new Date(),
new File("build/outputs/aar", "Zinyan-all-release.aar"))
sdkArchives new DefaultPublishArtifact("sdk", "aar", "aar", null, new Date(),
new File("build/outputs/aar", "Zinyan-sdk-release.aar")) //这个aar文件的名称> 你需要根据你的本地文件进行修改
}
//全部SDK
task uploadAll(type: Upload, group: "upload", dependsOn: 'assembleAll') { // assembleAll 就会帮我们生成相关的aar文件的task。 Gradle/XXX/Tasks/build/assembleXXXX 你自己的项目进行选择。
setConfiguration(project.configurations.getByName("AllArchives"))
repositories.mavenDeployer {
def mavenDirPath = file('D:\\Zinyan\\mavenLib')
repository(url: "file://${mavenDirPath.absolutePath}")
pom.project {
groupId "com.zinyan.dev" // 包名
artifactId "all" // module的名字
version '1.0.0'// 版本号
}
// 特殊的dependencies手动添加 例: internalImplementation 'com.tencent.tbs:tbssdk:44165'
dependencies{
dependency {
groupId "com.tencent.tbs"
artifactId "tbssdk"
version "44165"
}
}
//如果不加下面的pom文件就不正确
pom.withXml {
def dependenciesNode = asNode().appendNode("dependencies")
configurations.implementation.allDependencies.forEach() {
Dependency dependency ->
if (dependency.version != "unspecified" && dependency.name != "unspecified") {
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', dependency.group)
dependencyNode.appendNode('artifactId', dependency.name)
dependencyNode.appendNode('version', dependency.version)
}
}
}
}
}
//第二个SDK
task uploadSdk(type: Upload, group: "upload", dependsOn: 'assembleSdk') {
setConfiguration(project.configurations.getByName("sdkArchives"))
repositories.mavenDeployer {
def mavenDirPath = file('D:\\Zinyan\\mavenLib')
repository(url: "file://${mavenDirPath.absolutePath}")
pom.project {
groupId "com.zinyan.dev" // 包名
artifactId "sdk" // module的名字
version '1.0.0'// 版本号
}
pom.withXml {
def dependenciesNode = asNode().appendNode("dependencies")
configurations.implementation.allDependencies.forEach() {
Dependency dependency ->
if (dependency.version != "unspecified" && dependency.name != "unspecified") {
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', dependency.group)
dependencyNode.appendNode('artifactId', dependency.name)
dependencyNode.appendNode('version', dependency.version)
}
}
}
}
}
Gradle插件开发系列之gradle插件调试方法
直接在gradle窗口选择任务直接右键debug
组件化下如何优雅进行本地调试,即aar依赖与module依赖动态切换
aar和源码切换插件Plus