Android Gradle插件升级填坑指南

[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)
    }
}

8. 参考

  1. New Build System
  2. Transform Api
  3. AndroidPublicXmlCompat