近期在项目中遇到了一个问题,使用picasso加载图片在Android5.0以下版本图片显示不来。
由于之前在几个项目中都使用过picasso而且未出现类似问题,觉得值得好好研究一下。
简单定位一下问题所在,我们一直使用picasso大致会是下面的代码
Picasso.
with(
context).load(url).into(imageView)
;
我们知道into函数还有另外一个版本,可以添加callback,如下:
Picasso.
with(
context).load(url).into(imageView
, new Callback() {
@Override
public void
onSuccess() {
}
@Override
public 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
;
}
@Override
public String[]
getDefaultCipherSuites() {
return
delegate.getDefaultCipherSuites()
;
}
@Override
public String[]
getSupportedCipherSuites() {
return
delegate.getSupportedCipherSuites()
;
}
@Override
public Socket
createSocket(Socket s
, String host
, int port
, boolean autoClose)
throws IOException {
return patch(
delegate.createSocket(s
, host
, port
, autoClose))
;
}
@Override
public Socket
createSocket(String host
, int port)
throws IOException{
return patch(
delegate.createSocket(host
, port))
;
}
@Override
public Socket
createSocket(String host
, int port
, InetAddress localHost
, int localPort)
throws IOException{
return patch(
delegate.createSocket(host
, port
, localHost
, localPort))
;
}
@Override
public Socket
createSocket(InetAddress host
, int port)
throws IOException {
return patch(
delegate.createSocket(host
, port))
;
}
@Override
public 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,这个应该不是官方的,虽然解决了不少问题,但是由于包名变了,如果要替换请根据项目的实际情况来。