Android 构建那些事

Posted by Frank on 2018-02-28

Android 构建系统

概述

构建 APK 的过程是个相当复杂的过程,Android 构建系统需要将应用的资源文件和源文件一同打包到最终的 APK 文件中。应用可能会依赖一些外部库,构建工具要灵活地管理这些依赖的下载、编译、打包(包括合并、解决冲突、资源优化)等过程。应用的源码可能包括 Java 、RenderScript、AIDL 以及 Native 代码,构建工具需要分别处理这些语言的编译打包过程,而有些时候我们需要生成不同配置(如不同 CPU 架构、不同 SDK 版本、不同应用商店配置等)的 APK 文件,构建工具就需要根据不同情况编译打包不同的 APK。总之,构建工具需要完成从工程资源管理到最终编译、测试、打包、发布的几乎所有工作。而 Android Studio 选择了使用 Gradle,一个高级、灵活、强大的自动构建工具构建 Android 应用,利用 Gradle 和 Android Gradle 插件可以非常灵活高效地构建和测试 Android 应用了:
The build process of a typical Android app module
Gradle和其Android插件可以帮助你自定义以下几方面的构建配置:

Build Types
: Build types(构建类型)定义了一些构建、打包应用时 Gradle 要用到的属性,主要用于不同开发阶段的配置,如 debug 构建类型要启用 debug options 并用 debug key 签名,release 构建类型要删除无效资源、混淆源码以及用 release key 签名

Product Flavors
: Product flavors(产品风味)定义了你要发布给用户的不同版本,比如免费版和付费版。你可以在共享重用通用版本功能的时候自定义 product flavors 使用不同的代码和资源,Product flavors 是可选的所以你必须手动创建

Build Variants
: build variant(构建变体)是 build type 和 product flavor 的交叉输出(如free-debug、free-release、paid-debug、paid-release),Gradle 构建应用要用到这个配置。也就是说添加 build types 或 product flavors 会相应的添加 build variants

Manifest Entries
: 你可以在 build variant 配置中指定 manifest 文件中的某个属性值(如应用名、最小 SDK 版本、target SDK 版本),这个值会覆盖 manifest 文件中原来的属性值

Dependencies
: 构建系统会管理工程用要用到的本地文件系统和远程仓库的依赖。

Signing
: 构建系统会让你指定签名设置以便在构建时自动给你的 APK 签名,构建工具默认会使用自动生成的 debug key 给 debug 版本签名,你也可以生成自己的 debug key 或 release key 使用。

ProGuard
: 构建系统让你可以为每个构建变体指定不同的混淆规则文件

Multiple APK Support
: 构建系统让你可以为不同屏幕密度或 Application Binary Interface (ABI)的设备生成包含其所需要的资源的 APK 文件,如为 x86 CPU 架构的设备生成只包含该 x86 架构 so 库的 APK 文件。

而这些构建配置要体现在不同的构建配置文件中,典型的Android应用结构为:
The default project structure for an Android app module

Gradle Settings 文件

位于工程根目录的 settings.gradle 文件用于告诉Gradle构建应用时需要包含哪些 module,如 :

1
include ':app', ':lib'

顶层 Build 文件

位于工程根目录的 build.gradle 文件用于定义工程所有 module 的构建配置,一般顶层 build 文件使用 buildscript 代码块定义 Gradle 的 repositories 和 dependencies,如自动生成的顶层 build 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* buildscript代码块用来配置Gradle自己的repositories和dependencies,所以不能包含modules使用的dependencies
*/

buildscript {

/**
* repositories 代码块用来配置 Gradle 用来搜索和下载依赖的仓库
* Gradle 默认是支持像 JCenter,Maven Central,和 Ivy 远程仓库的,你也可以使用本地仓库或定义你自己的远程仓库
* 下面的代码定义了 Gradle 用于搜索下载依赖的 JCenter 仓库和 Google 的 Maven 仓库
*/

repositories {
google()
jcenter()
}

/**
* dependencies 代码块用来配置 Gradle 用来构建工程的依赖,下面的代码表示添加一个
* Gradle 的 Android 插件作为 classpath 依赖
*/

dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
}
}

