Android Gradle Plugin源码解析

笔者最近在做公司项目的模块化重构,做的过程中一直在思考以下几个问题:

  • 一个apk文件和一个aar文件有什么区别?
  • 什么样的工程会导出一个apk,什么样的工程可以导出aar?
  • 一个apk的诞生伴随着哪些配置的过程,aar呢?
  • 他们俩之间可以快速的进行交换吗?

以上的这些疑问都在Google大大给我们开发的两个plugin中得到答案:

  • com.android.application
  • com.android.library

这是我们开发安卓应用时最常用的两个plugin,作为一个Android开发者,怎么可能不对它的实现不感兴趣呢,所以接下来我将用两到三个博客的内容,谈一谈读Android Gradle Plugin源码的一些心得。今天主要讲一些基础的部分。

源码下载方式

Android Gradle Plugin 一个版本的源码大概有30多个G,如果你的磁盘资源充足,可以使用repo的方式下载到本地,下面是2.3.0分支的一个示例:

1
2
3
4
$ mkdir gradle_2.3.0
$ cd gradle_2.3.0
$ repo init -u https://android.googlesource.com/platform/manifest -b gradle_2.3.0
$ repo sync

如果你没有翻墙的工具,可以使用国内的一些镜像:

repo的初始化可以参照 Google 教程 https://source.android.com/source/downloading.html

下载后的源码用IntelliJ IDEA打开tools的base路径,目录结构如下:
项目结构

主要代码在红框内的三个module中。在看Android Gradle Plugin的源码之前,我们先简单的看一下一个自定义的Gradle Plugin是如何实现的。

Gradle plugin简介

关于自定义一个 Gradle Plugin 的教程很多,我们简单的做一个说明。使用 gradle init 命令可以在当前目录下新建一个简单的gradle工程,目录结构如下:

gradle工程

这是一个基于Gradle Wrapper的多工程Gradle项目。在settings.gradle中可以配置子项目的路径,像我们在Android项目中经常配置的:

1
2
include ':app'
include ':lib'

说到这里可以多说一句在模块化开发中的经验,我们可以通过指定subproject的路径的方式,可以将本地任何路径下的代码导入工程中来,方便我们进行本地调试:

1
2
include ':lib'
project(':lib').projectDir = new File('xx/xx/xx/lib')

在rootproject的build.gradle文件中创建一个最简单的gradle plugin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class GreetingPluginExtension {
String message
String greeter
}

class GreetingPlugin implements Plugin<Project> {
void apply(Project project) {
def extension = project.extensions.create('greeting', GreetingPluginExtension)
project.task('hello') {
doLast {
println "${extension.message} from ${extension.greeter}"
}
}
}
}


apply plugin: GreetingPlugin

greeting {
message = 'Hi'
greeter = 'Gradle'
}

这段代码中,我们定义了一个GreetingPlugin,他新增了一个名为 ‘hello’ 的task,在终端输出一行信息,这个信息可以通过 GreetingPluginExtension 进行配置,我们执行一下:

1
2
3
4
5
6
7
8
9
./gradlew hello
Starting a Gradle Daemon (subsequent builds will be faster)
Parallel execution with configuration on demand is an incubating feature.
:hello
Hi from Gradle

BUILD SUCCESSFUL

Total time: 4.0 secs

其实也可以看出自定义一个plugin主要就是新增 Task 及所需参数进行配置的 Extension。Android Gradle Plugin定义了很多task,其中我们最常用的包括 clean build assemble 等,还有更多这些task运行时依赖的task,涉及到安卓编译打包的各个方面,我们在下一个博客中再具体阐述。今天主要Android Plugin的Extension的部分实现,这也是我们日常配置一个Android工程最主要的工作。

Extension机制

如何理解 Gradle 的 Extension,这涉及到Groovy的闭包委托特性。Groovy的闭包有this、owner、delegate三个属性,当你在闭包内调用方法时,由他们来确定使用哪个对象来处理。有关闭包的详情可以查看 Groovy Closures。利用Groovy的闭包委托特性,我们可以简单的实现Extension:

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
class Person {
String personName = '李四'
int personAge = 18
def printPerson(){
println "name is ${personName},age is ${personAge}"
}
}
def person(Closure<Person> closure){
Person p = new Person();
closure.delegate = p //委托模式优先
closure.setResolveStrategy(Closure.DELEGATE_FIRST);
return closure
}

