解决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() {
    @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协议的默认配置图如下:

解决Picasso在Android 5.0以下版本不兼容https导致图片不显示_第1张图片

从上图可以得出如下结论:

  • 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,这个应该不是官方的,虽然解决了不少问题,但是由于包名变了,如果要替换请根据项目的实际情况来。

你可能感兴趣的:(android)