/**
* allprojects 代码块用来配置工程中所有 modules 都要使用的仓库和依赖
* 但是你应该在每个 module 级的 build 文件中配置 module 独有的依赖。
* 对于一个新工程,Android Studio 默认会让所有 modules 使用 JCenter 仓库和 Google 的 Maven 仓库
*/

allprojects {
repositories {
google()
jcenter()
}
}

除了这些,你还可以使用 ext 代码块在这个顶层 build 文件中定义工程级(工程中所有 modules 共享)的属性:

1
2
3
4
5
6
7
8
9
10
11
buildscript {...}

allprojects {...}

ext {
// 如让所有 modules 都使用相同的版本以避免冲突
compileSdkVersion = 26
supportLibVersion = "27.0.2"
...
}
...

每个 module 的 build 文件使用 rootProject.ext.property_name 语法使用这些属性即可:

1
2
3
4
5
6
7
8
9
android {
compileSdkVersion rootProject.ext.compileSdkVersion
...
}
...
dependencies {
compile "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
...
}

Module 级 Build 文件

位于每个 project/module/ 目录的 build.gradle 文件用于定义该 module 自己的构建配置,同时你也可以重写顶层 build 文件或 main app manifest 的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/**
* 为这个构建应用 Gradle 的 Android 插件,以便 android 代码块中 Android 特定的构建配置可用
*/

apply plugin: 'com.android.application'

/**
* android 代码块用来配置 Android 特定的构建配置
*/

android {

/**
* compileSdkVersion 用来指定 Gradle 用来编译应用的 Android API level,也就是说
* 你的应用可以使用这个 API level 及更低 API level 的 API 特性
*/

compileSdkVersion 26

/**
* buildToolsVersion 用来指定 SDK 所有构建工具、命令行工具、以及 Gradle 用来构建应用的编译器版本
* 你需要使用 SDK Manager 下载好该版本的构建工具
* 在 3.0.0 或更高版本的插件中。该属性是可选的,插件会使用推荐的版本
*/

buildToolsVersion "27.0.3"

/**
* defaultConfig 代码块包含所有构建变体(build variants)默认使用的配置,也可以重写 main/AndroidManifest.xml 中的属性
* 当然,你也可以在 product flavors(产品风味)中重写其中一些属性
*/

defaultConfig {

/**
* applicationId 是发布时的唯一指定包名,尽管如此,你还是需要在 main/AndroidManifest.xml 文件中
* 定义值是该包名的 package 属性
*/

applicationId 'com.example.myapp'

// 定义可以运行该应用的最小 API level
minSdkVersion 15

// 指定测试该应用的 API level
targetSdkVersion 26

// 定义应用的版本号
versionCode 1

// 定义用户友好型的版本号描述
versionName "1.0"
}

/**
* buildTypes 代码块用来配置多个构建类型,构建系统默认定义了两个构建类型: debug 和 release
* debug 构建类型默认不显式声明,但它包含调试工具并使用 debug key 签名
* release 构建类型默认应用了混淆配置
*/

buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

/**
* 由于 product flavors 必须属于一个已命名的 flavor dimension,所以你至少需要定义一个 flavor dimension
* 如定义一个等级和最小 api 的 flavor dimension
*/

flavorDimensions "tier", "minApi"

productFlavors {
free {
// 这个 product flavor 属于 "tier" flavor dimension
// 如果只有一个 dimension 那么这个属性就是可选的
dimension "tier"
...
}

paid {
dimension "tier"
...
}

minApi23 {
dimension "minApi"
...
}

minApi18 {
dimension "minApi"
...
}
}

/**
* 你可以使用 splits 代码块配置为不同屏幕分辨率或 ABI 的设备生成仅包含其支持的代码和资源的 APK
* 同时你需要配置 build 文件以便每个 APK 使用不同的 versionCode
*/

splits {
density {

// 启用或禁用构建多个 APK
enable false

// 构建多个 APK 时排除这些分辨率
exclude "ldpi", "tvdpi", "xxxhdpi", "400dpi", "560dpi"
}
}
}