def closure = person {
personName = '张三'
personAge = 20
printPerson()
}

task configClosure {
doLast {
closure()
}
}

首先我们定义一个Person对象,然后定义一个person的方法,它接受一个闭包作为参数,修改闭包委托模式优先,执行person方法,定义一个task执行这个闭包,运行结果如下:

1
2
3
4
5
6
7
8
/gradlew configClosure
Parallel execution with configuration on demand is an incubating feature.
:configClosure
name is 张三,age is 20

BUILD SUCCESSFUL

Total time: 0.981 secs

Gradle中大量使用了Extension这一特性,引用gradle一句原话:

Many Gradle objects are extension aware. This includes; projects, tasks, configurations, dependencies etc.

安卓插件使用的Extension都继承自BaseExtension:

1
2
3
4
5
6
7
* <ul>
* <li>Plugin <code>com.android.application</code> uses {@link AppExtension}
* <li>Plugin <code>com.android.library</code> uses {@link LibraryExtension}
* <li>Plugin <code>com.android.test</code> uses {@link TestExtension}
* <li>Plugin <code>com.android.atom</code> uses {@link AtomExtension}
* <li>Plugin <code>com.android.instantapp</code> uses {@link InstantAppExtension}
* </ul>

com.android.application插件使用的是AppExtension,com.android.library插件使用的是LibraryExtension。下面分别讲一下两个Extension的详细配置。

AppExtension

以下是AppExtension的所有配置,我按照使用频率进行一个简单的介绍,第一行是官方对于属性的介绍,我会针对每个属性做一些使用上的说明。

applicationVariants

The list of Application variants. Since the collections is built after evaluation, it should be used with Gradle’s all iterator to process future items.

applicationVariants是AppExtension继承自BaseExtension唯一拓展的成员变量,它的参数类型是DefaultDomainObjectSet,这是不同buildType及Flavor的集合,
applicationVariants最常用的是它的all方法,例如一个简单的修改apk名字的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def buildTime() {
return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}
android {
applicationVariants.all { variant ->
variant.outputs.each { output ->
def outputFile = output.outputFile
if (outputFile != null && outputFile.name.endsWith('.apk')) {
def fileName = "${variant.buildType.name}-${variant.versionName}-${buildTime()}.apk"
output.outputFile = new File(output.outputFile.parent,fileName)
}
}
}
}

buildToolsVersion

Required. Version of the build tools to use.

defaultConfig

Default config, shared by all flavors.

