介绍
现在我们知道了Gradle如何工作,如何创建自己的Task以及Plugin,如何执行test任务,如何设置CI。这一章会包含一些小技巧,接下来会从以下Topic进行讨论:
- Reducing the APK file size
- Speeding up builds
- Ignoring Lint
- Advanced app deployment
Reducing the APK file size
APK文件的大小在最近几年都在疯长。有很多的原因,更多的Library,更多的Densities,App功能越来越强大。GooglePlay限制了APK大小50M,而一个更小的APK也就意味着用户会更快的下载和安装,并且减少内存空间的占用。
在这一节我们来看看如何通过Gradle构建配置来减少APK大小。
ProGuard
ProGuard除了可以shrink(压缩),也可以进行optimize(优化),obfuscate(混淆),在编译时期进行preverify(预验证)。它通过应用程序中的所有代码路径来查找未使用的代码并删除它。ProGuard也会重命名你的类和属性。这个过程会使得内存占用更小,更难逆向。
Android Plugin在buildType
中有一个Boolean的属性名为minifyEnabled
,可以设置成true启用Proguard:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
当你设置了minifyEnabled
为true后,proguardRelease
任务就会执行,并且在构建过程中调用ProGuard
。在启用了ProGuard之后,最好重新测试一下整个APP,有可能它仍然把你一些有用的代码都移除了,比如说JNI中调用的Java代码。为了解决这个问题,你可以定义ProGuard rules来把一些真正有用的代码保证不被移除。proguardFiles
属性就是用来定义包含了ProGuard Rules的文件。例如,要Keep一个雷,你可以如下定义:
-keep public class <MyClass>
getDefaultProguardFile('proguard-android.txt')````这个函数会获取
proguard-android.txt文件作为默认的ProGuard配置文件。而该文件就在Android SDK的
tools/proguard目录下。而
proguard-rules.pro```文件会默认添加到新的Android Modules中,所以你可以在Modules中进行简单的Rule配置。
具体的ProGuard配置,可以参照官网压缩代码和资源
Shrinking resources
Gradle和Android Plugin在App打包的时候,会把没用的资源都删掉。如果你有一个旧的资源没有删除,那么它就会默认帮你删除掉。另外一个方面是当你引用了很多资源的Library,但是你只是用一小部分,你可以通过启用resource shrinking的方式来移除。这有两种方式来压缩资源,自动或者手动
Automatic shrinking
如果设置了shrinkResources
属性为true的话,Android Build Tools将会自动的决定哪些资源是没用的,并且不把它们打包到APK中。
这个特性也有一个要求,就是你需要启用proGuard
。正因为Resource Shrinking工作了,Android Build Tools不能指出哪些资源是无用的,直到这些代码引用的资源全部被移除。
在BuildType中自动配置资源Shrinking:
android {
buildTypes {
release {
minifyEnabled = true
shrinkResources = true
}
}
}
如果你希望看到你的APK减少了多少,你可以执行shrinkReleaseResources
这个任务。这个任务会打印出来包大小减少了多少:
:app:shrinkReleaseResources
Removed unused resources: Binary resource data reduced from 433KB to 354KB: Removed 18%
你可以通过添加--info
标志位来获取移除的资源信息:
$ gradlew clean assembleRelease --info
当使用这个Flag的时候,Gradle会打印出在构建过程中的很多其他信息,包括最终没有打入APK包中的每一个资源。
而Automatic Resource Shrinking有一个问题,就是它可能会移除大量的资源。尤其是使用动态获取的资源,可能会被移除掉。为了避免这种情况,我们可以在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_*"/>
而这个keep.xml文件本身也不会被打入最终的包中。
Manual shrinking
减少资源的一种不极端的方案是减少多密度,多语言等文件。某些Library中包含了很多语言,例如Google Play Services。如果你的APP只想支持一个或者两个语言,而不想把所有的语言都打入最终的APK中。你可以使用resConfigs
属性来配置你希望保留的资源,而剩下的都会被丢弃。
如果你希望保存English,Danish,Dutch的字符串,你可以使用resConfigs
如下:
android {
defaultConfig {
resConfigs "en", "da", "nl"
}
}
你同样也可以在density进行选择:
android {
defaultConfig {
resConfigs "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"
}
}
它甚至可以组合Launguages和Density。实事上,每一种资源类型都可以通过这个属性进行配置。如果你不想配置ProGuard,或者你只想去掉不支持的语言和Density,那么使用resConfigs
是一个不错的选择。
Speeding up builds
Gradle的构建速度会比Ant长一些,因为Gradle在构建的生命周期中有三个阶段,而当你每次执行Task的时候,它都会经过这三个阶段。这会使得整个过程都很容易进行配置,但是确实会比较慢。不过,我们也有一些方法能够提升Gradle构建速度。
Gradle properties
一种提升速度的方法就是修改默认的设置。我们之前提到过parallel构建,你可以通过设置parallel属性来提升构建速度。
首先在Top-Level创建一个gradle.properties
文件。然后添加:
org.gradle.parallel=true
另外一种方式是启用Gradle Daemon。启用后,会在第一次启动构建的时候启动一个后台进程。当后续的构建启动时,都会使用这个后台进程,因此会节省一些启动的开销。这个进程会在你使用Gradle期间一直存在,而在空闲3个小时后关闭。使用Daemon在短时间内构建是非常有用的。你可以在gradle.properties
中添加:
org.gradle.daemon=true
在Android Studio中,Gradle Daemon是默认启用的。我这也就意味着在IDE中第一次启动构建后,后续的构建都会比较快。如果你从命令行执行构建的话,Gradle Daemon则是关闭的,除非在Properties中启用。
为了提升编译本身的速度,你可以设置JVM的参数。在Gradle的属性中,名为jvmargs
,可以用来为JVM启用设置内存分配的值。这两个参数也会对构建速度有直接的影响:Xms
和Xmx
。
-
Xms
:用来设置初始化使用的内存总值
-Xmx
:用来设置内存使用最大值
可以在gradle.properties
文件中添加:
org.gradle.jvmargs=-Xms256m -Xmx1024m
默认的Xmx是256M,而Xms没有设置。这两个选项都是基于你电脑的能力。
最后一个你可以配置的影响构建速度的属性是:org.gradle. configureondemand
。如果具有多个模块的复杂项目,则此属性特别有用,因为它试图通过跳过正在执行任务不需要的模块来限制配置阶段花费的时间。如果设置了这个属性,那么Gradle就会在配置阶段前,查出哪个模块的配置有修改,而哪个没有修改。
如果只有一个App或者Library工程的话,那么就不会有用。如果你有很多Module,并且比较复杂的话,那么这个属性可以节省很多构建时间
Profiling
如果你想看到是那部分过程拖慢了构建速度,那么可以在Gradle Task的时候通过添加--profile
标志位来获取一个Profile文件。当提供了这个标志位后,Gradle创建出了一个Profiling Report,可以从这个文件看到那部分的构建消耗了最多的时间。
这个Report会存在/build/reports/profile
文件夹下,而类型为HTML。以下为一个执行完多Module的构建任务的Report:
这个Profile Report展示了每个阶段执行任务时所消耗的时间。在上面的Summary是每个Module在Configuration阶段所耗费的时间。而Dependency Resolution
展示了每个模块解决依赖关系所耗费的时间。Task Execution
阶段包含了执行阶段的时间。而这个里面包含了每一个任务所执行的时间,从高到低排序。
Jack and Jill
如果你想要使用一个实验工具,那么Jack
和Jill
也可以提升构建速度。
Jack
:Java Android Compile Kit,它是Android Build Toolchain中的一个新工具。她可以编译Java代码直接到Dex格式。它有它自己的.jack
库格式,并且处理了打包和压缩。
Jill
:Jack Intermediate Library Linker,它是一个可以把.aar和.jar文件转换成.jack库的工具。不建议在Production版本中使用这两个工具。
你可以把Build Tool版本提升到21.1.1以上,Gradle版本提升到1.0.0版本以上,然后在defaultConfig
代码块中添加属性:
android {
buildToolsRevision '22.0.1'
defaultConfig {
useJack = true
}
}
你可以在一个buildType
或者ProductFlavor
中启用。这种方式,你可以继续使用常规的Build Toolchain,并且可以进行一个测试的构建。
android {
productFlavors {
regular {
useJack = false
}
experimental {
useJack = true
}
}
}
Ignoring Lint
当你执行一个Release构建的时候,Lint会检查你的代码。Lint是一个静态代码分析工具,可以标志出Java代码以及Layout的Bug。某些情况下,甚至会打断构建。如果你之前没用Lint,而现在想在Gradle中启用的话,Lint可能会报很多错误。至少能够让构建过程能够正常运转,你可能会让Gradle别处理Lint的错误。这只是一个临时方案,因为忽略Lint的错误可能会导致App Crash。
为了将Lint错误导致中断的问题避免,可以禁用掉abortOnError
:
android {
lintOptions {
abortOnError false
}
}
临时禁用可以使Ant工程可以更快的升级到Gradle中。
Advanced app deployment
之前我们通过BuildType
和Product Flavors
来构建多个版本的APP。然而在很多情况下,我们可以通过其他方法来达到目的。
Split APK
Build Variants能够被分成很多块,每一块都有自己的代码,资源,Manifest文件。APK Split,从另一方面来说,只会影响App的打包。编译,压缩,混淆等等流程还是会共享。这种机制允许你可以基于Density或者ABI来分割APK。
你可以在android
的配置项中通过定义一个splits
代码块配置分割。为了配置density分割,就可以创建一个density
代码块。如果希望按照ABI分割,则使用abi
代码块。
如果你启用了density分割,Gradle会为了每个density创建一个单独的APK。如果不需要density的话,你可以手动的exclude其中的densities,来提升构建速度。例如:
android {
splits {
density {
enable true
exclude 'ldpi', 'mdpi'
compatibleScreens 'normal', 'large', 'xlarge'
}
}
}
如果你不只支持一些densities,你可以使用include
来创建一个densities的白名单。为了使用include
,首先需要使用reset()
属性,该属性可以重置densities的列表为一个空的字符串。
compatibleScreens
属性是可选的,并且会在manifest文件中注入一段代码。这个配置可以让一个App支持大屏幕,而不支持小屏幕。
使用ABI
分割APK也是同样的,所有的属性都和density分割一样。在执行完density分割后的构建结果中:
app-hdpi-release.apk
app-universal-release.apk
app-xhdpi-release.apk
app-xxhdpi-release.apk
app-xxxhdpi-release.apk
如果你希望把这些APK发布到Google Play上的话,你就需要确保每个APK都有不同的版本号。这也就意味着,分割必须有一个单独的版本号。而我们可以通过applicationVariants
属性来完成:
ext.versionCodes = ['armeabi-v7a':1, mips:2, x86:3]
import com.android.build.OutputFile
android.applicationVariants.all { variant ->
// assign different version code for each output
variant.outputs.each { output ->
output.versionCodeOverride = project.ext.versionCodes.get(output.getFilter(OutputFile.ABI)) * 1000000 +android.defaultConfig.versionCode
}
}
这一段代码会检查ABI,并且保证每一个Variant都有一个单独的版本号。