Gradle For Android

来源:互联网 发布:重庆市软件行业协会 编辑:程序博客网 时间:2024/05/29 10:08

Gradle最重要概念:projects 和 tasks

每项工程构建都至少由一个project组成。每个build.gradle代表一个project,Tasks被定义在编译脚本之中,Gradle在初始化构建过程中,其基于build文件编译得到Project和Task对象。而Task对象又包含Actions对象队列,他们将顺序执行。Action对象是可被执行的代码块,类似java中的方法。

构建生命周期

初始化阶段:该阶段是project对象创建之时,如果有多个模块,则有多个build.gradle文件对应,那么就有多个preject被创建。
配置阶段:脚本被执行,为每个project对象创建及配置所有task
执行阶段:gradle决定哪些task需要被执行。这取决于启动构建时传入的参数以及当前处于哪个目录。

It is good practice to use the latest Android API version as the compilation target.(compileSdkVersion)

Gradle Wrapper

在一个工程项目里统一gradle版本,防止因为gradle版本号不一致导致编译出问题。
it is also recommended to add the wrapper files to your version control system.

settings.gradle 在初始化阶段被执行,只有在多模块下才被需要。

TargetSdkVersion

指的是该app已经在所指定的系统版本设备上进行测试过了,这样操作系统就不需要开启各种向前兼容的行为了。这和compileSdkVersion一点关系没有。
我觉的TargetSdkVersion应该综合QA测试所使用及市占最高的系统版本来综合得出结论。如果TargetSdkVersion设置的合适,可以提高运行的速度。

compileSdkVersion

最佳实践推荐使用最新的SDK版本。

Manifest.xml 和 Build.gradle

gradle的设置会覆盖Manifest.xml对应的属性,使得Manifest.xml成为gradle的fallback角色。

Tasks

打印所有出所有task

gradlew tasks

这里写图片描述
打印所有出所有task包括依赖

gradlew tasks --all

dry run

假运行但输出日志,不产生实质工作。加参数 -m 或 –dry-run

Android Task

assemble:为每个build type 创建一个APK
clean:清除所有的编译产出,如APK文件
check:Lint检查
build:assemble + check ; build task依赖于check task

一些鲜为人知的Task

connectedCheck :在已连接的设备或模拟器上运行测试。
deviceCheck:留个空位,为在远程设备上运行测试。

BuildConfig文件

从SDK tools 17后,build tools 会生成BuildConfig,其内容根据gradle中的设置。在debug及release的时候可为BuildConfig中的恒量配置不同的值。比如release时候关闭log输出,debug采用测试URL.
这里写图片描述

Project 全局设置

如果一个工程中有多个模块,那么在 allprojects {} 块中 进行全局设置来一举应用到所有的模块上。
比如:
这里写图片描述

资源也可以变,哈!
这里写图片描述

属性值 Project properties

定义属性有这么几种方式:
* ext块

ext { local='hello world'}
  • gradle.properties文件
  • -P命令行参数传入
    这里写图片描述
    printProperties为task, cmd为其中属性;

依赖管理

依赖的依赖被称为 transitive dependencies

一个依赖由三部分唯一被确定,group ,name ,version.
group 公司域名的反写
name 库的唯一标识符

dependencies {       compile 'com.google.code.gson:gson:2.3'}

dependencies {       compile group: 'com.google.code.gson', name: 'gson', version:       '2.3'}

~/.m2是本地的依赖缓存位置所在

远程仓库

不用JCenter ,MavenCenter? 自己来

repositories {       maven {           url "http://repo.acmecorp.com/maven2"       }}

如果需要认证呢?

repositories {       maven {           url "http://repo.acmecorp.com/maven2"           credentials {               username 'user'               password 'secretpassword'           }} }

本地仓库

需要指定在本地仓库的相对或者绝对位置

repositories {    maven {        url "../repo"    }}

用普通的目录来作为仓库也是可以的

repositories {       flatDir {            dirs 'aars'         }}

本地依赖

文件依赖

依赖是本地的一个jar文件