所有Flavor的默认设置,它是一个 ProductFlavor 对象,可以做以下设置:

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
defaultConfig {
applicationId '**.**.**' //The application ID.

applicationIdSuffix '.two' //applicationId的后缀,可以用在想同时安装运行两个Flavor包的时候,比如同时安装debug包和Release包做一些对比。

minSdkVersion 14

targetSdkVersion 25

versionCode 1

versionName "1.0"

versionNameSuffix ".0" // versionName后缀

consumerProguardFiles 'proguard-rules.pro' //用于Library中,可以将混淆文件输出到aar中,供Application混淆时使用。

dimension 'api' //给渠道一个分组加维度的概念,比如你现在有三个渠道包,分成免费和收费两种类型,可以添加一个dimension, 打渠道包的时候会自动打出6个包,而不需要添加6个渠道,详细的说明可见 https://developer.android.com/studio/build/build-variants.html#flavor-dimensions。

externalNativeBuild { //ndk的配置,AS2.2之后推荐切换到cmake的方式进行编译。
cmake {
cppFlags "-frtti -fexceptions"
arguments "-DANDROID_ARM_NEON=TRUE"
buildStagingDirectory "./outputs/cmake"
path "CMakeLists.txt"
version "3.7.1"
}
ndkBuild {
path "Android.mk"
buildStagingDirectory "./outputs/ndk-build"
}
}

javaCompileOptions {
annotationProcessorOptions { //注解的配置。
includeCompileClasspath true //需要使用注解功能。
arguments = [ eventBusIndex : 'org.greenrobot.eventbusperf.MyEventBusIndex' ] //AbstractProcessor中可以读取到该参数。
classNames
}
}

manifestPlaceholders = [key:'value'] //manifest占位符 定义参数给manifest调用,如不同的渠道id。

multiDexEnabled true //开启 multiDex

multiDexKeepFile file('multiDexKeep.txt') //手动拆包,将具体的类放在主DEX。

multiDexKeepProguard file('multiDexKeep.pro') //支持Proguard语法,进行一些模糊匹配。

ndk {
abiFilters 'x86', 'x86_64', 'armeabi' //只保留特定的api输出到apk文件中。
}

proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' //混淆文件的列表,如默认的android混淆文件及本地proguard文件,
//切记不要遗漏android混淆文件,否则会导致一些默认的安卓组件无法找到。

signingConfig {
//签名文件的路径
storeFile file('debug.keystore')
//签名文件密码
storePassword 'android'
//别名
keyAlias 'androiddebygkey'
//key的密码
keyPassword 'android'
}

buildConfigField('boolean','IS_RELEASE','false') //代码中可以通过BuildConfig.IS_RELEASE 调用。

resValue('string','appname','demo') //在res/value 中添加<string name="appname" translatable="false">demo</string>。

resConfigs "cn", "hdpi" //指定特定资源,可以结合productFlavors实现不同渠道的最小的apk包。
}

productFlavors

All product flavors used by this project.
渠道包的列表,可以覆盖defaultConfig的参数配置,形成自己的风味

flavorDimensionList

The names of flavor dimensions.
添加维度的定义,维度的使用上面defaultConfig已经有说明了。

resourcePrefix

A prefix to be used when creating new resources. Used by Android Studio.
在模块化开发中比较重要,给每个模块指定一个特定的资源前缀,可以避免多模块使用相同的文件命名后合并冲突,在build.gradle中指定了这个配置后,AS会检查不合法的资源命名并报错。

buildTypes

Build types used by this project.

buildType的列表,默认有release和debug,可以自己自定义不同的buildtype,相应的构建task name是 assemble+buildTypeName, buildType部分配置和defaultConfig相同,不同配置使用说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
debug {

applicationIdSuffix '.debug' //同defaultConfig

versionNameSuffix '.1' //同defaultConfig

debuggable true //生成的apk是否可以调试 debug默认是true release默认false

jniDebuggable true //是否可以调试NDK代码 使用lldb进行c和c++代码调试

crunchPngs true //是否开启png优化,会对png图片做一次最优压缩,影响编译速度,debug默认是false release默认true

embedMicroApp true //Android Wear的支持

minifyEnabled true //是否开启混淆

renderscriptDebuggable false //是否开启渲染脚本

renderscriptOptimLevel 5 //渲染脚本等级 默认是5

zipAlignEnabled true //是否zip对齐优化 默认就是true app对齐
}

ndkDirectory

The NDK directory used.
NDK路径,也可以在local.properties 中配置 ndk.dir=/Users/xxxx/Library/Android/sdk

sdkDirectory

The SDK directory used.
同ndkDirectory,目前一般配置在local.properties。

aaptOptions

Options for aapt, tool for packaging resources.
aapt是一个资源打包工具,可以对资源优化做一些动态配置:

1
2
3
4
5
6
7
8
9
10
11
aaptOptions{
additionalParameters '--rename-manifest-package',
'cct.cn.gradle.lsn13','-S','src/main/res2','--auto-add-overlay' //aapt执行时的额外参数

cruncherEnabled true //对png进行优化检查

ignoreAssets '*.jpg' //对res目录下的资源文件进行排除 把res文件夹下面的所有.jpg格式的文件打包到apk中

noCompress '.jpg' //对所有.jpg文件不进行压缩

}

adbExecutable

The adb executable from the compile SDK.
adb工具的文件路径,可以配置在环境变量中。

adbOptions

Adb options.
adb命令的一些配置

