笔者最近在做公司项目的模块化重构,做的过程中一直在思考以下几个问题:
一个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 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) } } } }
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/