1. 問題
??一個(gè)App將其依賴的glide sdk 從4.9版本降級(jí)到4.7版本孙援,然后進(jìn)行灰度發(fā)布,隨后出現(xiàn)較多的線上崩潰扇雕。崩潰內(nèi)容如下:
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. 排查及解決
??根據(jù)崩潰的調(diào)用棧我們定位到崩潰所對(duì)應(yīng)的源碼如下拓售,這是我們依賴的另一個(gè)sdk(后面稱為“組件B”)內(nèi)部的代碼,組件B也依賴了glide sdk镶奉。
Glide.with(getActivity()).load(bean.getUrl()).apply(RequestOptions.centerCropTransform().circleCrop()).into(getImageView());
??但源碼并沒有出現(xiàn)調(diào)用circleCrop()Lcom/bumptech/glide/request/BaseRequestOptions;方法础淤,雖然這里有調(diào)用circleCrop()方法,但其返回值是RequestOptions哨苛。隨后我們對(duì)apk進(jìn)行反編譯查看其相關(guān)smali代碼如下:
注意如下這條語句:
invoke-virtual {v3}, Lcom/bumptech/glide/request/RequestOptions;->circleCrop()Lcom/bumptech/glide/request/BaseRequestOptions;
??原來在編譯后的字節(jié)碼里circleCrop()的返回值是BaseRequestOptions鸽凶,其中BaseRequestOptions 是glide 4.9 版本新增的。
??我們梳理了glide sdk的依賴情況如下:
??到這里我們找到了問題原因建峭,因?yàn)樽止?jié)碼里調(diào)用了一個(gè)返回類型BaseRequestOptions的circleCrop()方法玻侥,而事實(shí)上宿主App打包后,apk里面是不包含返回類型BaseRequestOptions的circleCrop()方法亿蒸,所以運(yùn)行到那段代碼的時(shí)候會(huì)會(huì)因找不到對(duì)應(yīng)的方法而拋NoSuchMethodError異常導(dǎo)致崩潰使碾。
??因此解決方法也比較簡(jiǎn)單,組件B基于glide 4.7重新出包給宿主App升級(jí)即可祝懂。
3. 為什么
??這里還有些疑問票摇,看起來這顯然是依賴沖突導(dǎo)致的問題,這種算是比較低級(jí)的問題為什么在灰度發(fā)布前沒有發(fā)現(xiàn)呢砚蓬?
??其實(shí)宿主App在降級(jí)glide sdk到4.7的時(shí)候矢门,是有讓組件B的同學(xué)確認(rèn)組件B是否需要重新出包給宿主升級(jí)。
??組件B的同學(xué)基于當(dāng)前宿主App依賴的組件B版本的代碼沒做任何修改灰蛙,只是將glide的版本號(hào)從4.9改成4.7祟剔,隨后編譯發(fā)現(xiàn)沒有任何報(bào)錯(cuò),并且運(yùn)行demo也沒有問題摩梧,于是他們認(rèn)為宿主App當(dāng)前依賴的組件B版本是可以直接兼容glide 4.7的物延,因?yàn)榇a都不需要修改,直接改版本號(hào)就能打包通過了仅父。
因此這里有兩個(gè)問題需要搞清楚:
3.1. 組件B的源碼里只是調(diào)用了類自身的circleCrop()方法叛薯,為什么字節(jié)碼里會(huì)出現(xiàn)返回值BaseRequestOptions
??對(duì)于字節(jié)碼而言浑吟,方法的調(diào)用是通過方法簽名來識(shí)別的,而方法簽名就包含了方法返回值耗溜,無論我們?cè)创a里有沒有使用到該方法的返回值组力。
??這里我們先看下glide 4.7 circleCrop() 方法的代碼。該方法直接定義在RequestOptions類里面抖拴。
public class RequestOptions implements Cloneable {
public RequestOptions circleCrop() {
return transform(DownsampleStrategy.CENTER_INSIDE, new CircleCrop());
}
}
下面是glide 4.9 circleCrop() 方法相關(guān)代碼燎字,我們發(fā)現(xiàn)新版本增加了基類,該方法被移動(dòng)到基類去了阿宅。
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類,由于在編譯成字節(jié)碼時(shí)會(huì)進(jìn)行泛型擦除洒放,因此在調(diào)用該方法的地方并不存在<T>而是直接被替換為泛型的基類BaseRequestOptions脱柱,所以才出現(xiàn)了該問題。
??除了方法的返回值類型拉馋,還有一些其它場(chǎng)景也會(huì)在編譯時(shí)將源碼不存在的內(nèi)容添加到字節(jié)碼里面榨为,比如一些參數(shù)類型的自動(dòng)轉(zhuǎn)換,拆箱裝箱或lambda表達(dá)式等煌茴。
3.2. 為什么宿主App在編譯打包時(shí)不會(huì)報(bào)錯(cuò)随闺,運(yùn)行時(shí)才拋異常呢
??因?yàn)?strong>組件B是以aar的形式被宿主依賴,aar內(nèi)部包含的是jar包蔓腐,也就是class字節(jié)碼矩乐。因此宿主在編譯時(shí),組件B是不參與編譯成字節(jié)碼這一環(huán)節(jié)(當(dāng)然宿主想這么做也做不到回论,因?yàn)樗拗鞑]有組件B的源代碼)散罕。
4. 總結(jié)
??前面提到的解決方法是通過組件B基于glide 4.7出新包給宿主更新,那是不是有其它方法呢傀蓉,比如通過exclude欧漱,compileOnly,runtimeOnly這些配置是否也能解決sdk依賴版本沖突問題呢葬燎。
??這里大概說下我對(duì)這幾個(gè)配置的理解:exclude頂多只能解決編譯問題误甚,甚至隱藏了運(yùn)行時(shí)可能出現(xiàn)的潛在問題,而compileOnly配置只會(huì)把問題復(fù)雜化(僅限于本文提及的這個(gè)問題)谱净,比如組件B使用compileOnly依賴了glide 4.9窑邦,那么宿主在梳理glide sdk依賴關(guān)系的時(shí)候,甚至無法得知組件B有依賴glide sdk壕探,runtimeOnly會(huì)保證在編譯時(shí)沒有調(diào)用到所依賴的sdk的相關(guān)api冈钦,也不適合這邊討論的這個(gè)問題。
??對(duì)于較大型的App李请,所依賴的sdk組件可能達(dá)幾十個(gè)甚至上百個(gè)瞧筛,各個(gè)sdk更新的情況也會(huì)非常頻繁厉熟,因此想絕對(duì)避免sdk依賴版本沖突的問題,幾乎是很難做到的驾窟。因此這里建議對(duì)于版本跨度較大或者降級(jí)這種特殊場(chǎng)景下的sdk版本變更時(shí)庆猫,變更前必須要梳理出sdk依賴關(guān)系认轨,確保各組件都能同時(shí)升級(jí)到一致的版本绅络。