/**
* 该 module 级 build 文件的 dependencies 代码块仅用来指定该 module 自己的依赖
*/

dependencies {
implementation project(":lib")
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:27.0.2'
}

Gradle 属性文件

位于工程根目录的 gradle.properties 文件和 local.properties 用来指定 Gradle 构建工具自己的设置。
gradle.properties 文件可以用来配置工程的 Gradle 设置,如 Gradle 守护进程的最大堆栈大小
local.properties 文件用来配置构建系统的本地环境属性,如 SDK 安装路径,由于该文件内容是 Android Studio 自动生成的且与本地开发环境有关,所以你不要更改更不要上传到版本控制系统中。

Gradle 概述

Gradle 是专注于灵活性和性能的开源自动构建工具。Gradle 的构建脚本使用 GroovyKotlin 语言。Gradle 构建工具的优势在于:

  • 高度可定制 - Gradle 是以最基本的可定制和可扩展的方式模块化的
  • 更快 - Gradle 通过重用之前执行的输出、只处理更改的输入以及并行执行 task 的方式加快构建速度
  • 更强大 - Gradle 支持跨多语言和平台,是 Android 官方构建工具,支持很多主流 IDE,包括 Android Studio、Eclipse、IntelliJ IDEA、Visual Studio 2017 以及 XCode,将来会支持更多语言和平台

学习 Gradle 的途径有很多:

Gradle 的依赖管理

依赖管理(Dependency management)是每个构建系统的关键特征,Gradle 提供了一个既容易理解又其他依赖方法兼容的一流依赖管理系统,如果你熟悉 Maven 或 Ivy 用法,那么你肯定乐于学习 Gradle,因为 Gradle 的依赖管理和两者差不多但比两者更加灵活。Gradle 依赖管理的优势包括:

  • 传递依赖管理 - Gradle 让你可以完全控制工程的依赖树
  • 支持非托管依赖 - 如果你只依赖版本控制系统或共享磁盘中的单个文件,Gradle 提供了强大的功能支持这种依赖
  • 支持个性化依赖定义 - Gradle 的 Module Dependencies 让你可以在构建脚本中描述依赖层级
  • 为依赖解析提供完全可定制的方法 - Gradle 让你可以自定义依赖解析规则以便让依赖可以方便地替换
  • 完全兼容Maven和Ivy - 如果你已经定义了 Maven POM 或 Ivy 文件,Gradle 可以通过相应的构建工具无缝集成
  • 可以与已存在的依赖管理系统集成 - Gradle 完全兼容 Maven 和 Ivy 仓库,所以如果你使用 Archiva、Nexus 或 Artifactory,Gradle 可以100%兼容所有的仓库格式

常用的依赖配置

Java Library插件 继承自 Java插件,但 Java Library 插件与 Java 插件最主要的不同是 Java Library 插件引入了将 API 暴露给消费者(使用者)的概念,一个 library 就是一个用来供其他组件(component)消费的 Java 组件。
Java Library 插件暴露了两个用于声明依赖的 Configuration(依赖配置): apiimplementation。出现在 api 依赖配置中的依赖将会传递性地暴露给该 library 的消费者,并会出现在其消费者的编译 classpath 中。而出现在 implementation 依赖配置中的依赖将不会暴露给消费者,也就不会泄漏到消费者的编译 classpath 中。因此,api 依赖配置应该用来声明library API 使用的依赖,而 implementation 依赖配置应该用来声明组件内部的依赖。implementation 依赖配置有几个明显的优势:

  • 依赖不会泄漏到消费者的编译 classpath 中,所以你也就不会无意中依赖一个传递依赖了
  • 由于 classpath 大小的减少编译也会更快
  • implementation 的依赖改变时,消费者不需要重新编译,要重新编译的很少
  • 更清洁地发布,当结合新的 maven-publish 插件使用时,Java librariy 会生成一个 POM 文件来精确地区分编译这个 librariy 需要的东西和运行这个 librariy 需要的东西

