OkHttp简单剖析

271 阅读6分钟

前言

OkHttp框架当下非常火热,使用这么久了也没怎么认真研究过!这几天抽空整理了一下,分享出来给大家一些参考吧!

官方地址

http://square.github.io/okhttp/

https://github.com/square/okhttp

版本

    目前最新版本v3.10.0
    
    这里为什么要画蛇添足的强调一下版本呢?因为OkHttp3.4.1版本之前和nginx的v1.13.6之后版本有冲突,
会导致请求失败。如果有小伙伴遇到这个问题,不妨试试将OkHttp框架升一下,应该就能解决这个问题了。
(笔者当时可是查了好久才发现的,真是被坑惨了!)
   
    具体原因,可以参考以下链接:
    https://trac.nginx.org/nginx/ticket/1397
    https://github.com/PhilippC/keepass2android/issues/44

使用

官方示例

    简单瞅瞅官方的示例,无外乎是构建Request然后拿到Response。
    //GET A URL
    //This program downloads a URL and print its contents as a string. Full source.
    
    OkHttpClient client = new OkHttpClient();
    
    String run(String url) throws IOException {
          Request request = new Request.Builder()
              .url(url)
              .build();
          //同步请求
          Response response = client.newCall(request).execute();
          return response.body().string();
    }
    
    
    
    //POST TO A SERVER
    //This program posts data to a service. Full source.
    
    public static final MediaType JSON
        = MediaType.parse("application/json; charset=utf-8");
    
    OkHttpClient client = new OkHttpClient();
    
    String post(String url, String json) throws IOException {
          RequestBody body = RequestBody.create(JSON, json);
          Request request = new Request.Builder()
              .url(url)
              .post(body)
              .build();
          //同步请求
          Response response = client.newCall(request).execute();
          return response.body().string();
    } 

实际使用

    上面已经标注了,官方示例使用的是同步请求的方式,意味着你必须放在线程中取执行。实际使用中,
我们还需要对OkHttpClient配置相关参数和进行异步请求操作。

配置具体如下:

   OkHttpClient.Builder builder = new OkHttpClient.Builder();
   //设置连接超时
   builder.connectTimeout(time, TimeUnit.SECONDS);
   //设置读取超时
   builder.readTimeout(time, TimeUnit.SECONDS);
   //允许重定向
   builder.followRedirects(true);
   //增加cookie(这个后面也可以单独讲)
   builder.cookieJar(cookiesManager);
   //添加拦截器(这个后面单独讲)
   builder.addInterceptor(new Interceptor());
   ……(此处省略很多属性,如果需要去看文档吧!)
   OkHttpClient mOkHttpClient = builder.build();
    我们项目中使用,一般都要设计成单例模式,这样才能体现出OkHttp的优势。原文如下:
    OkHttp performs best when you create a single {@code OkHttpClient} instance and reuse
it for all of your HTTP calls. This is because each client holds its own connection pool 
and thread pools. Reusing connections and threads reduces latency and saves memory. 
Conversely, creating a client for each request wastes resources on idle pools.

    那么问题来了,假如我要针对个别请求修改配置肿么办?别方,官网也给出了答案:
    You can customize a shared OkHttpClient instance with {@link #newBuilder()}. This 
builds a client that shares the same connection pool, thread pools, and configuration. 
Use the builder methods to configure the derived client for a specific purpose.
代码如下:
       OkHttpClient eagerClient = mOkHttpClient.newBuilder()
               .readTimeout(500, TimeUnit.MILLISECONDS) //修改读取超时,其他配置不变
               .build();

异步请求如下:

    Call call = mOkHttpClient.newCall(request);
    call.enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
            //失败的回调
        }

        @Override
        public void onResponse(Call call, Response response) throws IOException {
            //成功的回调
        }
      });
    下面我们重点讲一下成功的回调,Response这个对象他就很神奇了。稳住,我们能赢!!!
我们一般Http接口请求的目标都是Json对象,它的
    解析如下:
    String content = response.body().string();
    这个content就是接口返回的数据,继续进行Json解析就OK了。老铁,没毛病呀!的确这样的
流程并没有毛病,but你如果是一个有追求的人,你可能会这样写
    @Override
        public void onResponse(Call call, Response response) throws IOException {
            //成功的回调
            String content = response.body().string();
            ……
            中间省略一堆自定义的骚操作
            ……
            //通过封装的接口回调给外面的方法
            youcustomInterface.onBack(response)
        }
    老铁,这一波封装很稳。But,你会发现你在外部方法进行response.body().string()的时候程
