OkHttp簡單剖析

晴川落雨發表於2018-07-16

前言

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 攔截器

    為什麼要單獨講一下這個呢?因為他比較有意思唄!首先攔截器分為應用攔截和網路攔截,
我們這裡只講應用攔截。應用攔截器的嵌入流程如下圖所示:
複製程式碼

OkHttp簡單剖析

    可以攔截請求,也可以攔截響應,根據就提需要來定製。
    
    一個請求的攔截:
複製程式碼
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();
        }
    }
複製程式碼
    最後補上一段,這個應用攔截器都是針對全域性的,而你又使用的是單例模式,所以在使用過程當中一定要引起注意。複製程式碼

相關文章