那到底什么时候使用 API 依赖什么时候使用 Implementation 依赖呢?这里有几个简单的规则:
一个 API 是 library binary 接口暴露的类型,通常被称为 ABI (Application Binary Interface),这包括但不限于:

  • 父类或接口用的类型
  • 公共方法中参数用到的类型,包括泛型类型(公共指的是对编译器可见的 public,protected 和 package private)
  • public 字段用到的类型
  • public 注解类型

相反,下面列表重要到的所有类型都与 ABI 无关,因此应该使用 implementation 依赖:

  • 只用在方法体内的类型
  • 只出现在 private 成员的类型
  • 只出现在内部类中的类型

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// The following types can appear anywhere in the code
// but say nothing about API or implementation usage
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.*;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

public class HttpClientWrapper {

private final HttpClient client; // private member: implementation details

// HttpClient is used as a parameter of a public method
// so "leaks" into the public API of this component
public HttpClientWrapper(HttpClient client) {
this.client = client;
}

// public methods belongs to your API
public byte[] doRawGet(String url) {
GetMethod method = new GetMethod(url);
try {
int statusCode = doGet(method);
return method.getResponseBody();

} catch (Exception e) {
ExceptionUtils.rethrow(e); // this dependency is internal only
} finally {
method.releaseConnection();
}
return null;
}

// GetMethod is used in a private method, so doesn't belong to the API
private int doGet(GetMethod method) throws Exception {
int statusCode = client.executeMethod(method);
if (statusCode != HttpStatus.SC_OK) {
System.err.println("Method failed: " + method.getStatusLine());
}
return statusCode;
}
}

其中,public 构造器 HttpClientWrapper 使用了 HttpClient 参数暴露给了使用者,所以属于 API 依赖。而 ExceptionUtils 只在方法体中出现了,所以属于 implementation 依赖。所以 build 文件这样写:

1
2
3
4
dependencies {
api 'commons-httpclient:commons-httpclient:3.1'
implementation 'org.apache.commons:commons-lang3:3.5'
}

因此,应该优先选择使用 implementation 依赖:缺少一些类型将会直接导致消费者的编译错误,可以通过移除这些类型或改成 API 依赖解决。
compileOnly 依赖配置会告诉 Gradle 将依赖只添加到编译 classpath 中(不会添加到构建输出中),在你创建一个 Android library module 且在编译时需要这个依赖时使用 compileOnly 是个很好的选择。但这并不能保证运行时良好,也就是说,如果你使用这个配置,那么你的 library module 必须包含一个运行时条件去检查依赖是否可用,在不可用的时候仍然可以优雅地改变他的行为来正常工作,这有助于减少最终 APK 的大小(通过不添加不重要的transient依赖)。
runtimeOnly 依赖配置告诉 Gradle 将依赖只添加到构建输出中,只在运行时使用,也就是说这个依赖不添加到编译 classpath 中。
此外,debugImplementation 会使依赖仅在 module 的 debug 变体中可用,而如 testImplementationandroidTestImplementation 等依赖配置可以更好地处理测试相关依赖。

声明依赖

声明 binary 依赖

现在的软件工程很少单独地构建代码,因为现在的工程通常为了重用已存在且久经考验的功能而引入外部库,因此被称为 binary dependencies。Gradle 会解析 binary 依赖然后从专门的远程仓库中下载并存到 cache 中以避免不必要的网络请求:
Resolving binary dependencies from remote repositories
每个 artifact 在仓库中的 coordinate 都会包含 groupIdartifactIdversion 三个元素,如在一个使用 Spring 框架的 Java 工程中添加一个编译时依赖:

