笔者最近在做公司项目的模块化重构, 做的过程中一直在思考以下几个问题:
一个 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 及所需参数进行配置的 ExtensionAndroid Gradle Plugin 定义了很多 task, 其中我们最常用的包括 clean build assemble 等, 还有更多这些 task 运行时依赖的 task, 涉及到安卓编译打包的各个方面, 我们在下一个博客中再具体阐述今天主要 Android Plugin 的 Extension 的部分实现, 这也是我们日常配置一个 Android 工程最主要的工作
Extension 机制
如何理解 Gradle 的 Extension, 这涉及到 Groovy 的闭包委托特性 Groovy 的闭包有 thisownerdelegate 三个属性, 当你在闭包内调用方法时, 由他们来确定使用哪个对象来处理有关闭包的详情可以查看 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 有 abidensitylanguage 三个维度进行过滤:
- 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 实现以及其中的依赖关系
参考文献
- http://google.github.io/android-gradle-dsl/current/
- https://docs.gradle.org/current/dsl/
来源: http://www.jianshu.com/p/7533f1ec9e5a