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

来源:互联网 发布:淘宝连接转口令 编辑:程序博客网 时间:2024/05/16 07:39

深入理解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 UTCRevision:     d9e2113d9fb05a5caabba61798bdb8dfdca83719Groovy:       2.4.4Ant:          Apache Ant(TM) version 1.9.6 compiled on June 29 2015JVM:          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#额外添加,必须如下写,不能只写25sdk.api=android-25

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

//settings.gradledef 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.gradleandroid {//    采用api导入的方式    compileSdkVersion gradle.api }

gradle.properties

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

#gradle.propertiesorg.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.gradletask 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 clean2个命令玩起来了。

其他

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

192:Posdevice fish$ ./gradlew assemble -qsetting.gradle executesetting initSdkApiroot build.gradle executeapp build.gradle executeapk is coping lib build.gradle executejar is coping afterEvaluate -> disableDebugBuild libproject.tasks size 152debug tasks size 73taskGraph.whenReadyafter 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 onassembleDebug->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

0 0
原创粉丝点击