dependencies {       compile files('libs/domoarigato.jar')}

有很多jar文件,还可以指定整个目录

dependencies {       compile fileTree('libs')}

明确指定是jar包

dependencies {       compile fileTree(dir: 'libs', include: ['*.jar'])}

Native 库(.so)依赖

默认情况下,Android 插件(gradle) 支持Native库,只需要在模块层级下创建名为jniLibs的目录,并创建平台子目录,然后将.so放置到对应的目录下。
这里写图片描述

当然如果想放到别的位置也行,不过需要在gradle中设置。

android {       sourceSets.main {           jniLibs.srcDir 'src/main/libs'       }}

工程依赖

settings.gradle

include ':app', ':library'

dependencies {       compile project(':library')}

依赖aar库文件

在模块下为aar库创建个目录比如aars,
然后声明一下该目录(aars)为本地仓库

repositories {       flatDir {           dirs 'aars'        }}

依赖

dependencies {       compile(name:'libraryname', ext:'aar')}

依赖的概念

配置

为了能够编译代码,需要添加SDK到compile classpath.还有个场景就是无需将SDK打入APK中,因为它已经在设备中了,这个就需要配置多样化。

compile group: 'com.google.code.gson', name: 'gson', version:'2.3'

如上面的compile,总共包括如下几种配置:

标准配置

  • compile:默认的配置,依赖不仅添加到classpath,而且添加到apk中
  • apk:会打入到apk中,但是不添加到classpath中,使用场景不明。只接受jar包依赖
  • provided:会添加到classpath中,但是不会打入到apk中。只接受jar包依赖。这些依赖SDK在运行设备上已存在,无需打入到apk包中。
  • testCompile,androidTestCompile:只会在运行测试task的时候生效。

build variant + 标准配置

  • debugCompile:如果只想在buildTypes == debug的时候使用指定依赖。比如某种debug日志SDK
  • productFlavorCompile: 在说明中并没有特殊指明这种形式,但是在实践中可以使用,根据gradle通常的思路,是在该指定productFlavor的时候才使用依赖。

版本

版本的含义

major.minor.patch
  • major:增加意味着API发生不兼容的改变
  • minor:增加了新功能,但以向后兼容之方式
  • patch:修复了bug.

动态版本