1
2
3
4
5
adbOptions   {
installOptions '-r' '-d' //调用adb install命令时默认传递的参数

timeOutInMs 1000 //执行adb命令的超时时间
}

compileOptions

Compile options.
编译配置

1
2
3
4
5
6
7
8
9
10
compileOptions{
encoding 'UTF-8' //java源文件的编码格式 默认UTF-8


incremental true //java编译是否使用gradle新的增量模式

sourceCompatibility JavaVersion.VERSION_1_7 //java源文件编译的jdk版本

targetCompatibility JavaVersion.VERSION_1_7 //编译出的class的版本
}

dataBinding

Data Binding options.
DataBinding的使用细节可以查看 Google文档,可以在build.gradle中开启DataBinding:

1
2
3
4
5
dataBinding  {
enabled = true //开启databinding
version = "1.0"
addDefaultAdapters = true
}

defaultPublishConfig

Name of the configuration used to build the default artifact of this project.
指定发布的渠道及BuildType类型。在Library中使用,默认Release。

signingConfigs

Signing configs used by this project.
一个签名配置的列表,可以供不同渠道和buildType使用。

lintOptions

Lint options.
Lint可以检查出代码中一些不规范的使用,如果想保留一些苟且的代码,可以参考以下配置(简友lyzaijs同学对这一块有详细说明,下面引用自他的博客):

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
lintOptions { 
quiet true // 设置为 true时lint将不报告分析的进度

abortOnError false // 如果为 true,则当lint发现错误时停止 gradle构建

ignoreWarnings true // 如果为 true,则只报告错误

absolutePaths true // 如果为 true,则当有错误时会显示文件的全路径或绝对路径

checkAllWarnings true // 如果为 true,则检查所有的问题,包括默认不检查问题

warningsAsErrors true // 如果为 true,则将所有警告视为错误

disable 'TypographyFractions','TypographyQuotes' // 不检查给定的问题id

enable 'RtlHardcoded','RtlCompat', 'RtlEnabled' // 检查给定的问题 id

check 'NewApi', 'InlinedApi' // * 仅 * 检查给定的问题 id

noLines true // 如果为true,则在错误报告的输出中不包括源代码行

showAll true // 如果为 true,则对一个错误的问题显示它所在的所有地方,而不会截短列表,等等。

lintConfig file("default-lint.xml") // 重置 lint 配置(使用默认的严重性等设置)。

textReport true // 如果为 true,生成一个问题的纯文本报告(默认为false)

textOutput 'stdout' // 配置写入输出结果的位置;它可以是一个文件或 “stdout”(标准输出)

xmlReport false // 如果为真,会生成一个XML报告,以给Jenkins之类的使用

xmlOutput file("lint-report.xml") // 用于写入报告的文件(如果不指定,默认为lint-results.xml)

htmlReport true // 如果为真,会生成一个HTML报告(包括问题的解释,存在此问题的源码,等等)

htmlOutput file("lint-report.html") // 写入报告的路径,它是可选的(默认为构建目录下的 lint-results.html )

checkReleaseBuilds true // 设置为 true, 将使所有release 构建都以issus的严重性级别为fatal(severity=false)的设置来运行lint,并且,如果发现了致命(fatal)的问题,将会中止构建(由上面提到的 abortOnError 控制.

fatal 'NewApi', 'InlineApi' //设置给定问题的严重级别(severity)为fatal (这意味着他们将会在release构建的期间检查 (即使 lint 要检查的问题没有包含在代码中)

error 'Wakelock', 'TextViewEdits' // 设置给定问题的严重级别为error

warning 'ResourceAsColor' // 设置给定问题的严重级别为warning

ignore 'TypographyQuotes' // 设置给定问题的严重级别(severity)为ignore (和不检查这个问题一样)
}

}

