解决Picasso在Android 5.0以下版本不兼容https导致图片不显示
近期在項目中遇到了一個問題,使用picasso加載圖片在Android5.0以下版本圖片顯示不來。
由于之前在幾個項目中都使用過picasso而且未出現類似問題,覺得值得好好研究一下。
簡單定位一下問題所在,我們一直使用picasso大致會是下面的代碼
Picasso.with(context).load(url).into(imageView);我們知道into函數還有另外一個版本,可以添加callback,如下:
Picasso.with(context).load(url).into(imageView,?new?Callback()?{@Overridepublic?void?onSuccess()?{}@Overridepublic?void?onError()?{} });這樣可以在回調中做一些事情
通過上面的回掉測試發現圖片不顯示是因為error了,但是picasso的callback并沒有給出具體錯誤。
通過日志可以看到picasso給出了出錯信息:
Attempting to convert network exception javax.net.ssl.SSLHandshakeException to error code.
但是這段信息量不夠,隱約感覺與https證書有關。
深入調查就需要我們去追蹤picasso的源碼了。追蹤源碼可以看到請求經過OkHttpDownloader.load()和NerworkRequestHandler.load()這兩層函數,最終在BitmapHunter的run函數中得到處理,這個函數源碼如下:
@Override?public?void?run()?{try?{updateThreadName(data);if?(picasso.loggingEnabled)?{log(OWNER_HUNTER,?VERB_EXECUTING,?getLogIdsForHunter(this));}result?=?hunt();if?(result?==?null)?{dispatcher.dispatchFailed(this);}?else?{dispatcher.dispatchComplete(this);}}?catch?(Downloader.ResponseException?e)?{if?(!e.localCacheOnly?||?e.responseCode?!=?504)?{exception?=?e;}dispatcher.dispatchFailed(this);}?catch?(NetworkRequestHandler.ContentLengthException?e)?{exception?=?e;dispatcher.dispatchRetry(this);}?catch?(IOException?e)?{exception?=?e;dispatcher.dispatchRetry(this);}?catch?(OutOfMemoryError?e)?{StringWriter?writer?=?new?StringWriter();stats.createSnapshot().dump(new?PrintWriter(writer));exception?=?new?RuntimeException(writer.toString(),?e);dispatcher.dispatchFailed(this);}?catch?(Exception?e)?{exception?=?e;dispatcher.dispatchFailed(this);}?finally?{Thread.currentThread().setName(Utils.THREAD_IDLE_NAME);} }可以看到調用了dispatcher.dispatchFailed(this),這樣再經過Dispatcher的處理調用callback的。
至于整個請求及處理過程涉及到的源碼太多,這里就不詳細來說來,有時間我們另開一章。
因為在run函數以及catch了所有exception,所以我們需要在這里來獲取出錯的信息,通過debug看到,加載圖片出現的錯誤實際上是
javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0xb8de3a90: Failure in SSL ...
求助萬能的百度后得知,這個問題的確與證書有關。這里摘錄一段大神的解釋,其實也是google對SSLEngine的官方說明
這里截取不同Android版本針對于TLS協議的默認配置圖如下:
?
從上圖可以得出如下結論:
- TLSv1.0從API 1+就被默認打開
- TLSv1.1和TLSv1.2只有在API 20+ 才會被默認打開
- 也就是說低于API 20+的版本是默認關閉對TLSv1.1和TLSv1.2的支持,若要支持則必須自己打開
通過上面的解釋可以知道,TLSv1.2在Android 5.0以下系統默認是關閉的,那么問題的原因就清晰了。首先是我們的圖片服務器使用TLSv1.2證書,但未同步到前端開發人員,而picasso-v2.5.2底層所使用的網絡框架沒有為Android 5.0以下系統打開TLSv1.2導致的。
?問題原因我們知道的,如何解決呢?
我們知道Picasso默認底層網絡請求是HttpURLConnection,但是Picasso可以替換底層的網絡請求框架的,我們使用這一功能來實現對TLSv1.2的支持。
Picasso不僅封裝了HttpURLConnection,也封裝了OkHttp,所以我們可以使用Picasso自帶的OkHttp,經過修改后替換Picasso默認的HttpURLConnection即可,代碼如下:
if(Build.VERSION.SDK_INT?<?Build.VERSION_CODES.LOLLIPOP)?{OkHttpClient?client?=?new?OkHttpClient();try?{SSLContext?sc?=?SSLContext.getInstance("TLS");sc.init(null,?null,?null);client.setSslSocketFactory(new?PicassoSslSocketFactory(sc.getSocketFactory()));}?catch?(Exception?e)?{e.printStackTrace();}Picasso.Builder?builder?=?new?Picasso.Builder(context);builder.downloader(new?OkHttpDownloader(client));Picasso.setSingletonInstance(builder.build()); }先判斷是否是Android 5.0之下,其實這步判斷也可以不加。
然后就是創建一個OkHttpClient,注意這個是Picasso包中的,不能使用OkHttp包中的同名類(因為3.0之后OkHttp的包名變了)。
為OkHttpClient設置一個SslSocketFactory,如果我們不設置,在OkHttpClient中會有一個默認的SslSocketFactory,具體源碼如
private?synchronized?SSLSocketFactory?getDefaultSSLSocketFactory()?{if?(defaultSslSocketFactory?==?null)?{try?{SSLContext?sslContext?=?SSLContext.getInstance("TLS");sslContext.init(null,?null,?null);defaultSslSocketFactory?=?sslContext.getSocketFactory();}?catch?(GeneralSecurityException?e)?{throw?new?AssertionError();?//?The?system?has?no?TLS.?Just?give?up.}}return?defaultSslSocketFactory; }對比兩部分代碼可以發現,區別之處在client.setSslSocketFactory(new PicassoSslSocketFactory(sc.getSocketFactory()));這一句,很明顯我們在sc.getSocketFactory()之外又封裝了一下,PicassoSslSocketFactory這個類就是解決問題的關鍵,下面我們會講到。
讓我們先看后續的3行代碼,這3行代碼就是替換底層的網絡請求框架。新建一個Picasso的Builder,然后為其設置downloader,至于Builder其他的成員則使用default對象。
最后使用setSingleLetonInstance這個函數,Picasso這個類實際上是單例模式,調用這個函數后就會將我們新建的Builder對象賦予成這個唯一的對象,之后我們使用Picasso任何其他函數實際上都會使用這個對象,這樣就實現了替換。這個函數源碼如下
public?static?void?setSingletonInstance(Picasso?picasso)?{synchronized?(Picasso.class)?{if?(singleton?!=?null)?{throw?new?IllegalStateException("Singleton?instance?already?exists.");}singleton?=?picasso;} }可以看到如果已經賦值過,則不能再賦值,否則會報錯。而如果我們使用過picasso其他函數,實際上會創建一個默認的對象,這樣就無法替換了。所以替換必須在使用Picasso任何功能之前,那么就是在Application的onCreate中了。
上面實現了替換網絡框架,實際上打開TLSv1.2是在PicassoSslSocketFactory中,這個類的代碼如下:
public?class?PicassoSslSocketFactory?extends?SSLSocketFactory?{private?static?final?String[]?TLS_SUPPORT_VERSION?=?{"TLSv1.1",?"TLSv1.2"};final?SSLSocketFactory?delegate;public?PicassoSslSocketFactory(SSLSocketFactory?base)?{this.delegate?=?base;}@Overridepublic?String[]?getDefaultCipherSuites()?{return?delegate.getDefaultCipherSuites();}@Overridepublic?String[]?getSupportedCipherSuites()?{return?delegate.getSupportedCipherSuites();}@Overridepublic?Socket?createSocket(Socket?s,?String?host,?int?port,?boolean?autoClose)?throws?IOException?{return?patch(delegate.createSocket(s,?host,?port,?autoClose));}@Overridepublic?Socket?createSocket(String?host,?int?port)?throws?IOException{return?patch(delegate.createSocket(host,?port));}@Overridepublic?Socket?createSocket(String?host,?int?port,?InetAddress?localHost,?int?localPort)?throws?IOException{return?patch(delegate.createSocket(host,?port,?localHost,?localPort));}@Overridepublic?Socket?createSocket(InetAddress?host,?int?port)?throws?IOException?{return?patch(delegate.createSocket(host,?port));}@Overridepublic?Socket?createSocket(InetAddress?address,?int?port,?InetAddress?localAddress,?int?localPort)?throws?IOException?{return?patch(delegate.createSocket(address,?port,?localAddress,?localPort));}private?Socket?patch(Socket?s)?{if?(s?instanceof?SSLSocket)?{((SSLSocket)?s).setEnabledProtocols(TLS_SUPPORT_VERSION);}return?s;}}可以看到比較簡單,實際上是一層代理。
所有的createSocket函數都被代理了,如果是SSLSocket,則使用setEnabledProtocols打開TLSv1.1和TLSv1.2,這樣在Android 5.0以下的版本中就可以使用TLSv1.2證書了。
這樣問題就解決了,看網上說新版本的picasso已經解決這個問題了,很多人說2.5.3版本但是沒有找到,官方好像一直停留在2.5.2版本。說實話這個版本bug不少,之前還遇到過5.0本地圖片加載失敗的問題(見剖析Picasso加載壓縮本地圖片流程(解決Android 5.0部分機型無法加載本地圖片的問題)),而目前網上能找到最新的版本是2.5.2.4b,這個應該不是官方的,雖然解決了不少問題,但是由于包名變了,如果要替換請根據項目的實際情況來。
?
總結
以上是生活随笔為你收集整理的解决Picasso在Android 5.0以下版本不兼容https导致图片不显示的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android魔术——手把手教你实现水晶
- 下一篇: 剖析Picasso中的内存缓存机制——L