一次完整的Gradle升级踩坑经历

4,504 阅读5分钟

一、背景

目前我司各App项目Gradle配置和kotlin配置如下:

gradle版本:5.6.4
gradle plugin版本:3.4.3
kotlin版本:1.3.50
kotlin协程版本:1.3.1 

PS:官方已经迭代到了gradle 7.x,AGP到了7.x

虽然这不是升级的主要因素,主要因素在于当我们要接入一些SDK的时候,发现目前的Gradle无法支持最新版,比如firebase,facebook,以及一些国内跟进升级比较积极的sdk厂商。

假如有一天业务驱使我们需要去集成以上sdk,以目前的技术框架,将无法满足业务需求,且临阵改造风险极大。所以,为了避免未来面临窘迫的局面,提前适配Gradle是迫在眉睫的。

二、难点&分析

1、首先我们发现,升级Gradle版本,必须要升级对应匹配的Gradle Plugin版本,同时也需要升级对应匹配的Kotlin版本,以及Kotlin 协程版本;

而以上几个不只是改版本号那么简单,由于Gradle和kotlin升级并不是100%向下兼容,可能很多业务上需要对语法进行适配,一些之前写的工具都要进行适配;

2、升级Gradle Plugin之后,我们还发现插件都不能用了,如aspectj,以及自定义的一些插件,出现编译失败,或者编译成功但无法运行的情况,所以插件还有一个一个进行适配;对于第三方sdk的插件是否生效也需要进一步验证;

三、适配过程

==========

1、确定升级目标

==========

我们浏览一下官方的Gradle和AGP版本对应如下:

image.png

由于在7.0以后,编译流程改变过于巨大,对插件的修改成本极高,且要求JDK11,所以我们折中选择了4.1.0+的版本进行适配升级,即能满足我们的要求,又能降低改造带来的风险; 因此升级目标确认如下:

gradle版本:5.6.4
gradle plugin版本:3.4.3
kotlin版本:1.3.50
kotlin协程版本:1.3.1 

======》升级为:

gradle版本:6.5
gradle plugin版本:4.1.1
kotlin版本:1.5.10
kotlin协程版本:1.5.0 
asm5.0升级到7.0(编译插件使用)

以下有几个版本查下链接:

1、gradle与gradle plugin对应版本链接:developer.android.com/studio/rele…

2、kotlin与kotlin协程对应版本链接:blog.csdn.net/weixin_4423…

3、gradle 与 kotlin对应版本链接:

xy2401.com/local-docs/…

如果升级到其他版本,按照链接进行匹配即可;

==========

2、升级过程

==========

2.1、将主项目app修改项目版本为目标版本,尝试第一次编译

gradle版本:6.5
gradle plugin版本:4.1.1
kotlin版本:1.5.10
kotlin协程版本:1.5.0 

尝试第一次进行编译....

遇到第一个问题:

Execution failed for task ':app:processZroTestDebugManifest'.
    > Could not find method getManifestOutputDirectory() for arguments [] on 
    task':app:processZroTestDebugManifest' of type 
    com.android.build.gradle.tasks.ProcessMultiApkApplicationManifest.
* Try:
    Run with --stacktrace option to get the stack trace. Run with --info 
    or --debugoption to get more log output. Run with --scan to get full insights.
* Get more help at https://help.gradle.org

看报错日志,getManifestOutputDirectory这个api找不到了,这个猜测估计是Gradle 4.1.1版本编译流程里 Process Mainfest的Task发生了变更,废弃了这个API导致的。

这里有个链接是可以查看5.x升级到6.x的一些变更说明: xy2401.com/local-docs/… 但文档不能描述到所有的事情,所以还是要学会查看源码技能。

这里有一篇gradle task详解可以阅读一下:juejin.cn/post/684490…

由于我们项目里在编译期间,对Manifest文件做了一些修改,因此需要解决这个问题。那我们如何寻找替换方案?这就涉及到如何查看gradle源码的问题,因为我们从AS看Gradle文件里的脚本是无法直接点击进入查看源码的,所以我们需要在build.gradle里配置一下依赖,把源码拉下来,如:

implementation 'com.android.tools.build:gradle:4.1.1' //看完源码后可以注释掉

然后进行Sync后,就可以在AS左下角的External Libs里看到这个库了。

image.png

