美文网首页Android进阶之路
解决Picasso在Android 5.0以下版本不兼容http

解决Picasso在Android 5.0以下版本不兼容http

作者: chzphoenix | 来源:发表于2017-09-04 10:16 被阅读99次

    近期在项目中遇到了一个问题,使用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本地图片加载失败的问题(有时间我整理一篇出来),而目前网上能找到最新的版本是2.5.2.4b,这个应该不是官方的,虽然解决了不少问题,但是由于包名变了,如果要替换请根据项目的实际情况来。

    相关文章

      网友评论

        本文标题:解决Picasso在Android 5.0以下版本不兼容http

        本文链接:https://www.haomeiwen.com/subject/nnwsjxtx.html