1. 问题
一个App将其依赖的glide sdk 从4.9版本降级到4.7版本,然后进行灰度发布,随后出现较多的线上崩溃。崩溃内容如下:
java.lang.NoSuchMethodError: No virtual method circleCrop()Lcom/bumptech/glide/request/BaseRequestOptions; in class Lcom/bumptech/glide/request/RequestOptions; or its super classes (declaration of 'com.bumptech.glide.request.RequestOptions' appears in base.apk)
2. 排查及解决
根据崩溃的调用栈我们定位到崩溃所对应的源码如下,这是我们依赖的另一个sdk(后面称为“组件B”)内部的代码,组件B也依赖了glide sdk。
Glide.with(getActivity()).load(bean.getUrl()).apply(RequestOptions.centerCropTransform().circleCrop()).into(getImageView());
但源码并没有出现调用circleCrop()Lcom/bumptech/glide/request/BaseRequestOptions;方法,虽然这里有调用circleCrop()方法,但其返回值是RequestOptions。随后我们对apk进行反编译查看其相关smali代码如下:
注意如下这条语句:
invoke-virtual {v3}, Lcom/bumptech/glide/request/RequestOptions;->circleCrop()Lcom/bumptech/glide/request/BaseRequestOptions;
原来在编译后的字节码里circleCrop()的返回值是BaseRequestOptions,其中BaseRequestOptions 是glide 4.9 版本新增的。
我们梳理了glide sdk的依赖情况如下:
到这里我们找到了问题原因,因为字节码里调用了一个返回类型BaseRequestOptions的circleCrop()方法,而事实上宿主App打包后,apk里面是不包含返回类型BaseRequestOptions的circleCrop()方法,所以运行到那段代码的时候会会因找不到对应的方法而抛NoSuchMethodError异常导致崩溃。
因此解决方法也比较简单,组件B基于glide 4.7重新出包给宿主App升级即可。
3. 为什么
这里还有些疑问,看起来这显然是依赖冲突导致的问题,这种算是比较低级的问题为什么在灰度发布前没有发现呢?
其实宿主App在降级glide sdk到4.7的时候,是有让组件B的同学确认组件B是否需要重新出包给宿主升级。
组件B的同学基于当前宿主App依赖的组件B版本的代码没做任何修改,只是将glide的版本号从4.9改成4.7,随后编译发现没有任何报错,并且运行demo也没有问题,于是他们认为宿主App当前依赖的组件B版本是可以直接兼容glide 4.7的,因为代码都不需要修改,直接改版本号就能打包通过了。
因此这里有两个问题需要搞清楚:
3.1. 组件B的源码里只是调用了类自身的circleCrop()方法,为什么字节码里会出现返回值BaseRequestOptions
对于字节码而言,方法的调用是通过方法签名来识别的,而方法签名就包含了方法返回值,无论我们源码里有没有使用到该方法的返回值。
这里我们先看下glide 4.7 circleCrop() 方法的代码。该方法直接定义在RequestOptions类里面。
public class RequestOptions implements Cloneable {
public RequestOptions circleCrop() {
return transform(DownsampleStrategy.CENTER_INSIDE, new CircleCrop());
}
}
下面是glide 4.9 circleCrop() 方法相关代码,我们发现新版本增加了基类,该方法被移动到基类去了。
public class RequestOptions extends BaseRequestOptions<RequestOptions> {
}
public abstract class BaseRequestOptions<T extends BaseRequestOptions<T>> implements Cloneable {
public T circleCrop() {
return transform(DownsampleStrategy.CENTER_INSIDE, new CircleCrop())
}
}
由于glide 4.9 circleCrop()方法被声明为泛型<T>,该泛型T要求继承于BaseRequestOptions类,由于在编译成字节码时会进行泛型擦除,因此在调用该方法的地方并不存在<T>而是直接被替换为泛型的基类BaseRequestOptions,所以才出现了该问题。
除了方法的返回值类型,还有一些其它场景也会在编译时将源码不存在的内容添加到字节码里面,比如一些参数类型的自动转换,拆箱装箱或lambda表达式等。
3.2. 为什么宿主App在编译打包时不会报错,运行时才抛异常呢
因为组件B是以aar的形式被宿主依赖,aar内部包含的是jar包,也就是class字节码。因此宿主在编译时,组件B是不参与编译成字节码这一环节(当然宿主想这么做也做不到,因为宿主并没有组件B的源代码)。
4. 总结
前面提到的解决方法是通过组件B基于glide 4.7出新包给宿主更新,那是不是有其它方法呢,比如通过exclude,compileOnly,runtimeOnly这些配置是否也能解决sdk依赖版本冲突问题呢。
这里大概说下我对这几个配置的理解:exclude顶多只能解决编译问题,甚至隐藏了运行时可能出现的潜在问题,而compileOnly配置只会把问题复杂化(仅限于本文提及的这个问题),比如组件B使用compileOnly依赖了glide 4.9,那么宿主在梳理glide sdk依赖关系的时候,甚至无法得知组件B有依赖glide sdk,runtimeOnly会保证在编译时没有调用到所依赖的sdk的相关api,也不适合这边讨论的这个问题。
对于较大型的App,所依赖的sdk组件可能达几十个甚至上百个,各个sdk更新的情况也会非常频繁,因此想绝对避免sdk依赖版本冲突的问题,几乎是很难做到的。因此这里建议对于版本跨度较大或者降级这种特殊场景下的sdk版本变更时,变更前必须要梳理出sdk依赖关系,确保各组件都能同时升级到一致的版本。