构建 APK 的过程是个相当复杂的过程,Android 构建系统需要将应用的资源文件和源文件一同打包到最终的 APK 文件中。应用可能会依赖一些外部库,构建工具要灵活地管理这些依赖的下载、编译、打包(包括合并、解决冲突、资源优化)等过程。应用的源码可能包括 Java 、RenderScript、AIDL 以及 Native 代码,构建工具需要分别处理这些语言的编译打包过程,而有些时候我们需要生成不同配置(如不同 CPU 架构、不同 SDK 版本、不同应用商店配置等)的 APK 文件,构建工具就需要根据不同情况编译打包不同的 APK。总之,构建工具需要完成从工程资源管理到最终编译、测试、打包、发布的几乎所有工作。而 Android Studio 选择了使用 Gradle,一个高级、灵活、强大的自动构建工具构建 Android 应用,利用 Gradle 和 Android Gradle 插件可以非常灵活高效地构建和测试 Android 应用了:
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应用结构为:
位于工程根目录的 settings.gradle
文件用于告诉Gradle构建应用时需要包含哪些 module,如 :
include ':app', ':lib'
位于工程根目录的 build.gradle
文件用于定义工程所有 module 的构建配置,一般顶层 build 文件使用 buildscript
代码块定义 Gradle 的 repositories 和 dependencies,如自动生成的顶层 build 文件:
/** * 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 共享)的属性:
buildscript {...} allprojects {...} ext { // 如让所有 modules 都使用相同的版本以避免冲突 compileSdkVersion = 26 supportLibVersion = "27.0.2" ... } ...
每个 module 的 build 文件使用 rootProject.ext.property_name
语法使用这些属性即可:
android { compileSdkVersion rootProject.ext.compileSdkVersion ... } ... dependencies { compile "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}" ... }
位于每个 project/module/
目录的 build.gradle
文件用于定义该 module 自己的构建配置,同时你也可以重写顶层 build 文件或 main app manifest 的配置:
/** * 为这个构建应用 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.properties
文件和 local.properties
用来指定 Gradle 构建工具自己的设置。
gradle.properties
文件可以用来配置工程的 Gradle 设置,如 Gradle 守护进程的最大堆栈大小
local.properties
文件用来配置构建系统的本地环境属性,如 SDK 安装路径,由于该文件内容是 Android Studio 自动生成的且与本地开发环境有关,所以你不要更改更不要上传到版本控制系统中。
Gradle 是专注于灵活性和性能的开源自动构建工具。Gradle 的构建脚本使用 Groovy 或 Kotlin 语言。Gradle 构建工具的优势在于:
学习 Gradle 的途径有很多:
依赖管理(Dependency management)是每个构建系统的关键特征,Gradle 提供了一个既容易理解又其他依赖方法兼容的一流依赖管理系统,如果你熟悉 Maven 或 Ivy 用法,那么你肯定乐于学习 Gradle,因为 Gradle 的依赖管理和两者差不多但比两者更加灵活。Gradle 依赖管理的优势包括:
Java Library插件 继承自 Java插件 ,但 Java Library 插件与 Java 插件最主要的不同是 Java Library 插件引入了将 API 暴露给消费者(使用者)的概念,一个 library 就是一个用来供其他组件(component)消费的 Java 组件。
Java Library 插件暴露了两个用于声明依赖的 Configuration (依赖配置): api
和 implementation
。出现在 api
依赖配置中的依赖将会传递性地暴露给该 library 的消费者,并会出现在其消费者的编译 classpath 中。而出现在 implementation
依赖配置中的依赖将不会暴露给消费者,也就不会泄漏到消费者的编译 classpath 中。因此, api
依赖配置应该用来声明library API 使用的依赖,而 implementation
依赖配置应该用来声明组件内部的依赖。 implementation
依赖配置有几个明显的优势:
implementation
那到底什么时候使用 API 依赖什么时候使用 Implementation 依赖呢?这里有几个简单的规则:
一个 API 是 library binary 接口暴露的类型,通常被称为 ABI (Application Binary Interface),这包括但不限于:
相反,下面列表重要到的所有类型都与 ABI 无关,因此应该使用 implementation 依赖:
例如
// 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 文件这样写:
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 变体中可用,而如 testImplementation
、 androidTestImplementation
等依赖配置可以更好地处理测试相关依赖。
现在的软件工程很少单独地构建代码,因为现在的工程通常为了重用 已存在且久经考验的功能 而引入外部库,因此被称为 binary dependencies 。Gradle 会解析 binary 依赖然后从专门的远程仓库中下载并存到 cache 中以避免不必要的网络请求:
每个 artifact 在仓库中的 coordinate 都会包含 groupId 、 artifactId 和 version 三个元素,如在一个使用 Spring 框架的 Java 工程中添加一个编译时依赖:
apply plugin: 'java-library' repositories { mavenCentral() } dependencies { implementation 'org.springframework:spring-web:5.0.2.RELEASE' }
Gradle 会从 Maven中央仓库 解析并下载这个依赖(包括它的传递依赖),然后使用它去编译 Java 源码,其中的 version 属性是指定了具体版本,表明总是使用这个具体的依赖不再更改。
当然,如果你总是想使用最新版本的 binary 依赖,你可以使用动态的 version ,Gradle 默认会缓存 24 小时:
implementation 'org.springframework:spring-web:5.+'
有些情况开发团队在完全完成新版本的开发之前为了让使用者能体验最新的功能特色,会提供一个 changing version,在 Maven 仓库中 changing version 通常被称作 snapshot version,而 snapshot version会包含 -SNAPSHOT
后缀,如:
implementation 'org.springframework:spring-web:5.0.3.BUILD-SNAPSHOT'
工程有时候不会依赖 binary 仓库中的库,而是把依赖放在共享磁盘或者版本控制系统的工程源码中(JFrog Artifactory 或 Sonatype Nexus 可以存储解析这种外部依赖),这种依赖被称为 file dependencies ,因为它们是以不涉及任何 metadata(如传递依赖、作者)的文件形式存在的。如我们添加来自 ant
、 libs
和 tools
目录的文件依赖:
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 中编译。
project(':web-service') { dependencies { implementation project(':utils') implementation project(':api') } }
强制所有的 android support libraries 使用相同的版本:
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 文件名:
android.applicationVariants.all { variant -> variant.outputs.all { outputFileName = "${variant.name}-${variant.versionName}.apk" } }
如果开启了 Multidex 后在 Android 5.0 以下设备上出现了 java.lang.NoClassDefFoundError
异常,可能是由于构建工具没能把某些依赖库代码放进主 dex 文件中,这时就需要手动指定还有哪些要放入主 dex 文件中的类。在构建类型中指定 multiDexKeepFile
或 multiDexKeepProguard
属性即可:
在 build 文件同级目录新建 multidex-config.txt
文件,文件的每一行为类的全限定名,如:
com/example/MyClass.class com/example/MyOtherClass.class
android { buildTypes { release { multiDexKeepFile file('multidex-config.txt') ... } } }
或者新建 multidex-config.pro
文件,使用 Proguard 语法指定放入主 dex 文件中的类,如:
-keep class com.example.** { *; }
android { buildTypes { release { multiDexKeepProguard file('multidex-config.pro') ... } } }