一、背景
目前我司各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版本对应如下:
由于在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
asm从5.0升级到7.0(编译插件使用)
以下有几个版本查下链接:
1、gradle与gradle plugin对应版本链接:https:///studio/releases/gradle-plugin?hl=zh-cn
2、kotlin与kotlin协程对应版本链接:kotlin标准库与kotlin协程相关支持库对应关系(持续更新。。。)_pumpkin的玄学的博客
3、gradle 与 kotlin对应版本链接:
如果升级到其他版本,按照链接进行匹配即可;
==========
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的一些变更说明: 将您的构建从Gradle 5.x升级到6.0 但文档不能描述到所有的事情,所以还是要学会查看源码技能。
由于我们项目里在编译期间,对Manifest文件做了一些修改,因此需要解决这个问题。那我们如何寻找替换方案?这就涉及到如何查看gradle源码的问题,因为我们从AS看Gradle文件里的脚本是无法直接点击进入查看源码的,所以我们需要在build.gradle里配置一下依赖,把源码拉下来,如:
implementation 'com.android.tools.build:gradle:4.1.1' //看完源码后可以注释掉
然后进行Sync后,就可以在AS左下角的External Libs里看到这个库了。
查看Gradle源码需要对编译流程有一个比较基础的了解,至少要知道task是哪里找,我们找到gradle编译的所有task文件夹:
在里边我们找到了一些和processmainfest相关的task
从任务名称上,我们需要仔细查看task大概率是:ProcessMultiApkApplicationManifest这个类。
回到我们的原始需求,我们先遍历variant.outputs找到processManifestTask,然后在processManifestTask完成后,读取manifest文件路径,替换其中内容后重新写入
licationVariants.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合并后的文件夹路径;于是我们将脚本改为如下:
licationVariants.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语法的变更,可参考:https:///kotlin/guides-changelog?hl=zh-cn
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
有些项目升级到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: .FragmentActivity
java.lang.ClassNotFoundException: com.meiyou.sdk.common.http.HttpBizProtocol
java.lang.ClassNotFoundException: com.meiyou.app.common.abtest.bean.ABTestBean$ABTestAlias
java.lang.ClassNotFoundException: .FragmentActivity
java.lang.ClassNotFoundException: .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 .LoadedApk.makeApplication(LoadedApk.java:1273)
at .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的性能和效率更好一点。
至此项目升级成功;