查看Gradle源码需要对编译流程有一个比较基础的了解,至少要知道task是哪里找,我们找到gradle编译的所有task文件夹:

image.png

在里边我们找到了一些和processmainfest相关的task

image.png

从任务名称上,我们需要仔细查看task大概率是:ProcessMultiApkApplicationManifest这个类。

回到我们的原始需求,我们先遍历variant.outputs找到processManifestTask,然后在processManifestTask完成后,读取manifest文件路径,替换其中内容后重新写入

android.applicationVariants.all { variant ->
    variant.outputs.each { output ->
        output.processManifest.doLast {
            def dated = new Date().format("MMdd HH:mm")
            def manifestOutputDirectory = output.processManifest.manifestOutputDirectory.asFile.get()
            String manifestFile = "$manifestOutputDirectory/AndroidManifest.xml"
            println("Mainfest路径是:${manifestFile}")
            def updatedContent = new File(manifestFile).getText('UTF-8')
                    .replaceAll("MY_APP_PKGNAME", "${MY_APP_PKGNAME}");
                    .replaceAll("MY_APK_VERSION", "${archivesBaseName}-${dated}")
            new File(manifestFile).write(updatedContent, 'UTF-8')
        }
    }
}

所以关键点在于查找processTask,然后找到manifest的文件路径 我们查看一下如何获取processTask,因为旧版本是从variant.outputs里获取,所以我们看下BaseVariantOutput这个类的API有没有变更:

gradle 3.4.2版本
@Managed
public interface BaseVariantOutput extends OutputFile {
    /** @deprecated */
    @Deprecated
    ProcessAndroidResources getProcessResources();

    TaskProvider<ProcessAndroidResources> getProcessResourcesProvider();

    /** @deprecated */
    @Deprecated
    ManifestProcessorTask getProcessManifest();

    TaskProvider<ManifestProcessorTask> getProcessManifestProvider();
    ...
gradle 4.1.1版本
  @Managed
public interface BaseVariantOutput extends OutputFile {

    /**
     * Returns the Android Resources processing task.
     *
     * @deprecated Use {@link #getProcessResourcesProvider()}
     */
    @NonNull
    @Deprecated
    ProcessAndroidResources getProcessResources();

    /**
     * Returns the {@link TaskProvider} for the Android Resources processing task.
     *
     * <p>Prefer this to {@link #getProcessResources()} as it triggers eager configuration of the
     * task.
     */
    @NonNull
    TaskProvider<ProcessAndroidResources> getProcessResourcesProvider();

    /**
     * Returns the manifest merging task.
     *
     * @deprecated Use {@link #getProcessManifestProvider()}
     */
    @NonNull
    @Deprecated
    ManifestProcessorTask getProcessManifest();
  
    @NonNull
    TaskProvider<ManifestProcessorTask> getProcessManifestProvider();

    ...

我们发现BaseVariantOutput这个类并没有发生变更,只是getProcessManifest一直被标记废弃了,

所以我们改为使用getProcessManifestProvider的返回ManifestProcessorTask,而ProcessMultiApkApplicationManifest刚好继承了ManifestProcessorTask;

我们通过查看ProcessMultiApkApplicationManifest这个类的方法发现有个API:

/** The merged Manifests files folder.  */
@get:OutputDirectory
abstract val multiApkManifestOutputDirectory: DirectoryProperty

看注释,这个就是mainifest合并后的文件夹路径;于是我们将脚本改为如下:

android.applicationVariants.all { variant ->
        variant.outputs.each { output ->
            def processManifest = output.getProcessManifestProvider().get()
            processManifest.doLast { task ->
                def dated = new Date().format("MMdd HH:mm")
                def outputDir = task.multiApkManifestOutputDirectory
                File outputDirectory
                if (outputDir instanceof File) {
                    outputDirectory = outputDir
                } else {
                   outputDirectory = outputDir.get().asFile
                }
                def manifestFile = "$outputDirectory/AndroidManifest.xml"
                println("----------- ${manifestFile} ----------- ")
                def updatedContent = new File(manifestFile).getText('UTF-8')
                        .replaceAll("MY_APP_PKGNAME", "${MY_APP_PKGNAME}")
                        .replaceAll("MY_APK_VERSION", "${archivesBaseName}-${dated}")
                new File(manifestFile).write(updatedContent, 'UTF-8')
            }
        }
    }

至此该问题得到解决; 同时,因为我们打包aab时,修改mainfest文件使用的api是:getBundleManifestOutputDirectory,这个api在4.1.1也是没有的,已经合并为multiApkManifestOutputDirectory路径了,不再区分aab和apk的mainfest;

2.2、继续编译,遇到kotlin语法问题

e: xxxx/activity/baby/controller/BabyAddController.kt: (157, 61): Using 'maxBy((T) -> R): T?' is an error. Use maxByOrNull instead.

