您的位置:首页 > 移动开发 > Android开发

3深入理解Android之Gradle学习笔记

2017-02-08 16:19 507 查看

深入理解Android之Gradle学习笔记

最近在学习gradle,innost的这篇文章可以说是目前中文说gradle最好的文章

深入理解 Android 之 Gradle.文章名字虽然叫深入理解,但是其实讲的也不深,不过比其他的说脚本怎么配置的文章好太多了,读完之后收货颇多,在这里记录重点,并且把他文中的demo进行实现改进(作者未提供源码),算是对原文的一个总结和补充(源码在文末)。

基础知识

Gradle

Gradle 是一个框架,负责定义流程和规则,而具体的构建工作则是通过插件的方式来完成的,比如编译 Java 有 Java 插件,编译 Groovy 有 Groovy 插件,编译 Android APP 有 Android APP 插件,编译 Android Library 有 Android Library 插件。我们可以通过
apply plugin:'XXX'
来导入插件。

Gradle对象

Gradle 主要有三种对象,这三种对象和三种不同的脚本文件对应,在 gradle 执行的时候,会将脚本转换成对应的对象(delagate):

Gradle 对象:当我们执行 gradle xxx 或者什么的时候,gradle 会从默认的配置脚本中构造出一个 Gradle 对象。在整个执行过程中,只有这么一个对象。Gradle 对象的数据类型就是 Gradle。我们一般很少去定制这个默认的配置脚本。

Project 对象:每一个build.gradle 会转换成一个 Project 对象。对于multi-project build,root的build.gradle会变为root project,module内的build.gradle会变为subproject.

Settings 对象:每一个 settings.gradle 都会转换成一个 Settings 对象。

Gradle生命周期



Gradle工作流程



Gradle工作包含三个阶段:

首先是初始化阶段。对我们前面的multi-project build而言,就是执行settings.gradle

Configration阶段的目标是解析每个project中的build.gradle。比如multi-project build例子中,解析每个子目录中的build.gradle。在这两个阶段之间,我们可以加一些定制化的Hook。这当然是通过API来添加的。

Configuration阶段完了后,整个build的project以及内部的Task关系就确定了。恩?前面说过,一个Project包含很多Task,每个Task之间有依赖关系。Configuration会建立一个有向图来描述Task之间的依赖关系。所以,我们可以添加一个HOOK,即当Task关系图建立好后,执行一些操作。

最后一个阶段就是执行任务了。当然,任务执行完后,我们还可以加Hook。

multi-project

这里要重点说下multi-project,此时会有一个root project,若干个subproject(每一个subproject代表一个module)。那么root project和subproject之间有什么关系呢? 很久以前是只有subproject没有root project的,这个时候会有个问题,有一些共性的东西每个build.gradle都要写,若有100个module,那部分共性代码就写100次。然后有了root project,我们可以把公共的东西写到root project里。所以subprojects和allprojects这2个build script在root project会很常见。

基本task

android插件依赖于Java插件,而Java插件依赖于base插件。base插件有基本的tasks生命周期和一些通用的属性。base插件定义了例如assemble和clean任务,Java插件定义了check和build任务,这两个任务不在base插件中定义。

这些tasks的约定含义:

assemble: 集合所有的output

clean: 清除所有的output

check: 执行所有的checks检查,通常是unit测试和instrumentation测试

build: 执行所有的assemble和check

Posdevice实例

前提

本文OS为mac,直接使用AS的Terminal来构建,主要是2个命令
./gradlew assemble
./gradlew clean
,所以就不用搭环境了。当然很多时候我会在./gradlew xxx之后加入-q,可以去掉一些系统日志,让结果看起来更清晰点。

本文的gradlew版本如下

X-Pro:Version2Asset fish$ ./gradlew -version

------------------------------------------------------------
Gradle 2.14.1
------------------------------------------------------------

Build time:   2016-07-18 06:38:37 UTC
Revision:     d9e2113d9fb05a5caabba61798bdb8dfdca83719

Groovy:       2.4.4
Ant:          Apache Ant(TM) version 1.9.6 compiled on June 29 2015
JVM:          1.8.0_77 (Oracle Corporation 25.77-b03)
OS:           Mac OS X 10.10.5 x86_64


为什么用
./gradlew ...
,而不是用
gradle ...
呢?第二种是用环境变量里的gradle,第一种是用当前工程里的gradle即gradle wrapper里的gradle,一般我们都使用第一种,不同工程的gradle插件版本可能差别很大。

需求

(为了更好的体现gradle的思想,我对原文的需求进行适当的修改。)

