1. Android构建系统
介绍
Android的构建系统用于编译应用资源和源代码,然后将它们打包成可以测试或者发布的 APK。Android Studio 使用 Gradle 这一高级构建工具包来自动化执行和管理构建流程,同时也允许自定义构建配置。
构建流程
Android官网给我们提供了APK编译构建的流程:
- 编译器源代码转换成 DEX(Dalvik Executable) 文件(其中包括运行在 Android 设备上的字节码),将所有其他内容转换成已编译资源。
- APK 打包器将 DEX 文件和已编译资源合并成单个 APK。
- APK 打包器使用调试或发布密钥库签署APK
- 在生成最终 APK 之前,打包器会使用 zipalign 工具对应用进行优化,减少其在设备上运行时的内存占用
2. AndroidStudio中的Gradle
构建Android APK 需要借助Gradle和Gradle Android插件。Gradle构建脚本使用域特定语言 (DSL) 以 Groovy 语言描述和操作构建逻辑,Groovy是一种适用于 Java 虚拟机 (JVM) 的动态语言。Android Plugin for Gradle 引入了您需要的大多数 DSL 元素。
AS中的Gradle构建配置文件
上图是在AS中新建一个project所产生的文件,大致可以分为5部分来讲:
- Gradle设置文件 settings.gradle
- 顶级构建文件
- 模块级构建文件
- Gradle属性文件
- 其他文件
settings.gradle
文件位于项目根目录,用于指示Gradle在构建应用时应将哪些模块包括在内。如果是多个Module的话则需要把每个Module都加入到这个文件中。该文件包含的内容如下:
include ‘:app’
顶级构建文件 build.gradle
顶级 build.gradle 文件位于项目根目录,用于定义适用于项目中所有模块的构建配置。 默认情况下,此顶级构建文件使用 buildscript 代码块来定义项目中所有模块共用的 Gradle 存储区和依赖项。 以下代码示例描述的默认设置和 DSL 元素可在新建项目后的顶级 build.gradle 文件中找到。
/**
* 这段构建脚本是用来配置Gradle相关的仓库和依赖关系的,module相关的不能定义在这里
* */
buildscript {
/**
* The repositories block configures the repositories Gradle uses to
* search or download the dependencies. Gradle pre-configures support for remote
* repositories such as JCenter, Maven Central, and Ivy. You can also use local
* repositories or define your own remote repositories. The code below defines
* JCenter as the repository Gradle should use to look for its dependencies.
*
* New projects created using Android Studio 3.0 and higher also include
* Google's Maven repository.
*/
repositories {
google()
jcenter()
}
/**
* The dependencies block configures the dependencies Gradle needs to use
* to build your project. The following line adds Android plugin for Gradle
* version 3.3.2 as a classpath dependency.
*/
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
}
}
/**
* The allprojects block is where you configure the repositories and
* dependencies used by all modules in your project, such as third-party plugins
* or libraries. However, you should configure module-specific dependencies in
* each module-level build.gradle file. For new projects, Android Studio
* includes JCenter and Google's Maven repository by default, but it does not
* configure any dependencies (unless you select a template that requires some).
*/
allprojects {
repositories {
google()
jcenter()
}
}
多个module可以使用顶级构建文件中定义的属性来保持统一。
模块级配置文件 build.gradle
模块级 build.gradle 文件位于各 project/module/ 目录中,用于配置所在模块的构建设置。可以通过配置这些构建设置来提供自定义打包选项(例如附加构建类型和产品风格),以及替换 main/ 应用清单或顶级 build.gradle 文件中的设置。
/**
* The first line in the build configuration applies the Android plugin for
* Gradle to this build and makes the android block available to specify
* Android-specific build options.
*/
apply plugin: 'com.android.application'
/**
* The android block is where you configure all your Android-specific
* build options.
*/
android {
/**
* compileSdkVersion specifies the Android API level Gradle should use to
* compile your app. This means your app can use the API features included in
* this API level and lower.
*/
compileSdkVersion 28
/**
* buildToolsVersion specifies the version of the SDK build tools, command-line
* utilities, and compiler that Gradle should use to build your app. You need to
* download the build tools using the SDK Manager.
*
* This property is optional because the plugin uses a recommended version of
* the build tools by default.
*/
buildToolsVersion "28.0.3"
/**
* The defaultConfig block encapsulates default settings and entries for all
* build variants, and can override some attributes in main/AndroidManifest.xml
* dynamically from the build system. You can configure product flavors to override
* these values for different versions of your app.
*/
defaultConfig {
/**
* applicationId uniquely identifies the package for publishing.
* However, your source code should still reference the package name
* defined by the package attribute in the main/AndroidManifest.xml file.
*/
applicationId 'com.example.myapp'
// Defines the minimum API level required to run the app.
minSdkVersion 15
// Specifies the API level used to test the app.
targetSdkVersion 28
// Defines the version number of your app.
versionCode 1
// Defines a user-friendly version name for your app.
versionName "1.0"
}
/**
* The buildTypes block is where you can configure multiple build types.
* By default, the build system defines two build types: debug and release. The
* debug build type is not explicitly shown in the default build configuration,
* but it includes debugging tools and is signed with the debug key. The release
* build type applies Proguard settings and is not signed by default.
*/
buildTypes {
/**
* By default, Android Studio configures the release build type to enable code
* shrinking, using minifyEnabled, and specifies the Proguard settings file.
*/
release {
minifyEnabled true // Enables code shrinking for the release build type.
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
/**
* The productFlavors block is where you can configure multiple product flavors.
* This allows you to create different versions of your app that can
* override the defaultConfig block with their own settings. Product flavors
* are optional, and the build system does not create them by default.
*
* This example creates a free and paid product flavor. Each product flavor
* then specifies its own application ID, so that they can exist on the Google
* Play Store, or an Android device, simultaneously.
*
* If you declare product flavors, you must also declare flavor dimensions
* and assign each flavor to a flavor dimension.
*/
flavorDimensions "tier"
productFlavors {
free {
dimension "tier"
applicationId 'com.example.myapp.free'
}
paid {
dimension "tier"
applicationId 'com.example.myapp.paid'
}
}
/**
* The splits block is where you can configure different APK builds that
* each contain only code and resources for a supported screen density or
* ABI. You'll also need to configure your build so that each APK has a
* different versionCode.
*/
splits {
// Settings to build multiple APKs based on screen density.
density {
// Enable or disable building multiple APKs.
enable false
// Exclude these densities when building multiple APKs.
exclude "ldpi", "tvdpi", "xxxhdpi", "400dpi", "560dpi"
}
}
}
/**
* The dependencies block in the module-level build configuration file
* specifies dependencies required to build only the module itself.
* To learn more, go to Add build dependencies.
*/
dependencies {
implementation project(":lib")
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
主要代码块说明:
关于applicationId
applicationId是用于应用发布时的唯一ID,它和软件包名不一样,应用商店就是通过applicationId来区分是否为同一个应用。修改了applicationId对软件包名没有影响。
添加编译依赖项
只需要在 build.gradle 文件的 dependencies 程序块中指定依赖项配置,例如 implementation。依赖项大概可以分为三种:本地模块、本地二进制文件、远程二进制文件。
-
本地库模块依赖项
implementation project(':mylibrary')
这段代码声明名为“mylibrary”的 Android 库模块的依赖项(该名称必须匹配使用 settings.gradle 文件中的 include: 定义的库名称)。 在构建您的应用时,构建系统会编译库模块,并将生成的编译内容打包到 APK中。
-
本地二进制文件依赖项
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.aar'])
Gradle 声明项目 module_name/libs/ 目录中 JAR 文件的依赖项(因为 Gradle 会读取 build.gradle 文件的相对路径)。
-
远程二进制文件依赖项
implementation 'com.example.android:app-magic:12.3'
这段代码声明“com.example.android”命名空间组内“app-magic”库 12.3 版本的依赖项。
更详细的说明可以参考官网:添加构建依赖项
配置编译变体
-
buildTypes,用来配置编译的类型,比如编译release版本或者debug版本,用的比较多的属性就是minifyEnabled、debuggable、multiDexEnabled、applicationIdSuffix等;属性查询
android { defaultConfig { manifestPlaceholders = [hostName:"www.example.com"] ... } buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } debug { applicationIdSuffix ".debug" debuggable true } /** * The `initWith` property allows you to copy configurations from other build types, * then configure only the settings you want to change. This one copies the debug build * type, and then changes the manifest placeholder and application ID. */ staging { initWith debug manifestPlaceholders = [hostName:"internal.example.com"] applicationIdSuffix ".debugStaging" } } }
-
productFlavors,产品风味,强调不同的产品版本,比如付费和免费、不同的发布渠道
产品风格支持与 defaultConfig 相同的属性,这是因为 defaultConfig 实际上属于 ProductFlavor 类。android { ... defaultConfig {...} buildTypes { debug{...} release{...} } // Specifies one flavor dimension. flavorDimensions "version" productFlavors { demo { // Assigns this product flavor to the "version" flavor dimension. // This property is optional if you are using only one dimension. dimension "version" applicationIdSuffix ".demo" versionNameSuffix "-demo" } full { dimension "version" applicationIdSuffix ".full" versionNameSuffix "-full" } } }
-
更改默认源集配置
如果我们的源文件并不是Gradle 期望的那种文件结构,那么可以使用 sourceSets 代码块更改 Gradle 编译所需要的源文件位置。android { ... sourceSets { // Encapsulates configurations for the main source set. main { // Changes the directory for Java sources. The default directory is // 'src/main/java'. java.srcDirs = ['other/java'] // If you list multiple directories, Gradle uses all of them to collect // sources. Because Gradle gives these directories equal priority, if // you define the same resource in more than one directory, you get an // error when merging resources. The default directory is 'src/main/res'. res.srcDirs = ['other/res1', 'other/res2'] // Note: You should avoid specifying a directory which is a parent to one // or more other directories you specify. For example, avoid the following: // res.srcDirs = ['other/res1', 'other/res1/layouts', 'other/res1/strings'] // You should specify either only the root 'other/res1' directory, or only the // nested 'other/res1/layouts' and 'other/res1/strings' directories. // For each source set, you can specify only one Android manifest. // By default, Android Studio creates a manifest for your main source // set in the src/main/ directory. manifest.srcFile 'other/AndroidManifest.xml' ... } // Create additional blocks to configure other source sets. androidTest { // If all the files for a source set are located under a single root // directory, you can specify that directory using the setRoot property. // When gathering sources for the source set, Gradle looks only in locations // relative to the root directory you specify. For example, after applying the // configuration below for the androidTest source set, Gradle looks for Java // sources only in the src/tests/java/ directory. setRoot 'src/tests' ... } } } ...
如何加快构建速度
-
优化您的构建配置
-
保持工具处于最新状态
Android 工具几乎在每一次更新中都会获得构建优化和新功能,要充分利用最新优化,请保持以下工具处于最新状态。(Android Studio 和 SDK 工具、Android Plugin for Gradle) -
为开发创建构建变体
准备发布应用时需要的许多配置在开发应用时都不需要。启用不必要的构建进程会减慢您的增量构建和干净构建速度,因此,在开发您的应用期间,配置一个构建变体,使之仅包含您需要的构建配置。 -
避免编译不必要的资源
避免编译和打包您没有测试的资源(例如其他语言本地化和屏幕密度资源)。为此,您可以仅为“开发”风味指定一个语言资源和屏幕密度,如下面的示例中所示:android { ... productFlavors { dev { ... // The following configuration limits the "dev" flavor to using // English stringresources and xxhdpi screen-density resources. resConfigs "en", "xxhdpi" } ... } }
-
使用静态依赖项版本
在 build.gradle 文件中声明依赖项时,您应当避免在结尾将版本号与加号一起使用,应改为使用静态/硬编码版本号。 -
启用离线模式
如果您的网络连接速度比较慢,那么在 Gradle 尝试使用网络资源解析依赖项时,您的构建时间可能会延长。您可以指示 Gradle 仅使用它已经缓存到本地的工件来避免使用网络资源。要在使用 Android Studio 构建时离线使用 Gradle,请执行以下操作:
1. 点击 File > Settings(在 Mac 上,点击 Android Studio > Preferences),打开 Preferences 窗口。
2. 在左侧窗格中,点击 Build, Execution, Deployment > Gradle。
3. 勾选 Offline work 复选框。
4. 点击 Apply 或 OK。如果您正在从命令行构建,请传递 --offline 选项。
-
创建库模块
在应用中查找您可以转换成 Android 库模块的代码。通过这种方式将您的代码模块化可以让构建系统仅编译您修改的模块,并缓存这些输出以用于未来构建。这种方式也会让按需配置和并行项目执行更有效(如果您启用这些功能)。 -
增加 Gradle 的堆大小并启用 dex-in-process
Dex-in-process 在构建进程内而不是单独的 VM 进程中运行 DEX 编译器 - 这会同时加快增量构建和干净构建的速度。默认情况下,使用 Android Studio 2.1 及更高版本创建的新项目将向构建进程分配足够的内存来启用此功能。 -
将图像转换成 WebP
WebP 是一种既可以提供有损压缩(像 JPEG 一样)也可以提供透明度(像 PNG 一样)的图片文件格式,不过与 JPEG 或 PNG 相比,这种格式可以提供更好的压缩。降低图片文件大小可以加快构建的速度(无需执行构建时压缩),尤其是在您的应用使用大量图像资源时,更是如此。不过,在对 WebP 图像进行解压缩时,您可能会注意到设备的 CPU 使用率有小幅上升。 -
停用 PNG 处理
如果您无法(或者不想)将 PNG 图像转换成 WebP,仍可以通过在每次构建应用时停用自动图像压缩的方式加快构建速度。要停用此优化,请将以下代码添加到您的 build.gradle 文件中:android { ... aaptOptions { cruncherEnabled false } }
由于构建类型或产品风味不定义此属性,在构建发布版本的应用时,您需要将此属性手动设置为 true。
-
启用 Instant Run
Instant Run 可以在不构建新 APK 的情况下(某些情况下,甚至不需要重启当前 Activity)推送特定代码和资源更改,从而显著缩短更新您的应用所需的时间。
更多请参考:优化您的构建速度
-
-
分析构建
具体操作流程,参考官网链接:https://developer.android.com/studio/build/optimize-your-build?hl=zh-cn#profile
压缩代码和资源
-
压缩代码
要通过 ProGuard 启用代码压缩,需要在 build.gradle 内相应的构建类型中添加 minifyEnabled true。请注意,代码压缩会拖慢构建速度,因此您应该尽可能避免在调试构建中使用。不过,重要的是您一定要为用于测试的最终 APK 启用代码压缩,因为如果您不能充分地自定义要保留的代码,可能会引入错误。
android { buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } ... }
除了 minifyEnabled 属性外,还有用于定义 ProGuard 规则的 proguardFiles 属性:
- getDefaultProguardFile(‘proguard-android.txt’) 方法可从 Android SDK tools/proguard/ 文件夹获取默认的 ProGuard 设置。
- proguard-rules.pro 文件用于添加自定义 ProGuard 规则。默认情况下,该文件位于模块根目录(build.gradle 文件旁)。
-
压缩资源
资源压缩只与代码压缩协同工作。代码压缩器移除所有未使用的代码后,资源压缩器便可确定应用仍然使用的资源。这在添加了包含资源的代码库时体现得尤为明显 ---- 必须移除未使用的库代码,使库资源变为未引用资源,才能通过资源压缩器将它们移除。要启用资源压缩,请在 build.gradle 文件中将 shrinkResources 属性设置为 true(在用于代码压缩的 minifyEnabled 旁边)。
android { ... buildTypes { release { shrinkResources true minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } }
资源压缩器目前不会移除 values/ 文件夹中定义的资源(例如字符串、尺寸、样式和颜色)。这是因为 Android 资源打包工具 (AAPT) 不允许 Gradle 插件为资源指定预定义版本。
更多请参考:压缩代码和资源
多dex处理
- 64K引用限制
Andorid应用中的DEX规范将可在单个 DEX 文件内可引用的方法总数限制在 65,536,其中包括 Android 框架方法、库方法以及您自己代码中的方法。由于 65,536 等于 64 X 1024,因此这一限制也称为“64K 引用限制” - 规避64K限制
- 检查应用的直接和传递依赖项 - 确保在应用中使用任何庞大依赖库所带来的好处大于为应用添加大量代码所带来的弊端。一种常见的反面模式是,仅仅为了使用几个实用方法就在应用中加入非常庞大的库。减少您的应用代码依赖项往往能够帮助您规避 dex 引用限制。
- 通过 ProGuard 移除未使用的代码 - 为您的版本构建启用代码压缩以运行 ProGuard。启用压缩可确保您交付的 APK 不含有未使用的代码。
- 将应用配置成 Dalvik 可执行文件分包
android { defaultConfig { ... minSdkVersion 21 targetSdkVersion 28 multiDexEnabled true } ... }
Gradle属性文件
Gradle 还包括两个属性文件,均位于项目根目录中,可用于指定适用于 Gradle 构建工具包本身的设置:
-
gradle.properties
可以在其中配置项目范围级别的 Gradle 设置,例如 Gradle 后台进程的最大堆大小。 如需了解详细信息,请参阅构建环境
。 -
local.properties
为构建系统配置本地环境属性,例如 SDK 安装路径。 由于该文件的内容由 Android Studio 自动生成并且专用于本地开发者环境,因此您不应手动修改该文件,或将其纳入您的版本控制系统。
使用apk分析器分析您的构建
打开apk分析器的三种方式
- 把APK拖进 AS的编辑器窗口中
- 在Project窗口中切换到Project视图,然后双击构建出来的APK文件(默认目录:build/output/apks/)
- 在菜单栏中选择 Build > Analyze APK
查看文件和体检信息
APK分析器把apk中的文件和目录按照树形结构列了出来,每一个实体都会显示两种类型的大小,“Raw File Size”表示的是未压缩的文件大小,“Download Size”表示的是上传到Google Play之后预估压缩后的大小。
查看AndroidManifest.xml
查看DEX文件
APK分析器可以展示类的数量、apk自己定义的方法数量、引用的方法数量。引用方法数量包含了自己定义的方法、依赖库中的方法以及Java标准库和Android包被用到的方法。“Load Proguard mappings”可以将混淆过的apk恢复,查看方法更方便。
查看资源文件
APK文件比较
APK分析器还能比较两个不同apk文件的实体大小,它可以帮助我们分析两个不同版本的APK之间的大小差异。
Gradle的关键概念及编译周期
在解析 Gradle 的编译过程之前我们需要理解在 Gradle 中非常重要的两个对象。Project和Task。
每个项目的编译至少有一个 Project,一个 build.gradle就代表一个project,每个project里面包含了多个task,task 里面又包含很多action,action是一个代码块,里面包含了需要被执行的代码。
在编译过程中, Gradle 会根据 build 相关文件,聚合所有的project和task,执行task 中的 action。因为 build.gradle文件中的task非常多,先执行哪个后执行那个需要一种逻辑来保证。这种逻辑就是依赖逻辑,几乎所有的Task 都需要依赖其他 task 来执行,没有被依赖的task 会首先被执行。所以到最后所有的 Task 会构成一个 有向无环图(DAG Directed Acyclic Graph)的数据结构。
Android中Gradle的编译周期
编译过程分为三个阶段:
- 初始化阶段:创建 Project 对象,如果有多个build.gradle,也会创建多个project.
- 配置阶段:在这个阶段,会执行所有的编译脚本,同时还会创建project的所有的task,为后一个阶段做准备。
- 执行阶段:在这个阶段,gradle 会根据传入的参数决定如何执行这些task,真正action的执行代码就在这里.
参考
Android Studio配置构建
Gradle 完整指南(Android)
Gradle配置最佳实践
Android Gradle 看这一篇就够了
Gradle for Android 系列:为什么 Gradle 这么火