  • 解决办法:改为maxByOrNull

关于kotlin语法的变更,可参考:developer.android.com/kotlin/guid…

APP的模块最好使用新的gradle和kotlin重新打包,避免出现运行时闪退;

*2.3、继续编译,Aspectj插件问题


> Task :app:transformClassesWithAjxForZroTestDebug FAILED
:app:transformClassesWithAjxForZroTestDebug spend 194ms

  • 解决办法:

升级aspectj到2.0.10版本,如果kotlin有异常并在exclude里加上kotlin相关的系统库

 classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'

aspectjx 升级 2.0.4 -> 2.0.10 aspectjrt 升级 1.8.13 -> 1.9.5

官方地址:github.com/HujiangTech…  

升级issue:github.com/HujiangTech…

有些项目升级到2.0.10也会有问题,有两种可能: 一种是aspectj前边的插件有问题,导致的了aspectj有问题 另外一种是 aspectj没有exclude 一些包,如kotlin等官方sdk包;

*2.4、继续编译,自定义插件问题

      
          Dilutions jarName:org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.10; 
          /Users/ice/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk8/1.5.10/3f4af7aff21c4ec46e3cdd645639d0a63a68d3d0/kotlin-stdlib-jdk8-1.5.10.jar
   > Task :app:transformClassesWithDilutions-pluginForZroTestDebug FAILED

解决办法:将插件适配4.1.1插件版本;具体细节如下:

打开插件项目,将AGP配置为为4.1.1;并且依赖asm升级为7.0版本;然后对tansform过程进行整体try catch,并重新打包后,编译期间出现:


 java.lang.NullPointerException
        at com.meetyou.dilutions.BlackhandClassVisitor.visit(BlackhandClassVisitor.java:51)
        at org.objectweb.asm.ClassReader.accept(ClassReader.java:536)
        at org.objectweb.asm.ClassReader.accept(ClassReader.java:394)
        at org.objectweb.asm.ClassReader$accept.call(Unknown Source)
        at com.meetyou.dilutions.plugin.PluginImpl$_transform_closure1$_closure3.doCall(PluginImpl.groovy:288)
        at sun.reflect.GeneratedMethodAccessor2118.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:101)
        at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:323)
        at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:263)
        at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1041)
        at groovy.lang.Closure.call(Closure.java:405)
        at groovy.lang.Closure.call(Closure.java:421)
        at org.codehaus.groovy.runtime.DefaultGroovyMethods.each(DefaultGroovyMethods.java:2330)
        at org.codehaus.groovy.runtime.DefaultGroovyMethods.each(DefaultGroovyMethods.java:2315)
        at org.codehaus.groovy.runtime.DefaultGroovyMethods.each(DefaultGroovyMethods.java:2356)
        at org.codehaus.groovy.runtime.dgm$186.invoke(Unknown Source)
        at org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite$PojoMetaMethodSiteNoUnwrapNoCoerce.invoke(PojoMetaMethodSite.java:244)
        at org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite.call(PojoMetaMethodSite.java:53)
        at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:115)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:127)

.....

   以及大量的:

