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

这里写图片描述

 

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

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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BennuCTech

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值