dependencies {       compile 'com.android.support:support-v4:22.2.+' //自动获取最新的patch发布包       compile 'com.android.support:appcompat-v7:22.2+' //自动获取最新的minor版本,并且minor版本不得小于2       compile 'com.android.support:recyclerview-v7:+' //永远获取最新的版本}

尽量不要使用动态版本,这可能导致你的编译失败。

创建Build Variants

Build types

In the Android plugin for Gradle, a build type is used to define how an app or library should be built.
build types 用来定义一个app或库编译的方式。
默认会存在release和debug两种,

自定义Build types

android {    buildTypes {    staging.initWith(buildTypes.debug)    staging {        applicationIdSuffix ".staging"        versionNameSuffix "-staging"        buildConfigField "String", "API_URL",            "\"http://staging.example.com/api\""        }    }}

注:initWith相当于copy的意思,这样直接用另外一个既有的来初始化当前的,免的重新去初始化。这在后面常见到。

新Build Type 新Source Set

当创建一个新的build type时候,gradle也创建一个新的source set, 通常情况,source set 目录和build type 有相同的名称。但是但是!!不是自动产生的,而需要你手工建立。
这里写图片描述

这种新建source set的方式带来极大的方便,你可以在不同的build type 下指定个性化的drawable,layout,string,manifest.

These source sets open up a world of possibilities.

问题:需要将main中的代码都拷贝一份过去吗?

不需要,只需要在各个build type 增量添加需要的内容。

问题:那么新增的个性文件和main的同名该如何处理。

如果是drawable,layout文件,那么就是直接的覆盖。如果是string.xml ,则是合并,Manifest.xml文件也是同样的合并。关键是在string.xml及Manifest.xml中只需要定义你新增的内容,没必要全部都写一遍。

Build Type可有自己专属依赖

下面的依赖只为 debug build type 存在。

dependencies {       debugCompile 'de.mindpipe.android:android-logging-log4j:1.0.3'}

Product flavors

Build type 同一个APP或库执行不同的编译配置。(对内)
Product flavors 对同一个APP推出不同的版本(对外)

举个例子:

根据开发测试的需求,APP分 debug,staging,release, 这几种不同的配置就属于Build type;
当我们发布正式版本的时候,对普通商户,定制的OEM商户 分别打出不同的包。这属于Product flavors ;

如果不确定该使用哪一个,自问

你需要一个全新的app以单独发布 – 使用product flavors 其他见下
你想要对同一个app代码进行一次编译然后供内部使用 – 使用build types

product flavors也可拥有自己的Source set目录

更进一步,还可以拥有一个 product flavor 和 build type 组合的Source set目录,组合的比单独的Source set拥有更大的优先级,这体现在,如果拥有同名的资源时,优先使用组合的。

flavor还可以组合

最终想得到这样的build variants

blueFreeDebug and blueFreeReleasebluePaidDebug and bluePaidReleaseredFreeDebug and redFreeReleaseredPaidDebug and redPaidRelease

blue , red ,free ,paid 是flavor,

android {       flavorDimensions "color", "price"       productFlavors {           red {               flavorDimension "color"           }           blue {               flavorDimension "color"            }            free {               flavorDimension "price"           }           paid {               flavorDimension "price"           }       }}

定义了flavorDimensions后,gradle要求为每个flavor指定对应的flavorDimensions,如果没有指定,就会报错。flavorDimensions后面所定义的顺序很重要:
* 决定着flavor的优先级,谁有权限覆盖谁的相同配置或者资源。
* 生成的build variants名称谁在先谁在后。

Build variants

Build variants are simply the result of combining build types and product flavors. product flavors + Build types = Build Variants

Tasks

The Android plugin for Gradle will create tasks for every build variant you configure
New tasks are created for every build type, for every product flavor, and for every combination of build type and product flavor.

Resource and manifest 的合并

因为build type 及 flavor都可以拥有自己的source set ,那么在打包APP的时候,android gradle 插件需要将 build type 和 flavor的source set 合并到main的source set 上?
合并的优先级(左侧最高)如下图:
这里写图片描述
这个优先级顺序如何理解呢?如果一个资源在flavor和main source set中都被声明定义过了,那么合并的过程,该如何取舍呢?从上图可知,flavor拥有更高的优先级,那么flavor source set 中的同名资源将被打包,而非main的。库工程中的资源永远都是最低优先级。

Variant filters

如果不加以过滤,gradle为我们自动创建的build variant 将多的眼花,在app或库的根目录下的build.gradle文件中加上这些代码可过滤掉你不需要的那些build variant.

android.variantFilter { variant ->       if(variant.buildType.name.equals('release')) {           variant.getFlavors().each() { flavor ->               if (flavor.name.equals('blue')) {               variant.setIgnore(true);            }        }           }}

注: getFlavors返回的是flavor数组哦,因为flavor可以多个组合的,可以见见 flavor dimensions

签名

staging配置 使用initWith 直接将debug的签名配置都拷贝了,这意味着staging build签名用的是debug key ,免的自己再去定义一个。


  android {       productFlavors {
           blue {               signingConfig signingConfigs.release
} }}

使用上面的这个签名方式会导致一个问题,当给flavor一个配置赋值,实际上等于覆盖了build types 的签名配置,(flavor的优先级高于build types),那么更高明的办法是,在build types 中为每个flavor指明相应的签名配置。

android {       buildTypes {
           release {               productFlavors.red.signingConfig signingConfigs.red               productFlavors.blue.signingConfig signingConfigs.blue
} }}   

密码如何保存

在根目录中建立一个名为private.properties的文件

该文件不添加到仓库中(尤其指那种开源):
然后添加:

release.password = thepassword

然后定义一个任务来读取

task getReleasePassword << {       def password = ''       if (rootProject.file('private.properties').exists()) {           Properties properties = new Properties();           properties.load( rootProject.file             ('private.properties').newDataInputStream())           password = properties.getProperty('release.password')} }

注:properties.load()是专用来读取键值对的方法。

控制台询问

if (!password?.trim()) {           password = new String(System.console().readPassword             ("\nWhat's the secret password? "))       }

Hook into Gradle and Android Plugin

tasks.whenTaskAdded { theTask ->       if (theTask.name.equals("packageRelease")) {           theTask.dependsOn "getReleasePassword"       }}

因为packageRelease任务是Android插件动态产生的,不是在一开始就存在的,所以需要用到tasks.whenTaskAdded().也只有当packageRelease任务执行的时候才需要密钥。

如何命令行build module不至出错

在project的根目录 和 进入到模块目录去执行同一条命令的结果是完全不同的,这样有可能在编译的时候会出现混淆,那么一个办法就是在写命令行的时候指明你要编译的那个模块
gradlew :wear:assembleDebug

如何使得gradle build速度提高呢?

在gradle.properties文件里可以配置一个属性来激活“平行”编译
org.gradle.parallel=true
其宗旨就是会产生多个线程来同时执行模块的编译,每个线程对应一整个模块的编译工作。
但有个限制就是,要保证模块间无耦合。

那什么样子叫做“模块耦合”

只要模块间访问对方的任务或属性就叫做模块耦合。
在多模块项目中,可以在任何模块中使用allprojects应用属性到在project中的任何模块。而allprojects的使用将使得平行编译执行不能发挥其作用。
那么如何避免耦合情况的发生呢?不要直接的去访问其他模块的tasks或properties. 如果非得这么做,那么利用根模块作为中转,这样只会和根模块耦合,从而减少模块间的耦合。

实践Tasks 和 Plugins

浅析Groovy

  • 衍生自java
  • 运行在java虚拟机上的
  • 更简单,可读性更高

java:

System.out.println("Hello, world!");

groovy:

println 'Hello, world!'

插入符$

不仅可以插变量,还可以插方法,不过方法需要加上{}

   def name = 'Andy'   def greeting = "Hello, $name!"   def name_size "Your name is ${name.size()} characters long."

更离谱的是还可以动态插入“方法”

def method = 'toString'new Date()."$method"()

类和成员

跟java差不多

class MyGroovyClass {       String greeting       String getGreeting() {           return 'Hello!'} }
  • 类和方法默认是public ,而类成员则默认是private.
  • getter 和 setter是自动被加上的。
    def instance = new MyGroovyClass()   instance.setGreeting 'Hello, Groovy!'   instance.getGreeting()

直接使用关键字 def 创建新实例

方法

java

public int square(int num) {       return num * num;} square(2);

groovy

def square(def num) {       num * num}square 4
  • 无需为方法指定返回类型
  • 最后一行默认作为方法返回,无需return关键字。(但是return 扔被推荐)
  • def 被用来替代显式的类型,

还有一种更加简短的方式:closure

def square = { num ->       num * num}square 8

Closures

Closures are anonymous blocks of code that can accept parameters and can return a value. They can be assigned to variables and can be passed as parameters to methods.

Closure square = {       it * it}square 16

如果没有显式的添加参数给closure,Groovy会自动给添加一个,而这个参数被称为it,可直接在closure中使用,

集合

List

List list = [1, 2, 3, 4, 5]

如何遍历,使用each方法

list.each() { element ->       println element}

更短

list.each() {       println it}

Map

Map pizzaPrices = [margherita:10, pepperoni:12]

access:

pizzaPrices.get('pepperoni')pizzaPrices['pepperoni']pizzaPrices.pepperoni

Groovy in Gradle

apply plugin: 'com.android.application'

原形:

project.apply([plugin: 'com.android.application'])
dependencies {       compile 'com.google.code.gson:gson:2.3'}

原形:

project.dependencies({       add('compile', 'com.google.code.gson:gson:2.3', {           // Configuration statements       })})

dependencies实际上是Project的一个方法,原形为:

void dependencies(Closure configureClosure)Configures the dependencies for this project.This method executes the given closure against the DependencyHandler for this project. The DependencyHandler is passed to the closure as the closure's delegate.

那么delegate又是什么呢?

delegate对象是什么意思?当你在Script中操作一些不是Script自己定义的变 量,或者函数时候,gradle 会到 Script 的 delegate 对象去找,看看有没有定义这些 变量或函数。

在闭包中执行add方法的时候,自己Project没有啊,但DependencyHandler中有:
原形:

Dependency add(String configurationName,             Object dependencyNotation,             Closure configureClosure)Adds a dependency to the given configuration, and configures the dependency using the given closure.Parameters:configurationName - The name of the configuration.dependencyNotation - The dependency notation, in one of the notations described above.configureClosure - The closure to use to configure the dependency.Returns:The dependency.

Tasks

Tasks 属于Project 对象,并且每个task对象都是Task接口的实现。
创建有用的Task,需要给他添加一些actions.

task hello {     println 'Hello, world!'}

我们知道 Gradle build 有三阶段:初始化,配置,执行。
但上面这种写法实际上打印动作会在配置阶段执行,这种情况下,即使你执行另一个Task,它也会打印。
如果这样添加actions的话:

 task hello << {     println 'Hello, world!'}

那么实际上它的原形是什么呢?

task(hello) << {     println 'Hello, world!'}   task('hello') << {     println 'Hello, world!'}

实际是调用的是Project的task()方法,传入了两个参数,一个是task的名称,一个是闭包

   tasks.create(name: 'hello') << {     println 'Hello, world!'}

这是另一种task的实现形式,利用tasks对象(TaskContainer的实例)的create方法传入一个Map和闭包。

Task结构

每个Task包含了Action对象的集合。当Task被执行的时候,其内部的Action集合会按次序逐个执行,所以借助doFirst(),doLast()等方法来控制Action在队列中的顺序,同时也是执行的顺序。
上面的“<<”是doFirst()的缩写方式。

task hello {     println 'Configuration'     doLast {       println 'Goodbye'}     doFirst {       println 'Hello'} }

当有两个Task : task1 和 task2 ,当定义task2.mustRunAfter() task1;
表示task2应在task1之后执行。
和dependsOn的区别是,后者会触发task1的执行

task2.dependsOn task1

如何Hook into Android插件

操作build variants

android.applicationVariants.all { variant ->     // Do something}

通过遍历applicationVariants中的build variants,你可以修改属性,名字,描述,等等,如果是Android库,那么就将applicationVariants替换为libraryVariants。

eg:

android.applicationVariants.all { variant ->     variant.outputs.each { output ->       def file = output.outputFile       output.outputFile = new File(file.parent,      file.name.replace(".apk", "-${variant.versionName}.apk"))} }

注:遍历为什么不用each而用all?

This is necessary because each() is triggered in the evaluation phase before
the build variants have been created by the Android plugin.
The all() method, on the other hand, is triggered every time
a new item is added to the collection.

创建自定义插件

如何实现gradle tasks 在多个工程中的复用呢?
插件可以用Groovy ,java,scala 等能够在JVM运行的语言,Android插件就是Java和Groovy结合写出来的。

第一步:继承Plugin 接口

class RunPlugin implements Plugin<Project> {     void apply(Project project) {        project.android.applicationVariants.all { variant -> if (variant.install) {                project.tasks.create(name: "run${variant.name.capitalize()}", dependsOn: variant.install) {               // Task definition           }} }} }

可以看到apply里传入了一个Project对象,插件自身是无法hook into android gradle插件的编译过程中的,而是通过Project.

第二步:发布插件

为了能够被复用,插件需要被移入到一个单独的模块中,在这个模块有自己的build.gradle(配置依赖及发布工具):

apply plugin: 'groovy'   dependencies {       compile gradleApi()       compile localGroovy()}

这个单独的插件模块会产生JAR文件(包含了插件的classes和属性),这个JAR就是插件被复用的基础。

插件的项目结构:

这里写图片描述
目录groovy下存储的插件的代码;目录resources下存储的是插件属性。

如何让Gradle识别到插件

需要添加属性文件到src/main/resources/META-INF/gradle-plugins/下
比如添加文件:com.gradleforandroid.run.properties:
内容添加为:

   implementation-class=com.gradleforandroid.RunPlugin

com.gradleforandroid.RunPlugin是插件包名 + 插件类名

编译

gradlew assemble 

高级定制化编译

减少APK文件大小

ProGuard

其不仅可以减少大小,还可优化,混淆,预校验代码。寻找没有用到的code并且删除掉。
将minifyEnabled 置true即可启用。

Shrinking(消减) 资源

两种场合:
* 无用的资源文件忘记删掉了
* 导入了一个第三方库,其包含了大量的资源,但是实际只用了小部分而已。

自动消减

将shrinkResources置true,同时还得将minifyEnabled置true;

minifyEnabled = trueshrinkResources = true

通过执行:

gradle :library:shrinkReleaseResources

gradlew clean assembleRelease --info

能看出来被消减资源的数量。

不利的地方:
动态加载的资源很容易被误杀,所以需要在res/raw/增加一个keep.xml文件。

<?xml version="1.0" encoding="utf-8"?>   <resources xmlns:tools="http://schemas.android.com/tools"tools:keep="@layout/keep_me,@layout/also_used_*"/>

手动消减

  • 只保留用到的语言资源
android {       defaultConfig {           resConfigs "en", "da", "nl"       }}
  • 只保留用到的屏幕密度资源
android {       defaultConfig {           resConfigs "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"       }}

上面两个是可以混合的。

加速编译

注:以下所有都是在项目根目录的gradle.properties文件中添加

 org.gradle.parallel=true

gradle任务开启后,会启动一个守护进程,该守护进程会在3h的空闲后自动结束掉,这对于频繁的启动gradle任务来说,减少了启动进程的损耗。AS会自动开启。但是如果是命令行启动的话,该项默认是关闭的。

org.gradle.daemon=true

能影响编译速度的是Xms和Xmx.
Xms:初始使用的内存大小 默认没有设置
Xmx:使用内存最大值 默认为256M

org.gradle.jvmargs=-Xms256m -Xmx1024m

当项目中多个模块的场景下(对单个模块的简单项目来说不管用):

org.gradle. configureondemand=true

其限制耗费在configuration阶段上的时间,如果被设为true,Gradle在执行configuration阶段前,先看看那些模块的configuration发生了变化,哪些没有。

系统级别的gradle.properties

在HOME目录下~/.gradle 增添gradle.properties,将上面的配置全部都放到这个属性文件中,而不是放到项目属性文件中。这是因为对于编译服务器来说,关键需要内存消耗降下来,而不是编译时间。

新的编译工具Jack and Jill

要求 编译工具版本在21.1.1之上,Gradle在1.0.0之上。

android {       buildToolsRevision '22.0.1'       defaultConfig {         useJack = true       }}

Jack (Java Android Compiler Kit) is a new Android build toolchain that compiles Java source code directly to the Android Dalvik executable (dex) format. It has its own .jack library format and takes care of packaging and shrinking as well. Jill (Jack Intermediate Library Linker) is a tool that can convert .aar and .jar files to .jack libraries.

二者都处于试验阶段,bug采集期。虽然能够大幅改善编译速度。

该文http://www.2cto.com/kf/201511/449234.html 对编译过程有详细描述,可有空学习之。

编译时间量化

执行gradle任务后加上–profile,会在build/ reports/profile生成HTML 报告。里面将所有阶段所耗费的时间都量化出来了。

Tips:

you can execute assembleDebug by running gradlew assDeb, or even gradlew aD, from the command-line interface.

如果没有wrapper,执行:
gradle wrapper –gradle-version {gralde版本}

问题:

1.

我们知道每个build.gradle文件其实都是Project的一个实例,但是Project.java只是一个接口而已,里面看不到任何实质的内容,那么我们可利用什么方式能了解到每个build.gradle文件对应Project实例真正干了些什么呢?

3 0