这本书很薄。只有 131 页,但是确实是蛮有价值的。因为很多时候,我们知道这些东西,但理解的很模糊,只是从网上拷贝代码去进行配置。例如多渠道打包,加速编译,生成报告等等。但是却不知道为什么。通过这本书可以去系统的学习。下面就记录下在这本书学到的一些东西。
1,Gradle 和 Android studio 入门
基础:
Gradle 有约定优于配置原则,即为设置和属性提供默认值。Gradle构建脚本的书写没有基于传统的XML文件,而是基于 Groovy 的领域专用语言( DSL )
项目和任务:
在 Gradle 中,最重要的两个概念是项目和任务。每一次构建都包括至少一个项目,每一个项目又包括一个或多个任务。。每个 build.gradle 文件都代表着一个项目,任务定义在构建脚本里。当初始化构建过程时,Gradle 会基于 build 文件组装项目和任务对象。一个任务对象包含一系列动作对象,这些动作对象之后会按照顺序执行。一个单独的动作对象就是一个待执行的代码块,和java中的方法类是。
构建生命周期:
执行一个Gradle构建最简单形式是,只执行任务中的动作,而这些任务又依赖于其他任务。为了简化构建过程,构建工具会新建一个动态的模型流,叫作 Directed Acyclic Graph ( DAG ) ( 有向非循环图 ) 这意味着所有任务都会被一个接一个地执行,循环是不可能的。
一个 Gradle 的构建通常有如下三个阶段。
- 初始化:项目实例会在该阶段被创建。
- 配置:在该阶段,构建脚本会被执行,并为每个项目实例创建和配置任务。
- 执行:在该阶段,Gradle将决定那个任务会被执行。那些任务被执行取决于开始该次构建的参数配置和该Gradle文件的当前目录。
构建配置文件
1 | buildscript { |
在 repositories 代码块中,jcenter 库被配置为整个构建过程的依赖仓库。JCenter 是一个预配置的 Maven 仓库,不需要额为配置。当然 Gradle 中有多个仓库供选择,你可以容易地添加自己的本地或者远程仓库。
项目结构
Gradle 使用一个叫做源集 ( sourece set )的概念。Gradle 的官方文档是这么解释的:一个源集就是一组源文件,它们会被一起执行和编译。
Gradle Wrapper 入门
Gradle 是一个不断发展的工具,新版本可能会打破向后兼容性,而使用 Gradle Wrapper 可以避免这个问题,并能确保构建是可重复的。
Gradle Wrapper 为不同的操作系统提供了脚本,到那个执行这段脚本的时候,需要的 Gradle 版本会被自动下载( 如果不存在 )和使用。其原理是,每个需要构建应用开发者或自构建系统可以仅仅运行 Wrapper,然后由 Wrapper 搞定剩余部分。
获取 Gradle Wrapper
每一个新的 Android 项目都会包含 Gradle Wrapper,当然也可以手动安装 Gradle 到电脑。
安装完后在 traminal 的运行./gradlew -v
来检查你的项目中的 Gradle Wrapper 是否可用。运行该命令行将会显示 Gradle 的版本号和一些额为的消息
如下所示:
下载和安装完,我们可以创建一个包含下面3行的 build.gradle 文件:
1 | task wrapper(type: Wrapper) { |
之后运行 gradle wrapper 来生成 Wrapper 文件。
执行以下操作
Buile Success 后会生成以下的文件:
1 | . |
其中,gradle-wrapper.properties文件包含参数配置,并能决定使用那一个 Gradle 版本:
1 |
|
运行基本的构建任务
在 mac 系统下是运行./gradlew
。Windows下是运行gradlew.bat
。下面例子都是基于 mac
./gradlew tasks
这个命令会列出所有的任务列表,如果加上--all
参数。那么你将获得每个任务对应依赖的详细介绍。
./gradlew assembleDebug
这个任务会为这个应用创建一个 debug 版本的 APK。
除了assemble
外。还有其他三个基本任务。
- Check:运行所以的检查,通常意味着一个在一个连接的设备或虚拟机上运行测试。
- Build:触发 assemble 和 check。
- Clean:清除项目的输出。
2,基本自定义构建
理解 Gradle 文件
当Android Studio创建一个一个新的项目后,会默认生成三个 Gradle 文件。其中两个文件 setting.gradle 和 build.gradle 位于项目的根目录。另外一个 build.gradle 文件则在 Android app 模块内被创建。
settings 文件
对于只有一个 Android 应用项目来说,setting.gradle 是这样:
1 | include ':app' |
这背后,Gradle 会为每个 settings 文件创建一个 Setting 对象,并调用该对象的相关方法。
顶级构建文件 build.gradle
1 |
|
allprojects 代码块可用来声明那些需要被用于所有模块的属性。甚至可以在 allproject 中创建任务,这些任务最终会被运用到所有模块。
模块的构建文件
1 |
|
主要有三个代码块
1.插件
第一行用到了 Android 应用插件,该插件在顶层构建文件中被配置成了依赖。
2.Android
之所以 android 代码块能使用。是因为之前使用了 Android 插件。defaultConfig 代码库用于配置应用的核心属性。此代码块中的属性可覆盖 AndroidManifest.xml 文件中对应的条目。
1 | defaultConfig { |
applicationId 是这段代码的第一个属性。该属性覆盖了 manifest 文件中的 package name,但 applicationId 和 package name 有一些不同。在 Gradle 被用作默认的 Android 构建系统系统之前,AndroidManifest.xml 中的 package name 有两个用途: 作为一个应用的唯一标志。以及R资源类中被用作包名。
使用构建 variants,Gradle 可更容易地创建不同版本的应用。不同版本需要独立的标识符,这样它们才能同时被安装。然后资源代码和生成的 R 类,必须在任何时候都保存相同包名,否则,所有源文件都需要随着你正在构建的版本而改变。这就是为了解耦 package name 两种不同用法的原因。
依赖包
定义了一个应用或依赖项目的所有依赖包。
任务入门
基础任务
Gradle 的 Android 插件使用了 Java 基础插件,而 Java 基础插件又使用了基础插件。基础插件定义了 assemble 和 clean 任务,Java 插件定义了 check 和 buildtasks 。
Android任务
Android 插件扩展了基本任务,并实现了它们的行为。
- assemble: 为每个构建版本创建一个APK。
- clean: 删除所有的构建内容,例如APK文件。
- check: 运行 Lint 检查,如果 Lint 发现一个问题,则可以终止构建。
- build: 同时运行 assemble 和 check。
assemble 任务默认依赖于 assembleDebug 和 assembleRelease,如果添加了更多的构建类型。那么就会有更多的任务。
Android插件还添加了一些新的任务。
- connectedCheck: 在连接设备或模拟器上运行测试。
- deviceCheck: 一个占位任务,专为其他插件在远端设备上运行测试
- installDebug 和 installRelease : 在连接的设备或模拟器上安装特定版本
- 所有的 install 都有对应的 uninstall 方法
运行 check 任务会生成一份 Lint 报告,Lint 报告会包含所有的错误和警告,以及一份详细说明和一个文档链接。该报告在 app/build/outputs目录下。名称为 lint-results.html
自定义构建
BuildConfig和资源
构建工具升级到17后,构建工具都会生成一个叫做 BuildConfig 的类。该类包含一个按照构建类型设置值的 DEBUG
常量等。如果有一部分代码你只想在 debugging 时期运行,比如 logging,那么DEBUG
会非常有用。可以通过 Gradle 来扩展该文件,这样 debug 和 release 时,就可以拥有不同的常量。这些常量可用于切换功能或设置服务器URL。
3,依赖管理
依赖仓库
Gradle 支持三种不同的依赖仓库: Maven,lvy 和静态文件或文件夹。
预定义依赖仓库
Gradle 预定义了三个 Maven 仓库: JCenter、Maven Central 和本地 Maven 仓库。
1 |
|
mavenCentral 和 jcenter 是两个有名的远程仓库,一般推荐用 JCenter,JCenter 是 Maven Central 的超集,而且 JCenter 还支持 HTTPS。
远程仓库
可以把插件和依赖库,放在自有的 Maven 或 Ivy 服务器上,而不是将它发布到 Mavean Central 或 JCetner。需要在 maven 代码块中添加URL,而且可以为一个仓库添加凭证的方法。
本地仓库
可以在自己的硬盘驱动或网络驱动器上运行 Maven 和 Ivy 仓库。要想在构建中添加本地仓库,只需要配置一个相对或绝对路径的 URL 即可
本地依赖
文件依赖
可以使用 Gradle 提供的 files 方法来添加 JAR 文件作为一个依赖。这样一个个很麻烦。可以直接添加整个文件夹。用 fileTree 方法。
1 |
|
原生依赖库
用 c 或者 C++ 编写的依赖库可以被编译为特定平台的原生代码。通常包含几个 .so 文件,可用于所有平台。所需做的就是在模块层创建一个 jniLibs 文件夹,然后为每个平台创建子文件夹,将 .so 文件放在适当的文件夹中。
依赖项目
1.创建和使用依赖项目模块
不同与应用 Android 应用插件,构建脚本需要应用 Android 依赖库插件:
1 |
|
如果在项目中创建了一个模块作为依赖项目,那么你需要在 settings.gradle 中添加该模块:
1 |
|
使用.aar文件
在构建依赖库时,模块目录下的build/output/aar/ 文件夹将会生成 .aar文件。
然后为了复用,可以将它放在新的项目下的 libs 下,然后选择 File -> new module -> import .jar/.aar package 给 import 进来。
语义化版本
在语义化版本中,版本数字的格式一般未 major.minor.patch,数字则按照下列规则以此增加:
- 当做不兼容的 API 变化时,major 版本增加
- 当以向后兼容方式添加功能时,minor 版本增加
- 当修复一些 bug 时,patch 版本增加
4,创建构建 Variant
构建类型和 product flavor 的结合结果被称之为构建 variant。
构建类型
在 Gradle 的 Android 插件中,构建类型通常被用来定义如何构建一个应用或依赖库。
1 |
|
新模块的默认 build.gradle 文件配置了一个构建类型,叫 release。该构建类型用于禁用清除无用的资源( 通过设置 minifyEnabled 为 false )和定义默认的 ProGuard 配置文件的位置。
创建构建类型
新的构建类型只需在 buildTypes 代码块中新增一个对象即可。
1 |
|
staging 构建类型针对 applicationID 定义了一个新的后缀,使其和 debug 以及 release 版本的 applicationID 不一样。buildConfigField 属性使用一个构建配置变量,为 API 定义了一个自定义 URL。切换到 staging 就可以使用BuildConfig.API_URL
获取URL。
在创建一个新的构建类型时,还可以用另一个构建类型的属性来初始化该构建类型:
1 |
|
initWith()
方法创建了一个新的构建类型,并且复制了一个已经存在的构建类型的所有属性到新的构建类型中。然后我们可以通过新的构建类型对象中简单地定义来复写属性或者定义额外的属性。
源集
当创建一个新的构建类时,Gradle 也会创建一个新的源集。源集目录名称和构建类型相同。当使用不同的源集时,资源会被一种特殊的方式处理。Drawables 和 layout 文件将完全覆盖在 main 源集中有相同名称的资源,但是values 文件夹中的文件则不会被覆盖(例如 strings.xml )。Gradle 将合并构建类型中的资源到 main 资源中。
例如,在 main源集中有类似的 string.xml 文件
1 |
|
在 staging 中添加 strings.xml文件
1 |
|
那么在构建 staging 的时候就会合并文件。效果如下:
1 |
|
所以我们不需要整个拷贝过来。Android 插件将会合并这些。同理。manifest 也一样。
依赖
每个构建类型都可以有自己的依赖。如果只想在 debug 构建中添加一个 logging 框架。那么,可以这么写。这样只有在 debug 的时候才能调用 logging的代码。
1 |
|
product flavor
与被用来配置相同 App 或者 library 的不同构建类型相反,product flavor 被用来创建不同的版本。例如免费,收费。
创建 product flavor
1 |
|
product flavor 和构建类型相比有不同的属性。这是因为 product flavor 是 ProductFlavor类中的对象,就像存在于所有构建脚本的 defaultConfig 对象。这就意味着 defaultConfig 和你的所有 product flavors 享有相同的属性。
源集
和构建类型类是,product flavor 也可以拥有它们自己的源集目录。可以为一个特定构建类型和 flavor 的结合体创建一个文件夹。该文件夹的名称将是 flavor名称 + 构建类型的名称。合并文件夹的组成将比构建类型文件夹和 product flavor 文件夹拥有更高优先级。
构建 variant
构建 variant 是构建类型和 product flavor 结合的结果。
源集合并资源和 manifest
Gradle 的 Android 插件在打包应用之前将 main 源集和构建类型源集合并在一起。
资源和manifest的优先顺序如下:
Build type -> Flavor -> Main -> dependencies
variant 过滤器
在构建中,可以完全忽略某些 variant。通过这种方式,你就可以通过 assemble 命令来加快构建所有 variant 的进程,并且你的任务列表不会被任务无须执行的任务污染。
在 app 或 library 根目录下的 build.gradle 文件使用以下代码过滤 variants (备注。不是在 project 下的build.gradle)
1 |
|
这段代码是过滤掉release 下,blue 这个 flavor。过滤后,variant如下:
签名配置
不同的应用都需要不同的签名配置:
签名配置如下:
1 |
|
在不同的 flavor 使用不同的凭证,可以以相同的方式来定义他们:
1 |
|
5,管理多模块构建
在项目的 settings.gradle 文件中声明了所有模块,如下:
1 |
|
如果想给 app 模块添加 library 模块作为一个依赖,那么你需要将它添加到 app 模块的 build.gradle 文件中:
1 |
|
加速多模块构建
可以通过并行运行所有模块来使得构建过程更快。此功能默认不开启。
在项目的根目录的 gradle.properties 文件中配置 parallel 属性。
1 |
|
还有就是添加守护进程
1 |
|
还有加大 JVM 最大允许分配的堆内存和 JVM 最大允许分配的非堆内存
1 |
|
6,运行测试
单元测试 JUnit
可以运行./gradlew test
进行单元测试,如果所有测试都运行成功,则 Gradle 会显示 BUILD SUCCESSFUL。
一个失败会导致整个 test 任务失败,并且立即停止整个进程。这意味着,失败情况下,不是所有的测试用例都会被执行。如果想确保所有的构建 variant 都执行整套测试。使用 continue:
1 |
|
如果只针对某个测试类。可以像下面这样使用测试标识:
1 |
|
测试的任务还会生成报告,在 app/build/reports/tests/debug/index.html
测试覆盖率 Jacoco
只需要加这么一段代码:
1 |
|
当激活了测试覆盖率后,在执行 ./gradlew connectedCheck
会自动创建覆盖率报告。创建报告自身的任务是 createDebugCoverageReport
。但是它依赖connectedCheck
,所以不能分开执行它们。所以必须连接设备或者模拟器来生成一份测试报告。
执行成功后,会在 app/build/reports/coverage/debug/index.html 文件下找到覆盖率报告。
7,创建任务和插件
理解 Groovy
Groovy 是从 Java 衍生出来的,运行在 Java 虚拟机上的敏捷语言。
Java中,打印字符串到屏幕上,代码如下:
System.out.println("Hello,world!");
在 Groovy 中,则通过下面代码来实现:
println 'Hello,world!'
注意有以下不同点:
- 没有 System.out 命名空间
- 方法周围没有括号
- 一行末尾没有分号
对于字符串,你即可以使用单引号,也可以使用双引号,但他们有不同的用途。双引号字符串可以插入表达式。插值是评估包含占位符的字符串,并通过他们的值替换占位符的过程。这些占位符表达式可以是变量或方法。包含一个方法或者多个变量的占位表达式,需要使用 $
前缀并被花括号包裹。包含一个单独变量的占位符可以只含 $
前缀。
如下代码:
1 |
|
输出: Hello,Andy!
还允许动态执行代码。下面例子是打印当前日期:
1 |
|
输出: Sat Apr 08 14:44:25 CST 2017
类和成员变量
1 |
|
无论是类还是成员变量,都没有明确的访问修饰符。Groovy 中的默认访问修饰符与 Java 不同。类和方法一样,是共有的。而类成员却是私有的。
1 |
|
可以使用关键字 def来创建变量。一旦有一个类的新的实例,就可以用来操作它的成员变量。如果没有指定它的 get/set 依然可以使用每个成员变量的 get/set
方法
就好像使用变量一样。无效为你的方法定义一个特定的返回类型。但指定返回类型也是没有任何影响的。在 Groovy 中,方法的最后一行通常默认返回,即使没有使用 return 关键字。
如下:
1 |
|
返回 16
还可以这么写,用closure概念:
1 |
|
Closures
Closures 是匿名代码库,可以接受参数和返回值。他们可以被视为变量,被当做参数传递给方法。
1 |
|
添加 Closure 类型可以让每个使用该代码的人都能清楚地知道,一个 clousure 被定义。该例子引用了内含的未分类参数 it
的概念。如果你没有明确的给 clousure 指定一个参数。则 Groovy 会自动添加一个。这个参数通常被称为 it,你可以在所有的 clousure 中使用它。
在 Gradle 中,android 和 dependencies 代码块都是 closures。
集合
当在 Gradle 中使用 Groovy 时,有两个重要的集合类型: list 和 maps
1 |
|
it
概念下,还能这么写:
1 |
|
可以这么定义一个 map:
1 |
|
都是打印输出 12
可以通过点符号来获取 map 元素。
任务入门
自定义 Gradle tasks 可以显著提高一个开发者的日常。任务可以操作存在的构建进程,添加新的构建步骤,或影响构建输出。例如通过 hooking 到 Gradle 的 Android 插件,给一个生成的 APK 重命名。
定义任务:
1 |
|
执行后的输出:
1 |
|
“Hello world!” 在执行该任务之前就已被打印出来了。原因是 Gradle 构建的生产周期。在任一 Gradle 构建中,都有三个阶段、配置阶段和执行阶段。
所以上面的例子。添加了一个任务,实际上是在配置阶段。所以执行不同的任务,”Hello,world!” 也仍然会出现。
如果想在执行阶段给一个任务添加动作,则可以使用下面的表示法:
1 |
|
唯一的不同就是 closure 之前的<<
,其告知 Gradle,代码在执行阶段执行,而不是在配置阶段。
运行结果:
1 |
|
运行之后发现有警告,说 5.0 后会废弃这种做法。所以请用doLast
代码改成如下:
1 |
|
这样输出就不会再有警告了。
任务剖析
有doLast
也有doFirst
。所以看下面这段代码的执行顺序。
1 |
|
输出:
1 |
|
可以多次使用 doFirst 和 doLast() 如下所示:
1 |
|
运行结果:
1 |
|
注意:doFirst 总是添加一个动作到task的最前面,而 doLast 总是添加一个动作到最后面。
涉及排序,还有这个方法:mustRunAfter
,代码如下
1 |
|
这样执行 ./gradlew hello2 hello
,不管指定了什么顺序,hello 总是在 hello2 之前执行。
1 |
|
mustRunAfter()
方法不会添加任何依赖。如果一个任务依赖另一个,那么可以使用dependsOn
代码如下:
1 |
|
然后只执行 hello2 ./gradlew hello2
输出是:
1 |
|
Hook 到 Android 插件
Hook 到 Android 插件的方法之一是操控构建 variants。如下:
1 |
|
这里是通过 all()
来遍历构建 variant ,而不是之前提到的 each()
。这是因为 each()
会在构建 variant 被 Android 插件创建之前的评测阶段被触发。另外,all()
方法会在每次添加新项目到集合时触发。
自动重命名 APK
常用案例是操纵构建过程来重命名 APks,在它们打包后,添加版本号。可以遍历应用的构建 variant,来改变它们的输出属性 outputFile。代码如下:
1 |
|
编译输出后,apk 就带有版本号了
8,设置持续集成
持续集成(CI)是一个开发实践,需要一个团队的开发者,每天多次地整合他们的工作。每一次推送到主仓库都需要一次自动构建验证。
为Android设置CI的方式有许多种。常用以下系统
- Jenkins
- TeamCity
- Travis CI
这个配置就不具体深究了,一般都可以发布到第三方的应用托管提供给别人下载beta版本进行内测。例如蒲公英和fir等
9,高级自定义构建
减少 APK 文件大小
ProGuard
ProGuard是一个 Java 工具,其不仅可以缩减 APK 文件大小,还可以在编译器优化、混淆和预校验你的代码。
在 Gradle 的 Android 插件中,其构建类型下面有一个叫做 minifyEnabled 的布尔类型属性,你需要将它设置为 true来激活 ProGuard:
1 |
|
这样就可以在 proguard-rules.pro 这个文件写一些规则进行混淆等。
缩减资源
1,自动缩减
1 |
|
运行 gradlew clean assembleRelease -info
就会打印出 APK 缩减资源的预览
自动资源缩减有一个问题,即它可能移除了过多的资源,特别是那些被动使用的资源可能会被意外删除。为了防止这种情况发送。可以在res/raw/ 下创建一个叫做 keep.xml的文件去定义这些例外。
例如:
1 |
|
运行打包后,发现java82
这张图片不会被移除,注意,keep.xml 自身也会被移除出来。
手动缩减
例如包含多种语言,只想保留一两个。又或者说去除某个密度的图片。可以使用 resConfigs
属性来配置
以下这段代码就是保留 ch 这个语言包
1 |
|
打包输出:
1 |
|
加速构建
Gradle 参数
- 在gradle.properties 文件中启动并行构建,代码:
org.gradle.parallel=true
- 开启守护进程,后续构建复用该进程,代码:
org.gradle.daemon=true
- 调整 Java 虚拟机的参数来加速编译,代码:
org.gradle.jvmargs=-Xms512m -Xmx1024m
,其中xms
参数用来设置初始内存大小,Xmx
用来设置最大内存。
注意,可以在系统级别上配置 Gradle 属性,应用到所有 Gradle 项目。可以在 home 目录下的 .gradle文件夹内创建一个 gradle,properties 文件。因为这样是一个很好的做法,这样做的原因是,你通常想在构建服务器上降低内存消耗,而构建时间在服务器上并不是十分重要。
Profiling
执行一个Gradle 任务时,你可以通过 –profile 标志来实现这一点。当提供了该标志时,Gradle 会创建一份分析报告,其会告诉你构建过程中究竟是那一部分最耗时。报告最终生成在 build/reports/profile 下。报告如下:
忽略Lint
当通过 Gradle 执行一个 release 构建时,代码中就会执行一个 Lint 检查。在某些情况下会阻塞构建过程。所以可以禁用 abortOnError
1 |
|
高级应用部署
分割 APK
Apk solits 只影响应用的打包。编译、缩减、混淆等,仍然可以共享。这种机制允许你根据密度或 application binary interface (ABI) 来分割 APKs。
代码如下:
1 |
|
这段代码表示,忽略 ldpi 和 mdpi,如果只支持几种密度,则可以使用 include 来创建。在前面的代码中,compatibleScreens
属性是可选,并在 manifest 文件中注入了一个 matching节点。最后生成的APK: