升级Android Studio#Gradle的那些事

android studio 相关说明 | 2018-11-16 13:33

下载了最新版的Android Studio之后,gradle插件版本可以升级到3.2.0,对应gradle-wrapper版本升级到4.6,与此同时,项目中一些自定义任务需要进行相关的修改,记录一下。

在Android Studio里开发apk定义混淆很简单,只要在相应地方里配置proguardFile或者proguardFiles即可。如果是library模块供application依赖(project依赖或者aar依赖),可以在library里配置consumerProguardFile就可以自己控制混淆配置,无需在application层配置。而如果是library模块,输出jar包供第三方使用,则需要自己进行混淆。混淆可以直接使用proguard工具进行混淆,当然更方便的是使用Gradle自带的ProguardTask来进行混淆。

在之前版本的gradle环境(gradle 3.0.1)中,我们的配置如下:

1 task("proguard${nameCap}Jar", type: ProGuardTask, dependsOn: ["make${nameCap}Jar"]) { 2    group = 'jar' 3    description = 'make proguard jar' 4 5    injars originalJarPath 6    outjars proguardJarPath 7    configuration 'proguard-rules.pro' 8    printmapping "${mappingFilePath}/mapping.txt" 910    Plugin plugin = getPlugins().hasPlugin(AppPlugin) ?11            getPlugins().findPlugin(AppPlugin) :12            getPlugins().findPlugin(LibraryPlugin)13    if (plugin != null) {14        List<String> runtimeJarList15        if (plugin.getMetaClass().getMetaMethod("getRuntimeJarList")) {16            runtimeJarList = plugin.getRuntimeJarList()17        } else if (android.getMetaClass().getMetaMethod("getBootClasspath")) {18            runtimeJarList = android.getBootClasspath()19        } else {20            runtimeJarList = plugin.getBootClasspath()21        }22        for (String runtimeJar : runtimeJarList) {23            //给 proguard 添加 runtime24            libraryjars(runtimeJar)25        }26    }27    libraryjars files(configurations.compile.collect())28}29升级为3.2.0之后,runtimeJarList = plugin.getBootClasspath()行报错,gradle显示如下:

1* What went wrong:2A problem occurred configuring project ':projectXXX'.3> No signature of method: com.android.build.gradle.LibraryPlugin.getBootClasspath() is applicable for argument types: () values: []错误提示很明显,也很简单,就是找不到这个方法,然而google到类似的问题很多,原因却各不相同。经过多次尝试之后,发现旧版本的gradle运行时,else if (android.getMetaClass().getMetaMethod("getBootClasspath"))这块代码的返回值是true,执行的代码是runtimeJarList = android.getBootClasspath(),并不会知道else语句块里,所以不会报错。那么问题很明确了,出问题的地方就在于android.getMetaClass().getMetaMethod("getBootClasspath")这块代码,在升级之后,返回值变了。

这一块找了好久,一直没找到准确的问题,也无从寻找解决方案。后来偶然使用如下代码自定义plugin调试时,发现代码无法编译通过。

1class DebuggerPlugin implements Plugin<Project> { 2    void apply(Project project) { 3        project.task("debugTask") { 4            def hasApp = project.plugins.withType(AppPlugin) 5            def hasLib = project.plugins.withType(LibraryPlugin) 6 7            Plugin plugin = null 8            if (hasApp) { 9                plugin = project.plugins.findPlugin(AppPlugin)10            } else if (hasLib) {11                plugin = project.plugins.findPlugin(LibraryPlugin)12            }13            println(plugin)14            if (plugin != null) {15                List<String> runtimeJarList16                if (plugin.getMetaClass().getMetaMethod("getRuntimeJarList")) {17                    runtimeJarList = plugin.getRuntimeJarList()18                } else if (android.getMetaClass().getMetaMethod("getBootClasspath")) {19                    runtimeJarList = project.android.getBootClasspath()20                } else {21                    runtimeJarList = plugin.getBootClasspath()22                }23                println("runtimeJar:"+runtimeJarList)2425            }26        }27    }28}报如下错误:

1* What went wrong:2A problem occurred evaluating project ':scene-provider-android'.3> Failed to apply plugin [class 'DebuggerPlugin']4   > Could not get unknown property 'android' for task ':scene-provider-android:debugTask' of type org.gradle.api.DefaultTask.多年的代码敏感一下子就让我找到了问题所在,在我的build.gradle明明是声明了android.library工程,有android对象存在的。而在Plugin类中,自定插件类是跟外在载体无直接关联,因此无法直接访问到android,或者说无法感知到android的存在。解决方案很简单,就是在android前面加上project对象访问。即android.getMetaClass().getMetaMethod("getBootClasspath")修改为project.android.getMetaClass().getMetaMethod("getBootClasspath"),再次尝试,即编译通过。

注:该project对象为apply(Project project)的参数对象。

那我在混淆任务里,是不是也是这样呢?实践是检验真理的唯一标准。事实告诉我,是的。那么问题来了,为什么混淆任务可以直接访问到android对象,但是返回值却跟以前不一样了呢?我试了自定义一个任务,即debugTask里的代码,不放在plugin插件体里,而是直接放在build.gradle中,发现是可以找到getBootClasspath方法的,也能顺利拿到结果。我再回过头去看之前的代码,对比了下,区别在于,因为需要支持多渠道混淆任务,task("proguard${nameCap}Jar")是放在android.libraryVariants.all { ...}方法体里的。这意味着,在升级gradle之后,libraryVariants遍历的方法体里android和以前不一样了,即和project.android不一样了。通过打印project.android和android对象,得到了如下结果:

1project android: com.android.build.gradle.LibraryExtension_Decorated@a52android: nullandroid对象居然为null了!!说明在android.libraryVaiants.all遍历时,将不再能够直接访问project.android对象。那么为什么android.getMetaClass()调用的时候,不会报java.lang.NullPointerException呢?Groovy也是jvm系语言呀。

我感觉我打开了一个新世界的大门。

跟着好奇心,我打印了android.getMetaClass()和null.getMetaClass(),发现结果都是org.codehaus.groovy.runtime.HandleMetaClass@1e11ea06[groovy.lang.MetaClassImpl@1e11ea06[class org.codehaus.groovy.runtime.NullObject]]

你跟我一样都没有看错,并不会报NPE,也会有结果输出。Java语言的null是一个关键字,在jvm中,null不是一个对象,不会有任何内存。而Groovy的null,其实是org.codehaus.groovy.runtime.NullObject的实例。

1java.lang.Object2    groovy.lang.GroovyObjectSupport3        org.codehaus.groovy.runtime.NullObject那么这一切也不奇怪了,Groovy的null,其实就是一个对象。它也有相对应的方法。具体可参考-2.3.2/html/api/org/codehaus/groovy/runtime/NullObject.html

扯得稍微有点远,回过头来。混淆任务的libraryjars是为了将外部引用的jar包路径引入,才能对类混淆时keep住对应的类。而本例的task任务是将android.jar引入,保证Android SDK相关类能够正确保留。通过对plugin对象的分析,这个值除了可以通过project.android.getBootClasspath拿到,还可以通过project.extension.getBootClasspath拿到,因为本例的plugin.extension即LibraryExtension对象,也能取到该值。最终的结果如下:

1task("proguard${nameCap}Jar", type: ProGuardTask, dependsOn: ["make${nameCap}Jar"]) { 2    group = 'jar' 3    description = 'make proguard jar' 4 5    injars originalJarPath 6    outjars proguardJarPath 7    configuration 'proguard-rules.pro' 8    configuration 'proguard-rules-remove-debug-log.pro' 9    printmapping "${mappingFilePath}/mapping.txt"1011    println("called:" + nameCap)1213    Plugin plugin = getPlugins().hasPlugin(AppPlugin) ?14            getPlugins().findPlugin(AppPlugin) :15            getPlugins().findPlugin(LibraryPlugin)16    if (plugin != null) {17        List<String> runtimeJarList18        if (project.android.getMetaClass().getMetaMethod("getBootClasspath")) {19            runtimeJarList = project.android.getBootClasspath()20        } else if (plugin.extension.getMetaClass().getMetaMethod("getBootClasspath")) {21            runtimeJarList = plugin.extension.getBootClasspath()22        } else if (plugin.getMetaClass().getMetaMethod("getRuntimeJarList")) {23            runtimeJarList = plugin.getRuntimeJarList()24        } else if (plugin.getMetaClass().getMetaMethod("getBootClasspath")) {25            runtimeJarList = plugin.getBootClasspath()26        }27        if (runtimeJarList != null) {28            for (String runtimeJar : runtimeJarList) {29                //给 proguard 添加 runtime30                libraryjars(runtimeJar)31            }32        }3334    }3536    libraryjars files(configurations.myImplementation.collect())37}38不知道细心的你有没有发现,任务的最后一句也变了。因为新版的Android Studio启用了新的gradle依赖机制,废弃了compile、provided等依赖机制,引入了api、implementation、compileOnly等。具体此文不进行赘述。这个改变会影响我们混淆任务对于我们自有依赖库的引入。其实你依然可以通过compile取得使用了compile依赖的类库jar路径,但是如果是通过其他几种依赖的,就无法顺利找到了。当我们的依赖是使用implementation或者api依赖时,使用configurations.implementation.collect()会报错.

1* What went wrong:2A problem occurred evaluating project ':scene-provider-android'.3> Resolving configuration 'implementation' directly is not allowed解决办法是使用自定义编译配置。

1configurations {2    myImplementation.extendsFrom implementation3}注:因为配置采用的是继承关系,所以myImplementation可以找到所有使用implementation依赖的库,而implementation是通过继承api得来,所以我们只需要引用configurations.myImplementation.collect即可得到我们要的类库jar包路径。

还有其他的一些tip没讲完,感觉这一篇篇幅已经很大了。看来要分好几章了,希望能够加快进度~