有个android Project,内有2个module,分别是app module和library module.其中app module的名字叫app,library module的名字叫cposdevicesdk。

cposdevicesdk编译出release版本的jar包拷贝到根目录的output文件夹下,debug版本不编译

app编译产生的debug和release的apk都需要拷贝到根目录的output文件夹下

output文件夹下最后会有3个产物,app编译产生的debug和release版本,cposdevicesdk编译出release版本,这3个产物的名字内必须有版本号,版本号来自manifest

实现

需求定了就可以撸起来了。很容易的,我们new一个project叫做Posdevice,里面有2个module,app和cposdevicesdk,app依赖于cposdevicesdk。此时工程结构如下所示。



此时其实有3个build.gradle文件,一个setting.gradle文件。3个build.gradle分别是根build.gradle,module app内build.gradle以及cposdevicesdk内build.gradle。

一次gradle构建只产生一个gradle对象,有多少个module便对应多少个gradle project(注意和android studio的Project区分,本文中as的project我都会写明AS project)

所以这里会有一个gradle对象,1个root project对象,2个subproject对象,1个setting对象

编译环境配置

首先我们看到app和cposdevicesdk的build.gradle里面都有以下代码,注意下compileSdkVersion和buildToolsVersion,不同人的机器上,这些值可能不一样,所以最好不要在build.gradle里面写死(有时候github上拉下来的代码编译不过也是由此引起)。

android {
compileSdkVersion 25
buildToolsVersion "25.0.0"
defaultConfig {
...
}
buildTypes {
...
}
}


那怎么写compileSdkVersion和buildToolsVersion才比较灵活呢?有2种方法,一种是写在local.properties里面。我们在setting.gradle里去读取值,然后利用ext给grale对象创建一个成员存起来,以后全局都可以从gradle里获取值了。第二种是利用gradle.properties文件。我这里为了学习对compileSdkVersion采用方法1,对buildToolsVersion采用方法2

ext gradle

代码如下,首先在local.properties里添加sdk.api=android-25,注意必须要带android,不能只写25.

#local.properties

#AS帮我们生成的
sdk.dir=/Users/fish/Documents/android-sdk-macosx

#额外添加,必须如下写,不能只写25
sdk.api=android-25


接着在settings.gradle内读取到sdk.api的值,然后用ext给gradle增加一个变量api,这样其他地方就能用gradle.api来取这个值了

//settings.gradle
def initSdkApi(){
println "setting initSdkApi"
Properties properties = new Properties()
//local.properites 也放在 posdevice 目录下

File propertyFile = new File(rootDir.getAbsolutePath() + "/local.properties")
properties.load(propertyFile.newDataInputStream())
/*
根据 Project、Gradle 生命周期的介绍,settings 对象的创建位于具体 Project 创建之前
而 Gradle 底对象已经创建好了。所以,我们把 local.properties 的信息读出来后,通过
extra 属性的方式设置到 gradle 对象中
而具体 Project 在执行的时候,就可以直接从 gradle 对象中得到这些属性了!

*/
gradle.ext.api = properties.getProperty('sdk.api')
}
//初始化
initSdkApi()
include ':app', ':cposdevicesdk'


//app和cposdevicesdk里的build.gradle
android {
//    采用api导入的方式
compileSdkVersion gradle.api
}


gradle.properties

这种方式会更简单,在gradle.properties里定义buildToolsVer

#gradle.properties

org.gradle.jvmargs=-Xmx1536m

# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true
#额外添加
buildToolsVer=25.0.0


然后在2个module的build.gradle里都能用buildToolsVer了

android {
//    采用api导入的方式
compileSdkVersion gradle.api
//    利用gradle.properties
buildToolsVersion buildToolsVer
}


结论

明显第二种方法简单一些,所以我们尽量利用gradle.properties,当然第一种方法学习下来熟悉gradle也不错。

配置好之后,我们可以在AS的terminal里执行下
./gradlew assemble
,顺利通过

utils.gradle

在gradle中,我们常常会定义一些常用的函数,这样全局通用,这些函数往往会写到一个gradle文件里,我们就定一个utils.gradle。这里定义2个函数,一个是getVersionNameAdvanced,从manifest内去获取版本号。另一个是disableDebugBuild,对debug的task设置disable,这样task就不会执行了。

def getVersionNameAdvanced(){

def xmlFile = project.file("src/main/AndroidManifest.xml")
def rootManifest = new XmlSlurper().parse(xmlFile)
return rootManifest['@android:versionName']
}

//对于 android library 编译,我会 disable 所有的 debug 编译任务

