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分支的一个示例:
$ 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项目中经常配置的:
include ':app'
include ':lib'
说到这里可以多说一句在模块化开发中的经验,我们可以通过指定subproject的路径的方式,可以将本地任何路径下的代码导入工程中来,方便我们进行本地调试:
include ':lib'
project(':lib').projectDir = new File('xx/xx/xx/lib')
在rootproject的build.gradle文件中创建一个最简单的gradle plugin:
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 进行配置,我们执行一下:
./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:
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执行这个闭包,运行结果如下:
/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:
* <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<ApplicationVariant>,这是不同buildType及Flavor的集合,
applicationVariants最常用的是它的all方法,例如一个简单的修改apk名字的代码:
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 对象,可以做以下设置:
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相同,不同配置使用说明如下:
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是一个资源打包工具,可以对资源优化做一些动态配置:
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命令的一些配置
adbOptions {
installOptions '-r' '-d' //调用adb install命令时默认传递的参数
timeOutInMs 1000 //执行adb命令的超时时间
}
compileOptions
Compile options.
编译配置
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:
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同学对这一块有详细说明,下面引用自他的博客):
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的可以做以下配置:
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.
打包配置,尤其是在一些多模块开发工程中,涉及到的一些资源合并取舍的策略:
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。默认配置如下,有一些自定义路径的情况下需要修改:
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三个维度进行过滤:
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也考虑到这一点,可以动态设置忽略一些产出:
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闭包,可以获取到所有的应用回调,其中可以做一些特定的设置:
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实现以及其中的依赖关系。