最近在負(fù)責(zé)做一個(gè)圖片加載模塊,測(cè)試過程中反饋一個(gè)問題:有兩個(gè)測(cè)試設(shè)備上加載不了圖片。我就納悶了,我就一個(gè)加載圖片模塊怎么還跟機(jī)型適配扯上關(guān)系了。然后查了下日志異常如下:
java.lang.IllegalArgumentException: Unexpected char 0x8d2d at 13 in content-disposition value: filename="3.6購買頁.jpg"
at com.bumptech.glide.request.RequestFutureTarget.doGet(RequestFutureTarget.java:189)
at com.bumptech.glide.request.RequestFutureTarget.get(RequestFutureTarget.java:100)
at common.disk.ImageDiskCache.lambda$putCacheImage$0$ImageDiskCache(ImageDiskCache.java:100)
at common.disk.ImageDiskCache$$Lambda$0.run(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
Caused by: java.lang.IllegalArgumentException: Unexpected char 0x8d2d at 13 in content-disposition value: filename="3.6購買頁.jpg"
at okhttp3.Headers$Builder.checkNameAndValue(Headers.java:283)
at okhttp3.Headers$Builder.add(Headers.java:233)
at okhttp3.internal.http.Http2xStream.readHttp2HeadersList(Http2xStream.java:263)
at okhttp3.internal.http.Http2xStream.readResponseHeaders(Http2xStream.java:149)
at okhttp3.internal.http.HttpEngine.readNetworkResponse(HttpEngine.java:723)
at okhttp3.internal.http.HttpEngine.access$200(HttpEngine.java:81)
at okhttp3.internal.http.HttpEngine$NetworkInterceptorChain.proceed(HttpEngine.java:708)
at okhttp3.internal.http.HttpEngine.readResponse(HttpEngine.java:563)
at okhttp3.RealCall.getResponse(RealCall.java:241)
at okhttp3.RealCall$ApplicationInterceptorChain.proceed(RealCall.java:198)
at okhttp3.SNInterceptor.intercept(SNInterceptor.java:62)
at okhttp3.RealCall$ApplicationInterceptorChain.proceed(RealCall.java:187)
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:160)
at okhttp3.RealCall.execute(RealCall.java:57)
at glide.MOkHttpStreamFetcher.loadData(MOkHttpStreamFetcher.java:51)
at glide.MOkHttpStreamFetcher.loadData(MOkHttpStreamFetcher.java:22)
at com.bumptech.glide.load.engine.DecodeJob.decodeSource(DecodeJob.java:170)
at com.bumptech.glide.load.engine.DecodeJob.decodeFromSource(DecodeJob.java:128)
at com.bumptech.glide.load.engine.EngineRunnable.decodeFromSource(EngineRunnable.java:127)
at com.bumptech.glide.load.engine.EngineRunnable.decode(EngineRunnable.java:106)
at com.bumptech.glide.load.engine.EngineRunnable.run(EngineRunnable.java:58)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:423)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
at com.bumptech.glide.load.engine.executor.FifoPriorityThreadPoolExecutor$DefaultThreadFactory$1.run(FifoPriorityThreadPoolExecutor.java:118)
其實(shí)從日志上,問題原因已經(jīng)很明顯,但是查找問題的時(shí)候我犯了個(gè)錯(cuò)誤。就是沒有根據(jù)堆棧信息查找問題,這是由于平時(shí)發(fā)現(xiàn)問題的時(shí)候習(xí)慣于定位應(yīng)用的代碼入口,而不是查看源碼報(bào)錯(cuò)處。
當(dāng)時(shí)看到這個(gè)異常的時(shí)候,第一反應(yīng)是保存文件的時(shí)候使用中文出錯(cuò),但是我寫的代碼中保存圖片用的是自己定義的字符串,而這個(gè)filename在代碼里根本查不到。所以這種情況下這個(gè)filename只能是通過圖片url獲取到的,然后我打開chrome調(diào)試,可以看到圖片url的響應(yīng)報(bào)頭中有這個(gè)東西:
Content-Disposition:filename="3.6購買頁.jpg
Content-disposition是 MIME 協(xié)議的擴(kuò)展,MIME 協(xié)議指示 MIME 用戶代理如何顯示附加的文件。當(dāng) Internet Explorer 接收到頭時(shí),它會(huì)激活文件下載對(duì)話框,它的文件名框自動(dòng)填充了頭中指定的文件名。
知道了filename是哪里來的之后,再去找發(fā)生問題的原因。在我的代碼里我只調(diào)用了:
File imageFile = Glide.with(mContext).load(url).downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL).get();
所以當(dāng)時(shí)我的理解是,glide在加載圖片時(shí)內(nèi)部緩存文件時(shí)因?yàn)閒ilename報(bào)錯(cuò)??戳税胩煸创a后,發(fā)現(xiàn)最終調(diào)用的是項(xiàng)目中自定義的DataFetcher的loadData方法,然后就是okhttp的正常請(qǐng)求調(diào)用了。其實(shí)整個(gè)調(diào)用鏈跟異常日志的堆棧信息是一樣的。okhttp的詳細(xì)調(diào)用略過,最終的問題出現(xiàn)在Http2xStream(這個(gè)類是負(fù)責(zé)處理Http2.0協(xié)議的,還有一個(gè)Http1xStream類處理Http1.x協(xié)議,這個(gè)會(huì)根據(jù)當(dāng)前設(shè)備是否支持去初始化不同的類,這也是為什么會(huì)有請(qǐng)求頭中文報(bào)錯(cuò)只有部分機(jī)型存在)的readHttp2HeadersList方法,這里會(huì)讀取response的header,問題在這個(gè)調(diào)用
headersBuilder.add(name.utf8(), value);
okhttp3.Headers.java
/** Add a field with the specified value. */
public Builder add(String name, String value) {
checkNameAndValue(name, value);
return addLenient(name, value);
}
private void checkNameAndValue(String name, String value) {
if (name == null) throw new IllegalArgumentException("name == null");
if (name.isEmpty()) throw new IllegalArgumentException("name is empty");
for (int i = 0, length = name.length(); i < length; i++) {
char c = name.charAt(i);
if (c <= '\u001f' || c >= '\u007f') {
throw new IllegalArgumentException(String.format(
"Unexpected char %#04x at %d in header name: %s", (int) c, i, name));
}
}
if (value == null) throw new IllegalArgumentException("value == null");
for (int i = 0, length = value.length(); i < length; i++) {
char c = value.charAt(i);
if (c <= '\u001f' || c >= '\u007f') {
throw new IllegalArgumentException(String.format(
"Unexpected char %#04x at %d in %s value: %s", (int) c, i, name, value));
}
}
}
這里就是問題出現(xiàn)的原因,checkNameAndValue這個(gè)方法會(huì)對(duì)請(qǐng)求頭的name、value進(jìn)行校驗(yàn)。
解決思路
既然發(fā)現(xiàn)了出現(xiàn)問題的原因,現(xiàn)在就是找解決方案,其實(shí)在網(wǎng)上搜索okhttp請(qǐng)求頭中文,這個(gè)關(guān)鍵字也會(huì)搜到一些文章。但是這些給出的解決方案一般都是對(duì)請(qǐng)求頭進(jìn)行轉(zhuǎn)碼,因?yàn)橐话氵@種問題都出現(xiàn)在前端request的時(shí)候,而我碰到的服務(wù)端返回的請(qǐng)求頭中帶中文。
出現(xiàn)這問題之后我首先是想讓后端協(xié)助解決掉這個(gè)問題,但是跟后端溝通過后發(fā)現(xiàn)他也不知道這個(gè)filename是哪里來的,他沒有對(duì)這塊進(jìn)行處理。出于各種原因他也不能去專門修改這個(gè)問題,同時(shí)他也指出你們使用的框架不支持請(qǐng)求頭中文,本身就不合理。我聽他這么一說也挺有道理,就放棄了這個(gè)想法。
既然靠后端修改走不通,我又查了下資料。既然filename有一個(gè)名字,那肯定是哪里傳過去的,后臺(tái)配置圖片都配置的是英文,這中文必然是上傳圖片時(shí)存儲(chǔ)在本地的文件名。然后跟產(chǎn)品和運(yùn)營溝通了一下,果然這個(gè)命名是他們本地的。然后讓他們修改本地文件名重新上傳了一下,這問題算是解決了。
但是,問題肯定不能到這為止。如果其他人碰到這個(gè)問題,又沒辦法讓在源頭做修改,那這個(gè)問題如何解決?
下面說一下我個(gè)人的解決方案:
方案一:使用攔截器(適用于發(fā)起request時(shí))
這個(gè)方案比較常規(guī),你也可以在出錯(cuò)的請(qǐng)求發(fā)起時(shí)對(duì)對(duì)應(yīng)的request請(qǐng)求頭進(jìn)行轉(zhuǎn)碼。也可以在構(gòu)造OkHttpClient時(shí)addInterceptor,然后在intercept中對(duì)request統(tǒng)一進(jìn)行轉(zhuǎn)碼。具體代碼就不贅述了,到處都能找到。但是需要注意的是,在intercept中是無法規(guī)避response中請(qǐng)求頭有中文的,因?yàn)槌鲥e(cuò)的位置在你通過chain.proceed(request)拿到response之前,這點(diǎn)可以在源碼中看到后續(xù)我也會(huì)寫文章講okhttp整個(gè)流程。
方案二:反射
這個(gè)方案是我自己使用的方案,源于分析代碼的時(shí)候,問題出在readHttp2HeadersList的
if (!HTTP_2_SKIPPED_RESPONSE_HEADERS.contains(name)) {
headersBuilder.add(name.utf8(), value);
}
這里的add方法,而我碰到的問題是某一個(gè)請(qǐng)求頭會(huì)返回中文,并且客戶端不需要這個(gè)請(qǐng)求頭的參數(shù)。那既然這樣,如果我去修改HTTP_2_SKIPPED_RESPONSE_HEADERS這個(gè)List,不就可以實(shí)現(xiàn)我需要的功能并且改動(dòng)最小嗎。具體代碼如下:
private boolean hookOkHttpReadHeader(){
try {
Class clz = Class.forName("okhttp3.internal.http.Http2xStream");
Field field = clz.getField("HTTP_2_SKIPPED_RESPONSE_HEADERS");
field.setAccessible(true);
field.set(new ArrayList<>(), HTTP_2_SKIPPED_RESPONSE_HEADERS);
return true;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return false;
}
此方法適用于客戶端不需要請(qǐng)求頭中的參數(shù)的情況。
方案三:方法替換
雖然我自己的問題解決了,但是我還是在想能不能去完美的解決這個(gè)問題而不是取巧,有沒有一個(gè)方法能夠?qū)崿F(xiàn)替換Headers中的add方法實(shí)現(xiàn),讓add方法不去調(diào)用checkNameAndValue,或者是修改checkNameAndValue的內(nèi)部實(shí)現(xiàn)。
想過之后突然發(fā)現(xiàn)這不就是熱修復(fù)中的方法替換,andfix不就是通過替換方法指針達(dá)到修復(fù)的目的嗎,這個(gè)情況跟我想要實(shí)現(xiàn)的一模一樣。既然如此,可以使用andfix的方案解決問題。但是這樣會(huì)導(dǎo)致包體積增大以及兼容性問題,而且就算有源碼實(shí)現(xiàn)這個(gè)過程也是要耗費(fèi)大量時(shí)間的,這里只是提供一種思路。
方案四:自行編譯
這個(gè)方案是你自己去pull okhttp源碼,修改對(duì)應(yīng)位置,然后生成依賴供自己使用。這樣可以完整的規(guī)避這種問題。
本文同步發(fā)布在:https://pengsongandroid.github.io/