Android Gradle
Android项目使用 Gradle 作为构建框架,Gradle 又是以Groovy为脚本语言。所以学习Gradle之前需要先熟悉Groovy脚本语言。
Groovy是基于Java语言的脚本语言,所以它的语法和Java非常相似,但是具有比java更好的灵活性。下面就列举一些和Java的主要区别。
Android Gradle 的 Project 和 Tasks
这个Gradle中最重要的两个概念。每次构建(build)至少由一个project构成,一个project 由一到多个task构成。项目结构中的每个build.gradle文件代表一个project,在这编译脚本文件中可以定义一系列的task;task 本质上又是由一组被顺序执行的Action`对象构成,Action其实是一段代码块,类似于Java中的方法。
Android Gradle 构建生命周期
每次构建的执行本质上执行一系列的Task。某些Task可能依赖其他Task。那些没有依赖的Task总会被最先执行,而且每个Task只会被执行一遍。每次构建的依赖关系是在构建的配置阶段确定的。每次构建分为3个阶段:
这是创建Project阶段,构建工具根据每个build.gradle文件创建出一个Project实例。初始化阶段会执行项目根目录下的settings.gradle文件,来分析哪些项目参与构建。
所以这个文件里面的内容经常是:
include ':app'
include ':libraries'
这是告诉Gradle这些项目需要编译,所以我们引入一些开源的项目的时候,需要在这里填上对应的项目名称,来告诉Gradle这些项目需要参与构建。
这个阶段,通过执行构建脚本来为每个project创建并配置Task。配置阶段会去加载所有参与构建的项目的build.gradle文件,会将每个build.gradle文件实例化为一个Gradle的project对象。然后分析project之间的依赖关系,下载依赖文件,分析project下的task之间的依赖关系。
这是Task真正被执行的阶段,Gradle会根据依赖关系决定哪些Task需要被执行,以及执行的先后顺序。
task是Gradle中的最小执行单元,我们所有的构建,编译,打包,debug,test等都是执行了某一个task,一个project可以有多个task,task之间可以互相依赖。例如我有两个task,taskA和taskB,指定taskA依赖taskB,然后执行taskA,这时会先去执行taskB,taskB执行完毕后在执行taskA。
说到这可能会有疑问,我翻遍了build.gradle也没看见一个task长啥样,有一种被欺骗的赶脚!
其实不是,你点击AndroidStudio右侧的一个Gradle按钮,会打开一个面板,内容差不多是这样的:
里面的每一个条目都是一个task,那这些task是哪来的呢?
一个是根目录下的 build.gradle
中的
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
}
一个是 app 目录下的 build.gradle
中的
apply plugin: 'com.android.application'
这两段代码决定的。也就是说,Gradle提供了一个框架,这个框架有一些运行的机制可以让你完成编译,但是至于怎么编译是由插件决定的。还好Google已经给我们写好了Android对应的Gradle工具,我们使用就可以了。
根目录下的build.gradle中 dependencies { classpath ‘com.android.tools.build:gradle:3.2.1’ } 是Android Gradle编译插件的版本。
app目录下的build.gradle中的apply plugin: ‘com.android.application’ 是引入了Android的应用构建项目,还有com.android.library和com.android.test用来构建library和测试。
所有Android构建需要执行的task都封装在工具里,如果你有一些特殊需求的话,也可以自己写一些task。那么对于开发一个Android应用来说,最关键的部分就是如何来用Android Gradle的插件了。
认知Gradle Wrapper
Android Studio中默认会使用 Gradle Wrapper 而不是直接使用Gradle。命令也是使用gradlew而不是gradle。这是因为gradle针对特定的开发环境的构建脚本,新的gradle可能不能兼容旧版的构建环境。为了解决这个问题,使用Gradle Wrapper 来间接使用 gradle。相当于在外边包裹了一个中间层。对开发者来说,直接使用Gradlew 即可,不需要关心 gradle的版本变化。Gradle Wrapper 会负责下载合适的的gradle版本来构建项目。
Android 三个文件重要的 gradle 文件
Gradle项目有3个重要的文件需要深入理解:项目根目录的 build.gradle , settings.gradle 和模块目录的 build.gradle 。
1.settings.gradle 文件会在构建的 initialization 阶段被执行,它用于告诉构建系统哪些模块需要包含到构建过程中。对于单模块项目, settings.gradle 文件不是必需的。对于多模块项目,如果没有该文件,构建系统就不能知道该用到哪些模块。
2.项目根目录的 build.gradle 文件用来配置针对所有模块的一些属性。它默认包含2个代码块:buildscript{…}和allprojects{…}。前者用于配置构建脚本所用到的代码库和依赖关系,后者用于定义所有模块需要用到的一些公共属性。
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
buildscript:定义了 Android 编译工具的类路径。repositories中, jCenter是一个著名的 Maven 仓库。
allprojects:中定义的属性会被应用到所有 moudle 中,但是为了保证每个项目的独立性,我们一般不会在这里面操作太多共有的东西。
模块级配置文件 build.gradle 针对每个moudle 的配置,如果这里的定义的选项和顶层 build.gradle定义的相同。它有3个重要的代码块:plugin,android 和 dependencies。
定制项目属性(project properties)
在项目根目录的build.gradle配置文件中,我们可以定制适用于所有模块的属性,通过ext 代码块来实现。如下所示:
ext {
compileSdkVersion = 28
buildToolsVersion = "28.0.0"
}
然后我们可以在模块目录的build.gradle配置文件中引用这些属性,引用语法为rootProject.ext.{属性名}。如下:
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
}
Android studio gradle Task
//构建
gradlew app:clean //移除所有的编译输出文件,比如apk
gradlew app:build //构建 app module ,构建任务,相当于同时执行了check任务和assemble任务
//检测
gradlew app:check //执行lint检测编译。
//打包
gradlew app:assemble //可以编译出release包和debug包,可以使用gradlew assembleRelease或者gradlew assembleDebug来单独编译一种包
gradlew app:assembleRelease //app module 打 release 包
gradlew app:assembleDebug //app module 打 debug 包
//安装,卸载
gradlew app:installDebug //安装 app 的 debug 包到手机上
gradlew app:uninstallDebug //卸载手机上 app 的 debug 包
gradlew app:uninstallRelease //卸载手机上 app 的 release 包
gradlew app:uninstallAll //卸载手机上所有 app 的包
这些都是基本的命令,在实际项目中会根据不同的配置,会对这些task 设置不同的依赖。比如 默认的 assmeble 会依赖 assembleDebug 和assembleRelease,如果直接执行assmeble,最后会编译debug,和release 的所有版本出来。如果我们只需要编译debug 版本,我们可以运行assembleDebug。
除此之外还有一些常用的新增的其他命令,比如 install命令,会将编译后的apk 安装到连接的设备。
gradle项目实战
最近参与基础架构组的crashly收集项目,其中一个模块就是收集项目中使用到的插件和sdk的四级(或三级)包路径和混淆后的mapping文件, 然后调用python文件实现文件和数据字段的上传。请求网络上传数据这块python代码量非常少就几行代码的事情,所以我在项目中使用了两个.gradle文件分别扫描项目中使用到的插件和sdk的四级(或三级)包路径,然后在.gradle文件中的task中调用python文件上传数据,这样工作就告一段落,然后当我找QA和负责打包的同事提交我的几个文件时才知道我这做法是多么lowbee, 这样实现需要每个业务线的项目都接入我的几个.gradle文件和.py文件,每次.gradle和.py文件修改在提交上去后各个业务线需要拉去最新的代码,这样的话我需要把各个业务线的项目源码都下载到本地,挨个改一遍在提交,这种做法没有做到通用性和可维护性。在同事的建议下,我又硬着头皮开始研究自定义gradle插件,每个业务线只需要apply插件就可以,这个方案不错,开始折腾gradle插件。
首先说下功能就是定义2个任务,其中一个任务就是扫描业务方的项目过滤出使用到的四级(或三级)包路径,然后生成一个文件连同参数一起使用py脚本上传到后台。
但是这里遇到了2个问题:
1. 在插件module下的python文件在插件中是没法访问的,插件中是很容易拿到业务方的目录,如果将python文件放在业务方的项目中,同样是扩展性不好;
2. 在插件中上传文件和其他参数我引入了第三方库的依赖,执行 uploadArchives 任务将代码上传到本地仓库后,在业务方demo中报错,无法访问到插件中的依赖。
我在技术群里问了几遍结果都没有人回应,仅有的一个同行回复是遇到类似的问题,然后他的建议是去网上下载一个有类似功能的demo,然后修改下逻辑。顺着这条线索我去网上找到第四个才算是试成功了,下面将操作过程重演一遍。
1) 首先新建一个Library库,删掉里面的文件最后保留和新建的文件夹如下图所示:
2) 删掉build.gradle中的内容,最终配置如下所示:
3) 在groovy文件夹下新建包名并在该名下新建文件CrashlyPlugin.groovy, 内容如下:
4) 在gradle-plugins文件夹下新建一个文件com.ke.crashly.plugin.properties, 内容如下:
5) sync一下项目,然后找到开发工具右侧的Gradle下的:crashlyplugin -> Tasks -> upload -> 双击uploadArchives,就会在当前项目下生成一个目录crashlyrepo, 接下来就可以在项目中使用这个插件了;
6) 在工程的根目录下的build.gradle文件下添加如下代码:
7) 在工程的主module的build.gradle文件最后添加如下代码:
8) sync一下工程,同样是在开发工具右侧的Gradle标签下,找到:app -> Tasks -> other -> uploadRepoNameForCrashly控制台就会打印百度首页网页代码,也可以在Terminal下输入命令: gradle task -q uploadRepoNameForCrashly -s.
生命周期函数的应用
project.gradle.buildFinished {
// 可以上传mapping.txt文件
}
project.afterEvaluate {
// 可以获取在build.gradle文件中的自定义的配置信息
it.android.applicationVariants.all { variant ->
// 当存在多个变体时,在这里可以遍历多个变体和applicationId, map的形式保存
// 根据构建模式的信息最终确定applicationId
}
}
project.gradle.startParameter.getTaskNames().each {
// 命令行输入 ./gradlew clean assembleRelease -s
// 会输出 clean assembleRelease
// 可以利用这个信息确定构建模式
}
gradle插件的三种形式
1. 在build.gradle中编写自定义插件
2. buildSrc工程项目
将插件的源代码放在rootProjectDir/buildSrc/src/main/groovy目录中,Gradle会自动识别来完成编译和测试。Android Studio会找rootPrjectDir/buildSrc/src/main/groovy目录下的代码。
坑:当新建libraray module并命名为buildSrc后会提示Plugin with id'com.android.library' not found.
这是因为buildSrc是Android的保留名称,只能作为plugin插件使用,我们修改buildSrc的build.gradle文件后就不报错了,且这个工程的构建时间最早。
3. 独立项目或独立Module
无论是在build.gradle中编写自定义插件,还是buildSrc项目中编写自定义插件,都只能在自己的项目中进行使用。如果想要分享给其他人或者自己用,可以在一个独立的项目中编写插件,这个项目会生成一个包含插件类的JAR文件,有了JAR文件就很容易进行分享了。
这里主要讲一下buildSrc工程项目, 这个工程可以支持多个gradle插件本地调试。
1. 目录结构如下所示
2. buildSrc#build.gradle文件内容
apply plugin: 'groovy'
apply plugin: 'maven'
repositories {
mavenCentral()
}
dependencies{
//gradle sdk
implementation gradleApi()
//groovy sdk
implementation localGroovy()
}
// 如果有多个gradle插件, 在下面添加文件路径
sourceSets {
main {
java.srcDirs = ['src/main/java'
]
groovy.srcDirs = ['src/main/groovy',
'../lifecycle_plugin/src/main/groovy'
]
resources.srcDirs = ['src/main/resources',
'../lifecycle_plugin/src/main/resources'
]
}
}
3. gradle插件的build.gradle文件
apply plugin: 'groovy'
dependencies{
//gradle sdk
implementation gradleApi()
//groovy sdk
implementation localGroovy()
}
//以上配置比较固定
//以下内容主要用来上传插件
apply plugin: 'maven'
repositories {
mavenCentral()
}
// classpath: 'group:name:version' // buildSrc模式下, name为module名称
group = 'com.ke.plugin'
version = '1.0.0-SNAPSHOT'
uploadArchives{
repositories {
mavenDeployer{
repository(url: uri('../repositories')) // 和gradle插件同级目录下
}
}
}
4. gradle插件实现的功能, 拷贝mapping.txt文件到指定目录下
class LifeCyclePlugin implements Plugin {
@Override
void apply(Project project) {
project.extensions.create("pluginConfig", PluginConfig)
// 命令行输入 ./gradlew clean assembleRelease -s
project.gradle.startParameter.taskNames.each {
println("taskName: " + it.toString())
// taskName: clean
// taskName: assembleRelease
}
// writeReleaseApplicationId 在这个tasks里面
project.afterEvaluate {
project.tasks.each {
}
println("mappingPath ---> " + project.extensions.pluginConfig.mappingPath)
}
project.gradle.taskGraph.whenReady {
project.gradle.taskGraph.allTasks.each {
}
}
project.tasks.create(name: "copyMappingFileTask", type: Copy) {
// from(new File('build/outputs/mapping/release/mapping.txt'))
from(new File("build/outputs/mapping"))
include '*/mapping.txt'
into "build/output/mapping"
}
project.task('copyMappingFile') {
doLast {
try {
File resultFile = new File(project.buildDir, "/output/mapping.txt")
if (!resultFile.exists()) {
resultFile.parentFile.mkdirs()
resultFile.createNewFile()
}
List plugMappings = searchFiles(new File(project.buildDir.absolutePath + "/outputs/mapping"), "mapping.txt")
if (plugMappings && plugMappings.size() > 0 && resultFile.exists()) {
// 使用NIO来操作流
FileChannel channel = new FileOutputStream(resultFile.absolutePath).getChannel()
for (File file : plugMappings) {
FileChannel fc = new FileInputStream(file).getChannel()
channel.transferFrom(fc, channel.size(), fc.size())
fc.close()
}
channel.close()
}
} catch (Exception e) {
e.printStackTrace()
}
}
}
project.tasks.whenTaskAdded { task ->
if ("assembleRelease".equals(task.name)) {
// task.dependsOn(project.tasks.getByName('copyMappingFile'))
}
if ('transformClassesWithDexBuilderForRelease'.equals(task.name)) {
// task.dependsOn(project.tasks.getByName('copyMappingFileTask'))
}
}
}
/**
* 获取给定目录下指定文件名的文件集合
*/
static List searchFiles(File folder, final String keyword) {
List result = new ArrayList()
File[] subFolders = folder.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
if (file.isDirectory()) {
return true
}
if (file.getName().toLowerCase().contains(keyword)) {
return true
}
return false
}
})
if (subFolders != null) {
for (File file : subFolders) {
if (file.isFile() && file.getName().toLowerCase().contains(keyword)) {
// 如果是文件则将文件添加到结果列表中
result.add(file)
} else {
// 如果是文件夹,则递归调用本方法,然后把所有的文件加到结果列表中
result.addAll(searchFiles(file, keyword))
}
}
}
return result
}
}
5. 本地使用该gradle插件
根目录下的build.gradle文件加入如下代码
buildscript {
repositories {
...
maven {
// ../ 代表当前父目录的父目录
url (uri('./repositories'))
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
// group : PluginProjectName : version
classpath 'com.ke.plugin:lifecycle_plugin:1.0.0-SNAPSHOT'
}
}
module下的build.gradle添加一行代码
apply plugin: 'com.ke.lifecycle.plugin'
就可以在工程中调试gradle插件的功能。
针对共享插件, 修改的地方就是将产物上传到maven, 配置文件如下所示:
apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'
group = GROUP
version = VERSION_NAME
def getPropertyFromLocalProperties(key) {
File file = project.rootProject.file('local.properties');
if (file.exists()) {
Properties properties = new Properties()
properties.load(file.newDataInputStream())
return properties.getProperty(key)
}
}
def getRepositoryUrl() {
return isSnapshot() ? getPropertyFromLocalProperties("SNAPSHOT_REPOSITORY_URL") : getPropertyFromLocalProperties("RELEASE_REPOSITORY_URL")
// return isSnapshot() ? SNAPSHOT_REPOSITORY_URL : RELEASE_REPOSITORY_URL
}
def isSnapshot() {
return version.endsWith("SNAPSHOT");
}
def hasAndroidPlugin() {
return getPlugins().inject(false) { a, b->
def classStr = b.getClass().name
def isAndroid = ("com.android.build.gradle.LibraryPlugin" == classStr) || ("com.android.build.gradle.AppPlugin" == classStr)
a || isAndroid
}
}
task sourcesJar(type: Jar) {
if (hasAndroidPlugin()) {
from android.sourceSets.main.java.srcDirs
classifier = 'sources'
} else {
from sourceSets.main.allSource
classifier = 'sources'
}
}
artifacts {
archives sourcesJar
}
uploadArchives {
// repositories.mavenDeployer {
// repository(url: uri('/Users/xxx/.m2/repository/')) // 本地仓库的路径
// pom.groupId = "${project.group}" //groupId ,自行定义,一般是包名
// pom.artifactId = "${project.name}" //artifactId ,自行定义
// pom.version = "${version}" //version 版本号
// }
repositories.mavenDeployer {
repository(url: repositoryUrl) {
authentication(userName: getPropertyFromLocalProperties("USER"), password: getPropertyFromLocalProperties("PASSWORD"))
}
}
}
bintray {
user = getPropertyFromLocalProperties("bintray.user")
key = getPropertyFromLocalProperties("bintray.apikey")
configurations = ['archives']
pkg {
repo = 'maven'
name = "${project.group}:${project.name}"
userOrg = 'xxx'
licenses = ['Apache-2.0']
websiteUrl = 'xxx'
vcsUrl = 'xxx'
publish = true
}
}
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'
一些常量信息都是在local.properties中配置。
Gradle plugin 调试
1. run - > Edit Configurations - > Remote name = debug
2. 命令行输入: ./gradlew clean build -Dorg.gradle.daemon=false -Dorg.gradle.debug=true