def disableDebugBuild(){

//返回值保存到 targetTasks 容器中
println "project.tasks size "+ project.tasks.size()

//project.tasks 包含了所有的 tasks,下面的 findAll 是寻找那些名字中带 debug 的 Task。
def targetTasks = project.tasks.findAll{task ->
task.name.contains("Debug")
}
//对满足条件的 task,设置它为 disable。如此这般,这个 Task 就不会被执行

targetTasks.each{
//        println "disable debug task  : ${it.name}"
it.setEnabled false
}
}
//将函数设置为 extra 属性中去,这样,加载 utils.gradle 的 Project 就能调用此文件中定义的函数了

ext{
getVersionNameAdvanced = this.&getVersionNameAdvanced
disableDebugBuild = this.&disableDebugBuild
}


utils.gradle里面定义了这些函数,其他gradle文件要用必须要apply(相当于import)。那我们是不是要每个gradle都加下面代码呢?

apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"


这么做当然可以,但是还有更简单的方法,那就是在根build.gradle里配subprojects,完整的根build.gradle如下所示

// Top-level build file where you can add configuration options common to all sub-projects/modules.
println "root build.gradle execute"
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'

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

}

subprojects{
//为每个子 Project 加载 utils.gradle 。当然,这句话可以放到 buildscript 花括号之后,必须位于subprojects之内
apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"
}

allprojects {
repositories {
jcenter()
}
}

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


禁止cposdevicesdk的debug版本编译

好了,基础都写好了,下面来完成需求,在cposdevicesdk的build.gradle里加以下代码,就可以禁止cposdevicesdk的debug版本编译,这里的project就是cposdevicesdk的build.gradle对应的project,project.afterEvaluate会在task有向图创建完毕之后被调用。

/*
因为我的项目只提供最终的 release 编译出来的 Jar 包给其他人,所以不需要编译 debug 版的东西

当 Project 创建完所有任务的有向图后,我通过 afterEvaluate 函数设置一个回调 Closure。在这个回调

Closure 里,我 disable 了所有 Debug 的 Task
*/
project.afterEvaluate{
println 'afterEvaluate -> disableDebugBuild lib'
disableDebugBuild()
}


拷贝jar和apk

拷贝这件事情应该发生在assemble之后,我们如何在assmble之后插入一个拷贝的任务呢?介绍2种方法

函数调用

第一种方法是函数调用,innost的文章里用这种方法。先找的 assemble 任务,然后我通过 doLast 添加了一个 Action。这个 Action 就是 copyOutput,copyOutput是一个在utils.gradle里定义的函数。

tasks.getByName("assemble"){
it.doLast{
println "$project.name: After assemble, jar libs are copied to local repository"
copyOutput(true)
}
}


task->finalizedBy

这种方法是写一个copyTask,然后把copyTask绑定在assembleRelease后面,在我的代码里使用这种方法,代码如下,其实要是绑在assemble后面更加合理,但是我试了下不行,不知道为什么。

//lib的build.gradle
task copyTask(type: Copy){
println "i am coping "
from('build/intermediates/bundles/release/')
into('../output/')
include('classes.jar')
rename (/(.*).jar/, 'cposdevicesdk-release'+project.getVersionNameAdvanced()+'.jar')
}

tasks.whenTaskAdded { task ->
//下边如果用 assemble,不行
if (task.name == 'assembleRelease') {
task.finalizedBy 'copyTask'
}
}


命名加版本号

其实上边的copyTask里已经加入改名字的代码了.app的copyTask如下,比较简单,我们自己定义了一个task,copyTask 他的类型是Copy(代表继承AbstractCopyTask),后面的from,into,include,rename都是AbstractCopyTask的方法,返回this,这是gradle task的常见写法。

rename的时候使用正则替换,第一个变量是一个正则表达式用//包起来,代表以.apk结尾的任意字符串,第二个变量里的$1就是.apk之前的所有字符串。

task copyTask(type: Copy){
println "apk is coping "
from('build/outputs/apk')
include('*.apk')
into('../output/')
rename(/(.*).apk/,'$1-'+project.getVersionNameAdvanced()+'.apk')
}


lib的copyTask如下,首先lib编译出来是aar文件,而我们想要jar包,jar包在哪呢?jar包是中间产物,文件是 ./build/intermediates/bundles/release/classes.jar,我们只要把他拷贝出来就行了。

task copyTask(type: Copy){
println "jar is coping "
from('build/intermediates/bundles/release/')
into('../output/')
include('classes.jar')
rename (/(.*).jar/, 'cposdevicesdk-release'+project.getVersionNameAdvanced()+'.jar')
}


