转至:http://www.jianshu.com/p/2cbf4e6d09c8
gradle语法详解:http://www.kancloud.cn/digest/itfootball-gradle/105808
介绍
多渠道打包对于 Android 来说有很多种方式,网络上也有很多相应的文章可以参考,比如stormzhang的「[Android Studio 系列教程六--Gradle 多渠道打包][]」,还有美团技术分享的「[美团 Android 自动化之旅—生成渠道包][]」。
之前一直使用第一种方法,但是每个渠道都会重新构建一遍,一百多个渠道的打包需要花费一个小时,比较慢。美团的文章提供了一个很好的思路,在META-INF文件夹中添加空文件,使用文件名来标识渠道。美团的文章中主要是提供思路以及关键代码,GavinCT的 「[Android 批量打包提速 - 1 分钟 900 个市场不是梦][]」提供了一套完整的解决方案,非常具有参考价值。
美团以及 GavinCT 的文章中是使用Python进行打包处理的,我第一次参考实现的也一样。美中不足的是我的实现与Gradle无法有机结合。于是决定使用Groovy配合Gradle完成同样的操作,最后终于实现。
在折腾过程中参考了不少文章,工匠若水的两篇博客「[Groovy 脚本基础全攻略][]」和「[Gradle 脚本基础全攻略][]」非常有帮助,当然官方文档参考也是必不可少,主要有「[Gradle 官方指导文档][]」和「[Android Gradle 插件 DSL 文档][]」。
Gradle 中的Task简介
Gradle 构建系统中Task是非常重要的概念,最常用的生成 APK 包的命令assembleRelease就是一个Task,而当执行Task时 Android Studio 的 Run 窗口会显示 Gradle 的输出,其中很多类似:app:compileBaiduDebugAidl的行就是已经执行了Task的输出。Task之间可以互相依赖,可以用设置按照一定的顺序执行。
Zip 文件 JAVA 处理思路
在 JAVA 中的 Zip 压缩包内部的文件就是一个个ENTRY,将文件添加到 Zip 文件中主要就是三步,简要 Groovy 代码如下:
// 在 ZipOututStream 中新建一个 EntryzipOut.putNextEntry(newZipEntry(entry.name))// 写入内容zipOut << originZipFile.getInputStream(entry).bytes// 关闭 EntryzipOut.closeEntry()
添加空文件即为新建ENTRY随即关闭。
Talk Is Cheap
需要注意一点,我在这里使用了两种渠道统计,所以添加了两个空文件。现学现卖的 Groovy,还请高手多多指教!
importjava.util.zip.*// 发布文件夹defpackageLocPath ="/your/apk/outputs/dir"// 最终发布包存放的子目录defchildPath ="gen"// 渠道文件名defchannelFileName ="id.txt"// 打包日期defreleaseTime =newDate().format("yyyy-MM-dd", TimeZone.getTimeZone("GMT+8"))// 基础 flavor 名称defbaseFlavor ="your_flavor_name"// 打包的 buildType 名称defbuildType ="your_build_type"// 发布包前缀defbaseAppName ="your_apk_name_prefix"// Zip 条目前缀defentryPrefix ="META-INF/"defUMENG_CHANNEL = entryPrefix +"UMENG_CHANNEL_"defCHANNEL_VALUE = entryPrefix +"CHANNEL_VALUE_"// 存储需要特殊处理的渠道defspecialChannel = android.productFlavors.findAll { baseFlavor != it.name }.collect { it.name }// 将所有 AS 生成的包复制到发布文件夹defprepareAllPackage = project.tasks.create("copyAllPackage")prepareAllPackage.setGroup("MultiChannelPackage")// 生成所有最终发布版的 APK 包defpublishAllPackage = project.tasks.create("publishAllPackage")publishAllPackage.setGroup("MultiChannelPackage")// 读取渠道值文件 "友盟渠道值:自定义渠道值" 一行一个defreadChannelFromFile = { String path ->defchannelValue = [:]newFile(path).eachLine {defchannelName = it.split(":")[0].trim()defcustomValue = it.split(":")[-1].trim() channelValue[channelName] = customValue }returnchannelValue}// 对 APK 文件进行操作,添加代表渠道的空 entrydefprocessPackage = { String originFilePath, String processedFilePath, entriesPath ->deforiginZipFile =newZipFile(originFilePath)defoutFile =newFile(processedFilePath) outFile.withOutputStream { os ->defzipOut =newZipOutputStream(os)// 完全遍历拷贝原 APK 的 entryoriginZipFile.entries().each { entry -> zipOut.putNextEntry(newZipEntry(entry.name)) zipOut << originZipFile.getInputStream(entry).bytes zipOut.closeEntry() }// 创建传入的空 entryentriesPath.each { zipOut.putNextEntry(newZipEntry(it)) zipOut.closeEntry() } zipOut.close() } originZipFile.close()}// 遍历所有 Build Variants,添加动态 taskandroid.applicationVariants.all { variant ->// 只依赖特定 BuildTypeif(variant.buildType.name == buildType) {// 获取 productFlavor 名称defflavorName = variant.productFlavors[0].name// 新生成的文件名defnewCopyFileName ="${baseAppName}_V${android.defaultConfig.versionName}_${releaseTime}_${flavorName}.apk"// 复制到文件夹defcopyDir ="${packageLocPath}/"// 最终生成 APK 所在目录defgenDir ="${packageLocPath}/${childPath}/"// 准备好文件夹file(genDir).mkdirs()// 创建复制类型的 task 参考文档 https://docs.gradle.org/current/userguide/working_with_files.html#sec:copying_filesdefcopyAndRename = project.task("copy${variant.name.capitalize()}",type:Copy) copyAndRename.setGroup("MultiChannelPackage") copyAndRename.from(variant.outputs[0].outputFile) copyAndRename.into(copyDir) copyAndRename.rename { newCopyFileName } copyAndRename.doLast { println"Copy ${variant.name.capitalize()} APK File To ${packageLocPath} Done!"}// 处理依赖,动态子项依赖 assemblecopyAndRename.dependsOn project.getTasksByName("assemble${variant.name.capitalize()}",false)// 总 task 依赖所有动态子项prepareAllPackage.dependsOn copyAndRename// 定义处理 APK 文件的 taskdefchannelMap = readChannelFromFile(channelFileName)defprocessApkTask// 因为除了需要特殊处理的渠道,其余的渠道包都是一个底包if(variant.name.contains(baseFlavor)) { channelMap.each { k, v ->if(!specialChannel.contains(k)) {defnewTaskName ="publish${variant.name.replace(baseFlavor, k).capitalize()}"processApkTask = project.tasks.create(newTaskName) processApkTask.setGroup("MultiChannelPackage") processApkTask.doLast {defnewPkgFileName = newCopyFileName.replace(baseFlavor, k) processPackage(copyDir + newCopyFileName, genDir + newPkgFileName, [UMENG_CHANNEL + k, CHANNEL_VALUE + v]) println"${genDir + newPkgFileName} Generated"} processApkTask.dependsOn project.getTasksByName("copy${baseFlavor}Packages",false) publishAllPackage.dependsOn processApkTask processApkTask.outputs.file(genDir + newPkgFileName) } } }else{// 每个需要特殊处理的渠道包单独进行处理processApkTask = project.tasks.create("publish${variant.name.capitalize()}") processApkTask.setGroup("MultiChannelPackage") processApkTask.doLast { processPackage(copyDir + newCopyFileName, genDir + newCopyFileName, [UMENG_CHANNEL + flavorName, CHANNEL_VALUE + channelMap.flavorName]) println"${genDir + newCopyFileName} Generated"} processApkTask.dependsOn project.getTasks().findByName("copy${flavorName.capitalize()}Packages") processApkTask.outputs.file(genDir + newCopyFileName) publishAllPackage.dependsOn processApkTask } }}
要点说明
android.applicationVariants.all
Build Variant 是项目定义的productFlavor与buildType的排列组合,保存了所有 Build 的配置。在 Android Studio 左下角 Build Variants 标签,里面可以直观查看。
关于Task之间的依赖
以打包流程为例,默认的assemble这一类Task最终生成 APK 包,我们需要在这个包的基础上进行处理,先复制一份出来,再操作 Zip 文件。所以有这个写法<复制的 task 对象>.dependsOn ,<处理 APk 的 task 对象>.dependsOn <复制的 task 对象>。使用project.getTasks().findByName()获取已存在的Task对象。而且一个Task可以有多个依赖,所以创建一个publishAllPackage依赖所有动态添加的"publish"Task,做到一个命令生成所有发布包。
代码执行顺序
Gradle 有两个执行阶段,首先是处理构建脚本的阶段,以上代码除了写在doLast {}中的代码,都是在初始化阶段执行。而doLast {}中的代码则是具体开始执行Task时才真正执行。这点在 「[Gradle 脚本基础全攻略][]」中有更详细的说明。
Gradle 的增量构建机制
Gradle 根据Task的输入和输出是否变更来判断是否需要重新执行,若删除之前代码中的processApkTask.outputs.file(filePath),那么处理 APK 的Task每次都无脑执行,这明显是不科学的。关于增量构建的更详细内容可以参考 Gradle 官网教程 「[Feature Spotlight: Incremental Builds][]」,英文但是并不难。
[Android Studio 系列教程六--Gradle 多渠道打包]:
http://stormzhang.com/devtools/2015/01/15/android-studio-tutorial6/
[美团 Android 自动化之旅—生成渠道包]:
http://tech.meituan.com/mt-apk-packaging.html
[Android 批量打包提速 - 1 分钟 900 个市场不是梦]:
http://www.cnblogs.com/ct2011/p/4152323.html
[Groovy 脚本基础全攻略]:
http://blog.csdn.net/yanbober/article/details/49314255
[Gradle 脚本基础全攻略]:
http://blog.csdn.net/yanbober/article/details/49314255
[Gradle 官方指导文档]:
https://docs.gradle.org/current/userguide/userguide.html
[Android Gradle 插件 DSL 文档]:
http://google.github.io/android-gradle-dsl/current/
[Feature Spotlight: Incremental Builds]:
http://gradle.org/feature-spotlight-incremental-builds/