序FC了!错误如下:
    
    **java.lang.IllegalStateException: closed**
    
    什么原因呢?是时候展现一波真正的源码了:

    先看看body()方法,注释解释道ResponseBody对象只能消费一次,什么叫做消费一次呢?咱们
往下看看string()方法
     /**
       * Returns a non-null value if this response was passed to {@link Callback#onResponse}
       * or returned from {@link Call#execute()}. Response bodies must be {@linkplain 
       * ResponseBody closed} and may be consumed only once.
       *
       * <p>This always returns null on responses returned from {@link #cacheResponse}, {@link
       * #networkResponse}, and {@link #priorResponse()}.
       */
      public @Nullable ResponseBody body() {
        return body;
      }
    string()方法进行一次流读取操作,值得注意的是在finally里面做了一个关闭的操作,往下看看
      /**
       * Returns the response as a string decoded with the charset of the Content-Type header. 
       * If that header is either absent or lacks a charset, this will attempt to decode the 
       * response body in accordance to <a href="https://en.wikipedia.org/wiki/Byte_order_mark">
       * its BOM</a> or UTF-8.
       * Closes {@link ResponseBody} automatically.
       *
       * <p>This method loads entire response body into memory. If the response body is very 
       * large this may trigger an {@link OutOfMemoryError}. Prefer to stream the response 
       * body if this is a possibility for your response.
       */
      public final String string() throws IOException {
        BufferedSource source = source();
        try {
          Charset charset = Util.bomAwareCharset(source, charset());
          return source.readString(charset);
        } finally {
          Util.closeQuietly(source);
        }
      }
    这里貌似也没有看出来什么,只是关闭操作,那么我们继续往后看看
      /**
        * Closes {@code closeable}, ignoring any checked exceptions. Does nothing if
        * {@code closeable} is null.
        */
      public static void closeQuietly(Closeable closeable) {
        if (closeable != null) {
          try {
            closeable.close();
          } catch (RuntimeException rethrown) {
            throw rethrown;
          } catch (Exception ignored) {
          }
        }
      }

    重头戏来了,看方法的注释,这个close()方法用来关闭流并释放资源,如果流已经关闭再
次调用这个方法将是无效的。这里就可以看出来,流被关闭后,你再读取数据就会抛出
IllegalStateException的异常。
    /**
     * Closes this stream and releases any system resources associated
     * with it. If the stream is already closed then invoking this
     * method has no effect.
     *
     * <p> As noted in {@link AutoCloseable#close()}, cases where the
     * close may fail require careful attention. It is strongly advised
     * to relinquish the underlying resources and to internally
     * <em>mark</em> the {@code Closeable} as closed, prior to throwing
     * the {@code IOException}.
     *
     * @throws IOException if an I/O error occurs
     */
    public void close() throws IOException;
    废了老大劲了,终于看明白了,然后你以为结束了?To young to simple!还是sting()方法,
他的注释上还有一句提示 This method loads entire response body into memory. If the 
response body is very large this may trigger an {@linkOutOfMemoryError}. Prefer to
stream the response body if this is a possibility for your response.
    这句话很重要,他的意思是string()去读取数据的时候,stream流不能太大,不然会导致内
存溢出。为什么要强调这个呢?因为OKHttp还有下载的功能,而你又使用的是单例模式,所以你
搭建框架的时候一定要做好区分。

Interceptor 拦截器

    为什么要单独讲一下这个呢?因为他比较有意思呗!首先拦截器分为应用拦截和网络拦截,
我们这里只讲应用拦截。应用拦截器的嵌入流程如下图所示:

    可以拦截请求,也可以拦截响应,根据就提需要来定制。
    
    一个请求的拦截:
class RequestInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request originalRequest = chain.request();
            Request requestWithUserAgent = originalRequest.newBuilder()
                    .header("User-Agent", "your ua")//添加UA
                    .build();
            return chain.proceed(requestWithUserAgent);
        }
    }
    一个响应的拦截:
class ResponseInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            return chain.proceed(request);
        }
    }
    这里重点讲一下响应拦截,为什么呢?因为上文提到过,你关于response.body().string()的使用会出问题。
class ResponseInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            Response response = chain.proceed(request);
            String bodyMsg = response.body().string();
            ……
            一系列骚操作
            ……
            MediaType mediaType = response.body().contentType();
            //这边重新构建了一个response body,否则的话你在后面的响应无法继续解析了
            ResponseBody responseBody = ResponseBody.create(mediaType, bodyMsg);
            return response.newBuilder().body(responseBody).build();
        }
    }
    最后补上一段,这个应用拦截器都是针对全局的,而你又使用的是单例模式,所以在使用过程当中一定要引起注意。