clean注意清除output

我们每次
./gradlew assemble
都会把2个apk,1个jar拷贝到ouput里去,所以对应的clean要加入删除代码,在clean的时候删除output文件夹,我们可以在clean后加入删除的代码就好了,如下所示。

clean.doFirst {
delete "${rootDir}/output/"
println "delete output before clean"
}


好了,大功告成!可以用
./gradlew assemble
./gradlew clean
2个命令玩起来了。

其他

根据下边的日志看起来,copy应该会无效啊,此时afterEvaluate都没执行,是处于Configuration阶段,没有到Execution阶段(在execution阶段完成各种编译链接) 但是实际上copy是发生在assemble之后的,我估计这就是闭包的doLast和直接代码的区别,真正的copy发生在doLast内。L12之后开始execution。

192:Posdevice fish$ ./gradlew assemble -q
setting.gradle execute
setting initSdkApi
root build.gradle execute
app build.gradle execute
apk is coping
lib build.gradle execute
jar is coping
afterEvaluate -> disableDebugBuild lib
project.tasks size 152
debug tasks size 73
taskGraph.whenReady
after assemble


实例2 通过构建脚本影响源代码

需求

默认构建是产生debug和release2个包,要求再加一个demo包,demo包的签名和debug包保持一致

在apk的第一个页面显示 I am Debug/Release/Demo

实现

首先第一个需求加一个demo包,非常简单在buildtype那里加就ok了。主要看第二个需求,要不同的buildtype编译出来的apk能够知道自己是属于哪个buildtype的,这里实际上是通过gradle代码影响了工程代码。一般来说工程代码和构建脚本是相互独立的,要如何才能影响到工程的代码呢?我们可以在构建的时候把当前的buildtype写到某个文件,然后在apk的代码里去读取这个文件。ok,lets do it!

buildtype增加demo

首先实现buildtype增加demo,很简单,在app的build.gradle内的buildTypes内加下demo即可,demo还得配置下签名。buildTypes内其实隐藏了一个debug。

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
demo{
//和debug使用同一个签名
signingConfig signingConfigs.debug
}
}


assets文件记录buildtype

根据innost大神的思路,我写下了如下代码,在preDebugBuild、preReleaseBuild、preDemoBuild任务开始的时候添加一个doFirst任务,这是一种常见的做法,preXXXBuild完成之后就会执行我们的doFirst内的任务。这样看起来没什么问题,但是我试了下,有问题。

之前,我们一直在用2个命令
./gradlew assemble
./gradlew clean
,现在再学习几个。
./gradlew assembleDebug
./gradlew assembleRelease
./gradlew assembleDemo
,这3个命令分别是构建debug包,构建relese包和构建Demo包,实际上assemble就是依赖于assembleDebug、assembleRelease、assembleDemo这3个task。在这里,我试了下用
./gradlew assembleDebug
得到debug包,可是debug包里的assets文件里写的是I am release。然后我又用
./gradlew assembleDemo
构建了demo包,结构里面还是I am release。Why?

def  runtime_config_file = 'app/src/main/assets/runtime_config'

project.afterEvaluate{
//找到 preDebugBuild 任务,然后添加一个 Action
tasks.getByName("preDebugBuild"){
it.doFirst{
println "generate debug configuration for ${project.name}"
def configFile = new File(runtime_config_file)
configFile.withOutputStream{os->
os << I am Debug\n'  //往配置文件里写 I am Debug
}
}
}
//找到 preReleaseBuild 任务

tasks.getByName("preReleaseBuild"){
it.doFirst{
println "generate release configuration for ${project.name}"
def configFile = new File(runtime_config_file)
configFile.withOutputStream{os->
os << I am release\n'
}
}
}
//找到 preDemoBuild。这个任务明显是因为我们在 buildType 里添加了一个 demo 的元素

//所以 Android APP 插件自动为我们生成的

tasks.getByName("preDemoBuild"){
it.doFirst{
println "generate offlinedemo configuration for ${project.name}"
def configFile = new File(runtime_config_file)
configFile.withOutputStream{os->
os << I am Demo\n'
}
}
}
}


我把task的依赖图打出来后发现,原来有如下依赖关系,从下面可以看出assembleDebug会调用preDebugBuild, preDemoBuild,preReleaseBuild,所以虽然我们只是执行assembleDebug,但是preDebugBuild, preDemoBuild,preReleaseBuild都会被调用,所以最后写成了I am release。原作者能够成功,估计是gradle插件的版本不一样。