1
2
3
4
5
6
7
apply plugin: 'java-library'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework:spring-web:5.0.2.RELEASE'
}

Gradle 会从 Maven中央仓库 解析并下载这个依赖(包括它的传递依赖),然后使用它去编译 Java 源码,其中的 version 属性是指定了具体版本,表明总是使用这个具体的依赖不再更改。
当然,如果你总是想使用最新版本的 binary 依赖,你可以使用动态的 version,Gradle 默认会缓存 24 小时:

1
implementation 'org.springframework:spring-web:5.+'

有些情况开发团队在完全完成新版本的开发之前为了让使用者能体验最新的功能特色,会提供一个 changing version,在 Maven 仓库中 changing version 通常被称作 snapshot version,而 snapshot version会包含-SNAPSHOT后缀,如:

1
implementation 'org.springframework:spring-web:5.0.3.BUILD-SNAPSHOT'

声明文件依赖

工程有时候不会依赖 binary 仓库中的库,而是把依赖放在共享磁盘或者版本控制系统的工程源码中(JFrog Artifactory 或 Sonatype Nexus 可以存储解析这种外部依赖),这种依赖被称为 file dependencies ,因为它们是以不涉及任何 metadata(如传递依赖、作者)的文件形式存在的。如我们添加来自 antlibstools 目录的文件依赖:

1
2
3
4
5
6
7
8
9
10
11
configurations {
antContrib
externalLibs
deploymentTools
}

dependencies {
antContrib files('ant/antcontrib.jar')
externalLibs files('libs/commons-lang.jar', 'libs/log4j.jar')
deploymentTools fileTree(dir: 'tools', include: '*.exe')
}

声明工程依赖

现在的工程通常把组件独立成 module 以提高可维护性及防止强耦合,这些 module 可以定义相互依赖以重用代码,而 Gradle 可以管理这些 module 间的依赖。由于每个 module 都表现成一个 Gradle project,这种依赖被称为 project dependencies 。在运行时,Gradle 构建会自动确保工程的依赖以正确的顺序构建并添加到 classpath 中编译。

1
2
3
4
5
6
project(':web-service') {
dependencies {
implementation project(':utils')
implementation project(':api')
}
}

Gradle 常用配置

强制所有的 android support libraries 使用相同的版本:

1
2
3
4
5
6
7
8
9
10
11
12
configurations.all {
resolutionStrategy {
eachDependency { details ->
// Force all of the primary support libraries to use the same version.
if (details.requested.group == 'com.android.support' &&
details.requested.name != 'multidex' &&
details.requested.name != 'multidex-instrumentation') {
details.useVersion supportLibVersion
}
}
}
}

更改生成的 APK 文件名:

1
2
3
4
5
android.applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "${variant.name}-${variant.versionName}.apk"
}
}

如果开启了 Multidex 后在 Android 5.0 以下设备上出现了 java.lang.NoClassDefFoundError 异常,可能是由于构建工具没能把某些依赖库代码放进主 dex 文件中,这时就需要手动指定还有哪些要放入主 dex 文件中的类。在构建类型中指定 multiDexKeepFilemultiDexKeepProguard 属性即可:
在 build 文件同级目录新建 multidex-config.txt 文件,文件的每一行为类的全限定名,如:

1
2
com/example/MyClass.class
com/example/MyOtherClass.class

1
2
3
4
5
6
7
8
android {
buildTypes {
release {
multiDexKeepFile file('multidex-config.txt')
...
}
}
}

或者新建 multidex-config.pro 文件,使用 Proguard 语法指定放入主 dex 文件中的类,如:

1
-keep class com.example.** { *; }

1
2
3
4
5
6
7
8
android {
buildTypes {
release {
multiDexKeepProguard file('multidex-config.pro')
...
}
}
}

参考