> Task :app:transformClassesWithFirebasePerformancePluginForZroTestDebug
java.lang.ClassNotFoundException: androidx.fragment.app.FragmentActivity
java.lang.ClassNotFoundException: com.meiyou.sdk.common.http.HttpBizProtocol
java.lang.ClassNotFoundException: com.meiyou.app.common.abtest.bean.ABTestBean$ABTestAlias
java.lang.ClassNotFoundException: androidx.fragment.app.FragmentActivity
java.lang.ClassNotFoundException: androidx.fragment.app.FragmentActivity
java.lang.ClassNotFoundException: com.meiyou.sdk.common.http.exception.ParseException
java.lang.ClassNotFoundException: com.meiyou.sdk.common.http.exception.HttpException
java.lang.ClassNotFoundException: com.meiyou.sdk.common.http.exception.ParseException
java.lang.ClassNotFoundException: com.meiyou.sdk.common.http.HttpBizProtocol
java.lang.NoClassDefFoundError: com/meiyou/framework/io/PrefBase
java.lang.NoClassDefFoundError: com/meiyou/framework/io/PrefBase
java.lang.ClassNotFoundException: com.meiyou.app.common.abtest.bean.ABTestBean$ABTestAlias
java.lang.ClassNotFoundException: com.meiyou.sdk.common.http.HttpResult
java.lang.ClassNotFoundException: com.meiyou.app.common.abtest.bean.ABTestBean$ABTestAlias
java.lang.ClassNotFoundException: com.meetyou.crsdk.intl.homebanner.IntlHomeBannerAdRequestParams
java.lang.NoClassDefFoundError: androidx/recyclerview/widget/RecyclerView$ViewHolder
....

且运行后出现Crash:SeeyouApplication: java.lang.ClassNotFoundException!

022-07-20 10:16:12.056 31789-31789/com.meetyou.intl E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.meetyou.intl, PID: 31789
    java.lang.RuntimeException: Unable to instantiate application com.lingan.seeyou.ui.application.SeeyouApplication: java.lang.ClassNotFoundException: Didn't find class "com.lingan.seeyou.ui.application.SeeyouApplication" on path: DexPathList[[zip file "/data/app/com.meetyou.intl-U2TKFgZF8g9wmtrRoNr8eA==/base.apk"],nativeLibraryDirectories=[/data/app/com.meetyou.intl-U2TKFgZF8g9wmtrRoNr8eA==/lib/arm64, /data/app/com.meetyou.intl-U2TKFgZF8g9wmtrRoNr8eA==/base.apk!/lib/arm64-v8a, /system/lib64, /system/product/lib64]]
        at android.app.LoadedApk.makeApplication(LoadedApk.java:1273)
        at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7128

由此堆栈日志可知,该插件trasform时对kotlin-stdlib-jdk8 jar包进行vistor异常,导致整个transform异常,进而导致dex生成异常,运行crash;

于是我们给transform的遍历文件和jar路径全部打印,发现是transform对第三方jar包的处理出现了访问异常,比如kotlinx等,解决思路是设计白名单,将异常的第三方jar进行exclude,具体如下:

我们将插件的依赖改为:

     compile 'com.android.tools.build:gradle:4.1.1'
     compile 'org.ow2.asm:asm:7.0'
     compile 'org.ow2.asm:asm-commons:7.0'

我们在apply的时候从app提取配置:

         void apply(Project project) {
          project.extensions.create("dilutionConfig", DilutionConfig.class)
          mDilutionConfig =(DilutionConfig) project.property("dilutionConfig")
          def android = project.extensions.getByType(AppExtension);
          android.registerTransform(this)
         }

然后在app里配置如下:

apply plugin: 'dilutions' //安全起见,将此插件配置在aspectj之前
dilutionConfig{
    excludeJar= "didichuxing.doraemonkit," +
            "org.jetbrains.kotlinx," +
            "org.jetbrains.kotlin,"
            "android.local.jars,"+
            "com.didichuxing.doraemonkit,"+
            "io.ktor"
}

然后在transform里对jar进行过滤 :

def isExcludeJar = mDilutionConfig.isExcludeJar(jarName)
          if(isExcludeJar){
          ...不处理
          }

重新打包后,问题得到解决;

其他插件问题,同上处理;

*2.5、Release编译失败

The minSdk version should not be declared in the android manifest file. You can move the version from the manifest to the defaultConfig in the build.gradle file.
  	at com.android.builder.errors.IssueReporter.reportError(IssueReporter.kt:106)
  	at com.android.builder.errors.IssueReporter.reportError$default(IssueReporter.kt:102)

看日志信息,是我们在manifest配置了minsdkversion,新版本不允许这样配置了,需要改到build.gradle里。

*2.5、编译出现高概率的StackOverFlow

主要是因为我们项目手动禁用了

android.enableR8=false

去除此配置即可; 这个问题我们查了很久一直无法定位到具体的原因,我想应该是旧版本的混淆工具有性能问题,R8的性能和效率更好一点。

至此项目升级成功;