->表示depend on
assembleDebug->packageDebug->transformClassesWithDexForDebug->prepareDebugDependencies->prepareComAndroidSupportSupportCoreUi2501Libarary->preDebugBuild, preDemoBuild,preReleaseBuild


那怎么办呢?其实很简单,把assembleDebug和assembleDemo的具体task看一下,比较一下看看各自有什么特殊的task,基于这个task就可以了。怎么看assembleDebug的具体task呢?执行
./gradlew assembleDebug
就可以了,注意不要加-q,大概如下所示,以冒号开头的都是任务,比较多。

...
:app:prepareComAndroidSupportAnimatedVectorDrawable2501Library UP-TO-DATE
:app:prepareComAndroidSupportAppcompatV72501Library UP-TO-DATE
:app:prepareComAndroidSupportSupportCompat2501Library UP-TO-DATE
:app:prepareComAndroidSupportSupportCoreUi2501Library UP-TO-DATE
:app:prepareComAndroidSupportSupportCoreUtils2501Library UP-TO-DATE
:app:prepareComAndroidSupportSupportFragment2501Library UP-TO-DATE

...


我把assembleDebug和assembleDemo的具体task对比了一下,找到了prepareDebugDependencies和prepareDemoDependencies,尝试了下用prepareXXXDependencies,果然成功了,而且直接用
./gradlew assemble
生成3个包也没问题!

效果如下:



app的build.gradle部分代码如下所示

def  runtime_config_file = 'app/src/main/assets/runtime_config'

project.afterEvaluate{
println "task size "+tasks.size()
//找到 prepareDebugDependencies 任务,然后添加一个 Action
tasks.getByName("prepareDebugDependencies"){
it.doFirst{
println "generate debug configuration for ${project.name}"
def configFile = new File(runtime_config_file)
configFile.withOutputStream{os->
os << 'I am Debug\n'  //往配置文件里写 I am Debug
}
}
}
//找到 prepareReleaseDependencies 任务

tasks.getByName("prepareReleaseDependencies"){
it.doFirst{
println "generate release configuration for ${project.name}"
def configFile = new File(runtime_config_file)
configFile.withOutputStream{os->
os << 'I am release\n'
}
}
}
//找到 prepareDemoDependencies
tasks.getByName("prepareDemoDependencies"){
it.doFirst{
println "generate demo configuration for ${project.name}"
def configFile = new File(runtime_config_file)
configFile.withOutputStream{os->
os << 'I am Demo\n'
}
}
}
}


实例2优化–buildConfigField

实例2这么做其实挺复杂的,我们完全可以使用更简单的方式来解决问题,那就是使用buildConfigField。

我们在app的build.gradle内写如下代码,用buildConfigField来定义一个field叫做API_URL。

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
buildConfigField "String", "API_URL","\"i am release\""

}
demo{
//和debug使用同一个签名
signingConfig signingConfigs.debug
applicationIdSuffix 'demo'
buildConfigField "String", "API_URL","\"i am demo\""

}
debug{
buildConfigField "String", "API_URL","\"i am debug\""
}
}


这个gradle在编译之后会产生3个Build.Config文件,可以看到我们定义的API_URL变为了BuildConfig的一个成员变量,然后我们可以在代码里直接用BuildConfig.API_URL.为什么?看BuildConfig的包名,和我们程序包名一致,所以可以直接用。

package com.fish.test;
public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "com.fish.test";
public static final String BUILD_TYPE = "debug";
public static final String FLAVOR = "";
public static final int VERSION_CODE = 1;
public static final String VERSION_NAME = "1.0";
// Fields from build type: debug
public static final String API_URL = "i am debug";
}


android代码如下

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

String s = BuildConfig.API_URL;
TextView tv = (TextView) findViewById(R.id.aa);
tv.setText(s);
}
}


可以看到利用buildConfigField简便优雅的实现了实例2的需求。

经验总结

跟本地编译环境相关参数应该放入gradle.properties内

Gradle可以通过ext为对象额外添加属性或者方法

gradle命令支持缩写,比如aR表示assembleRelease

由于groovy支持动态类型,所以有时候写错了也不会有提示,而且AS内的gradle也无法debug,所以要多用println来打日志

由于AS的terminal比较简陋,所以我打日志的时候一般会在日志里填中文,这样会醒目很多

源码

Posdevice实例 https://github.com/chefish/Posdevice

实例2 https://github.com/chefish/Version2Asset

Ref

http://wiki.jikexueyuan.com/project/deep-android-gradle/four-four.html

https://segmentfault.com/a/1190000004234712?_ea=538654

https://segmentfault.com/a/1190000004241503#articleHeader12
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  gradle android 脚本