[TOC]
Android Gradle插件升级填坑指南
1. 引子
自打android开发环境从eclipse迁移到android studio之后,android项目的打包就从ant迁移到了gradle。自此只要是在打包过程中需要干扰代码生成或执行其他特殊处理,都需要通过gradle脚本完成,具体来说就是通过在打包过程中插入task或者给现用task添加hook。 对于我们的项目而言,在插件化和热修复技术,为了降低打包成本,我们编写了自己的gradle插件,用于支持app的插件化和热修复打包。
2. 由instant-run引发的血案
时间来到了16年,随着Instant-Run功能的逐渐完善,我们也升级android studio和gradle来体验强大的Instant-Run,刚一运行就崩溃了。。。
查看一下报错日志,发现proguardDebug
任务找不到;瞅了一下打包产生的临时目录,发现intermediates/classes-proguard目录也找不到了,并且还多出来了intermediates/transforms这个奇怪的目录,赶紧google一下,发现google在gradle插件高版本中引入了transform-api(主要是给大家提供了一个操作代码的接口,比如可以注入代码什么的),并且在高版本的gradle插件中使用transformClassesAndResourcesWithProguardForDebug
任务替换了低版本的proguardDebug
任务。
3. 兼容Transform Api
知道了问题所在,那就让我们撸起袖子干起来吧,将原本需要在proguardDebug
之后执行的代码迁移到transformClassesAndResourcesWithProguardForDebug
之后,并稍作改动就可以了。大概代码如下:
def proguardTaskName = "transformClassesAndResourcesWithProguardFor${flavor.capitalize()}${buildType.capitalize()}".toString()
gradle.taskGraph.afterTask { Task task, TaskState state ->
if (state.failure != null) {
println "${task} error: ${state.failure}"
state.failure.printStackTrace()
state.rethrowFailure();
return;
}
if (task.name.equals(proguardTaskName)) {
// 执行自己的代码,将先前的代码迁移至此,并稍作改动即可。
}
}
4. 兼容不同版本
这里我们为了兼容各个版本的gradle插件,那么就出现了一个新问题如何区分某个gradle插件版本是否支持transform api?
查看官网,发现上面有这么一句话 > (The API existed in 1.4.0-beta2 but it’s been completely revamped in 1.5.0-beta1)
也就是说,这个api在1.4.0-beta2的时候就已经存在了,但是直到1.5.0-beta1版本的时候才改造完成。这么来看的话,通过版本来判断比较复杂,并且不一定靠谱,那么如何能够既简单又靠谱的判断呢?
答案很简单,直接判断project是否拥有transform的task即可,大概代码如下:
boolean isSupportTransformApi(Project project, def variant) {
def supportTransformApi = false;
def flavor = variant.flavorName == null ? "" : variant.flavorName;
def buildType = variant.buildType.name == null ? "" : variant.buildType.name;
def proguardTaskName = "transformClassesAndResourcesWithProguardFor${variant flavor.capitalize()}${buildType.capitalize()}".toString()
project.tasks.each {
if (it.name.equals(proguardTaskName)) {
supportTransformApi = true;
}
}
return supportTransformApi;
}
搞完这个兼容,再次运行项目,就可以成功编译并安装apk了,点击运行,直接崩溃了!!!查看日志发现是资源找不到,并且资源id的值也是错误的,这么来看的话,那就是public.xml和ids.xml没有生效造成的。
5. 解决public.xml、ids.xml不生效问题
原来在gradle插件的高版本中打包时会忽略res/values/目录中定义的public.xml、ids.xml文件;对比一下老版本gradle插件打包生成的临时文件,我们发现其在打包时将public.xml、ids.xml复制到了intermediates/res/merged/${flavor}/${buildType}这个目录*(该目录是打包编译资源时生成的临时目录)*。那么我们自己手动复制这些文件到intermediates/res/merged/${flavor}/${buildType}目录中,是不是就可以让高版本的gradle插件支持public.xml、ids.xml了呢?
代码如下:
// 获取task名称
def mergeResourcesName = "merge${flavor.capitalize()}${buildType.capitalize()}Resources".toString();
def mergeResourceTask = project.tasks.getByName(mergeResourcesName)
mergeResourceTask.doLast {
// 复制public.xml
project.copy {
int i = 0;
from(project.android.sourceSets.main.res.srcDirs) {
include 'values/public.xml'
rename 'public.xml', (i++ == 0 ? "public.xml" : "public_${i}.xml")
}
into(task.outputDir)
}
// 复制ids.xml
project.copy {
int i = 0;
from(project.android.sourceSets.main.res.srcDirs) {
include 'values/ids.xml'
rename 'ids.xml', (i++ == 0 ? "ids.xml" : "ids_${i}.xml")
}
into(task.outputDir)
}
}
添加如上代码之后,重新编译工程,在合并资源时出现了资源重复定义错误,由此可以得出是复制public.xml和ids.xml到打包产生的临时目录导致的。
6. 解决public.xml与values.xml中的资源重复定义
从错误信息上面可以知道,是因为public.xml和values.xml出现了相同的元素导致;所以简单粗暴的将public.xml和values.xml中相同的元素剔除是不是就能够解决这个问题了呢?
我们在mergeResourceTask.doLast{}复制完public.xml和ids.xml后,解析并对比public.xml和values.xml文件,然后剔除values.xml中与public.xml中相同的元素,大致代码如下:
def valuesDir = new File(task.outputDir, 'values');
def publicResSet = [];
// 获取public.xml中的元素
valuesDir.eachFile { f ->
if (f.name.startsWith('public') && f.name.endsWith('.xml')) {
def publicNode = new XmlParser().parse(f);
publicNode.each { node ->
def name = node.attribute('name').toString();
def type = node.attribute('type').toString()
publicResSet.add(new PublicXmlRes(name, type));
}
}
}
// 剔除values.xml中与public.xml相同的元素。
def valuesXmlFile = new File(task.outputDir, 'values/values.xml');
def valuesNode = new XmlParser().parse(valuesXmlFile);
def noIdNode = new Node(null, 'resources');
valuesNode.each {
if ('item'.equals(it.name())) {
def name = it.attribute('name').toString();
def type = it.attribute('type').toString();
def publicRes = new PublicXmlRes(name, type);
if (publicResSet.contains(publicRes)) {
println "skip public res: ${publicRes}"
return;
} else {
println "not skip public res: ${publicRes}"
}
}
noIdNode.append(it);
}
// 使用剔除相同元素的values.xml覆盖原来的values.xml文件;
def out = new PrintWriter(new FileWriter(valuesXmlFile));
out << '<?xml version="1.0" encoding="utf-8"?>\n';
XmlNodePrinter xmlNodePrinter = new XmlNodePrinter(out);
xmlNodePrinter.with {
preserveWhitespace = true;
expandEmptyElements = false;
}
xmlNodePrinter.print(noIdNode)
out.flush()
IOGroovyMethods.closeWithWarning(out)
至此,整个项目就可以用Instant-Run跑起来了。。。
7. 兼容Multidex
7.1 Transform Api之前
在Transform Api出现之前,大家拆分dex时,一般都是获取dex任务,然后给dex添加参数,来实现干扰multidex,具体代码大致如下:
project.tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--multi-dex';
// dx.additionalParameters += '--minimal-main-dex';
dx.additionalParameters += '--set-max-idx-number=55000';
}
然后在生成manifest_keep.txt文件后,修改manifest_keep.txt文件,来达到拆分multidex,具体代码如下:
def multidexTaskNames = [];
afterEvaluate {
android.applicationVariants.all { variant ->
def flavorName = variant.flavorName == null ? "" : variant.flavorName;
def buildType = variant.buildType.name == null ? "" : variant.buildType.name;
def multidexTaskName = "collect${flavorName.capitalize()}${buildType.capitalize()}MultiDexComponents".toString();
multidexTaskNames.add(multidexTaskName);
}
}
gradle.taskGraph.beforeTask { Task multiDexTask ->
if (!multidexTaskNames.contains(multiDexTask.name)) {
return;
}
println "multidexTaskName=${multiDexTask.name}; multiDexTask.outputFile=${multiDexTask.outputFile}"
multiDexTask.doLast {
File manifestKeepFile = multiDexTask.outputFile;
// 修改manifestKeepFile文件,将不需要的类从该文件中删除;
// 来达到mutidex分包的目的.
}
}
7.2 Transform Api之后
当有了Transform Api之后,给dex任务添加dx.additionalParameters参数就无效了,但是*collect MultiDexComponents*仍然有效,所以只需要找到如何添加dx参数就好。分析gradle插件可以发现,在Transform Api中使用DexTransform来将jar转换为dex,并且在DexTransform最终由AndroidBuilder调用dx生成dex文件。
其中DexTransform在*transform*方法中调用androidBuilder的*convertByteCode*方法,代码如下:
public void transform(TransformInvocation transformInvocation) throws TransformException, IOException, InterruptedException {
// 忽略....
this.androidBuilder.convertByteCode(outputs2, outputDir3, this.multiDex, this.mainDexListFile, this.dexOptions, (List)null, false, true, outputHandler1);
// 忽略....
}
通过查看AndroidBuilder的*convertByteCode*方法签名如下:
void convertByteCode(Collection<File> inputs, File outDexFolder, boolean multidex, File mainDexList, DexOptions dexOptions, List<String> additionalParameters, boolean incremental, boolean optimize, ProcessOutputHandler processOutputHandler) throws IOException, InterruptedException, ProcessException {...}
对比以上两个代码片段,我们发现在*transform*方法中调用*convertByteCode*方法,直接将additionalParameters参数传入了null,所以只需要在调用*convertByteCode*方法时,传入自己的additionalParameters,就可以实现和以前一样的功能,那么如何实现呢?
我们的解决方案是,在执行DexTransform前替换DexTransform的androidBuilder字段,然后在调用*convertByteCode*方法前,添加dx参数,大概代码如下:
// 找到DexTransform
project.tasks.matching {
it instanceof TransformTask
}.each { TransformTask transformTask ->
Transform transform = transformTask.transform;
if (transform.name.equals('dex')) {
DexTransform dexTransform = (DexTransform) transform;
if (dexTransform.multiDex) {
// wrapper androidBuilder and add dex paramters.
replaceFieldAndroidBuilder(dexTransform);
}
}
}
// 替换androidBuilder字段。
private
static void replaceFieldAndroidBuilder(DexTransform dexTransform) {
def fieldAndroidBuilder = DexTransform.class.getDeclaredField('androidBuilder');
fieldAndroidBuilder.setAccessible(true);
AndroidBuilder androidBuilder = dexTransform.androidBuilder;
fieldAndroidBuilder.set(dexTransform, AndroidBuilderWrapper.wrapperAndroidBuilder(androidBuilder));
}
// 自定义androidBuidler.
private static class AndroidBuilderWrapper extends AndroidBuilder {
public
static AndroidBuilder wrapperAndroidBuilder(AndroidBuilder androidBuilder) {
return new AndroidBuilderWrapper(androidBuilder);
}
AndroidBuilderWrapper(AndroidBuilder androidBuilder) {
super(androidBuilder.mProjectId, androidBuilder.mCreatedBy, androidBuilder.getProcessExecutor(), androidBuilder.mJavaProcessExecutor, androidBuilder.getErrorReporter(), androidBuilder.getLogger(), androidBuilder.mVerboseExec);
setTargetInfo(androidBuilder.sdkInfo, androidBuilder.targetInfo, androidBuilder.mLibraryRequests);
}
@Override
void convertByteCode(Collection<File> inputs, File outDexFolder, boolean multidex, File mainDexList, DexOptions dexOptions, List<String> additionalParameters, boolean incremental, boolean optimize, ProcessOutputHandler processOutputHandler) throws IOException, InterruptedException, ProcessException {
println "AndroidBuilderWrapper invoke convertByteCode";
// 添加dx参数.
if (additionalParameters == null) {
additionalParameters = new ArrayList<String>();
}
dx.additionalParameters += '--multi-dex';
// dx.additionalParameters += '--minimal-main-dex';
dx.additionalParameters += '--set-max-idx-number=55000';
println "AndroidBuilderWrapper invoke convertByteCode, additionalParameters=${additionalParameters}";
super.convertByteCode(inputs, outDexFolder, multidex, mainDexList, dexOptions, additionalParameters, incremental, optimize, processOutputHandler)
}
}