###dexOptions
Dex options.
Android dx工具是将java的classes文件编译为字节码dex文件,工具位于android sdk platform-tools目录,我们在做热修复做差分包的时候可能会用到这个工具,在android打包过程中,dx的可以做以下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dexOptions {
additionalParameters '--minimal-main-dex','--set-max-idx-number=10000' //dx命令附加参数

javaMaxHeapSize '2048m' //执行dx时java虚拟机可用的最大内存大小

jumboMode true //开启大模式,所有的class打到一个dex中,可以忽略65535方法数的限制,低于14版本不可运行

keepRuntimeAnnotatedClasses true //在dex中是否保留Runtime注解 默认是true

maxProcessCount 4 //默认dex中的进程数 默认是4

threadCount 4 //默认的线程数

preDexLibraries true //对library预编译 提高编译效率 但是clean的时候比较慢 默认开启的
}

packagingOptions

Packaging options.
打包配置,尤其是在一些多模块开发工程中,涉及到的一些资源合并取舍的策略:

1
2
3
4
5
6
7
8
packagingOptions   {

pickFirsts = ['META-INF/LICENSE'] //pickFirsts做用是 当有重复文件时 打包会报错 这样配置会使用第一个匹配的文件打包进入apk

merge 'META-INF/LICENSE' //重复文件会合并打包入apk

exclude 'META-INF/LICENSE' //打包时排除匹配文件
}

sourceSets

All source sets. Note that the Android plugin uses its own implementation of source sets, AndroidSourceSet.An AndroidSourceSet represents a logical group of Java, aidl and RenderScript sources as well as Android and non-Android (Java-style) resources.

所有Android资源的集合,包括Java代码,aidl以及RenderScript。默认配置如下,有一些自定义路径的情况下需要修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sourceSets{
main {
res.srcDirs 'src/main/res'
jniLibs.srcDirs = ['libs']
aidl.srcDirs 'src/main/aidl'
assets.srcDirs 'src/main/assets'
java.srcDirs 'src/main/java'
jni.srcDirs 'src/main/jni'
renderscript.srcDirs 'src/main/renderscript'
resources.srcDirs 'src/main/resources'
manifest.srcFile 'src/main/AndroidManifest.xml'
}

free { //除了main,也可以给不同的渠道指定不同的配置

}
}

splits

APK splits options.
这个特性非常有用,不过国内应用基本使用不上,可以根据CPU架构和屏幕像素密度打出最小的apk包,再配合Google Play的市场分发机制,让你可以下载到适合你使用的apk。有abi、density、language三个维度进行过滤:

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
splits  {
abi {
enable true //开启abi分包

universalApk true //是否创建一个包含所有有效动态库的apk

reset() //清空defaultConfig配置

include 'x86','armeabi' //打出包含的包 这个是和defaultConfig累加的

exclude 'mips' //排除指定的cpu架构
}

density {

enable true //开启density分包

reset() //清空所有默认值

include 'xhdpi','xxhdpi' //打出包含的包 这个是和默认值累加的

exclude 'mdpi' //排除指定
}

language {
enable true //开启language分包
include 'en','cn' // 指定语言
}
}

variantFilter

Callback to control which variants should be excluded.
上面我们通过flavor及buildType构建出大量的apk,这里可能有你不需要的,android plugin也考虑到这一点,可以动态设置忽略一些产出:

1
2
3
4
5
6
7
8
9
10
variantFilter { variant ->

def buildTypeName = variant.buildType.name
def flavorName = variant.flavors.name

if (flavorName.contains("360") && buildTypeName.contains("debug")) {
// Tells Gradle to ignore each variant that satisfies the conditions above.
setIgnore(true)
}
}

LibraryExtension

libraryVariants

The list of library variants. Since the collections is built after evaluation, it should be used with Gradle’s all iterator to process future items.

libraryVariants也有类似于applicationVariants的all闭包,可以获取到所有的应用回调,其中可以做一些特定的设置:

1
2
3
4
5
android.libraryVariants.all { variant ->
def mergedFlavor = variant.getMergedFlavor()
// Defines the value of a build variable you can use in the manifest.
mergedFlavor.manifestPlaceholders = [hostName:"www.example.com"]
}

以上是对AppExtension及LibraryExtension的一个详细说明,这个Android Plugin所有task运行的基础配置,下一节将关注我们常用的task实现以及其中的依赖关系。

参考文献

http://google.github.io/android-gradle-dsl/current/

https://docs.gradle.org/current/dsl/