OkHttp源碼徹底解析(四)OkHttp攔截器的作用

 

 

 

本系列文章:

OkHttp源碼徹底解析(一)OkHttp請求流程

OkHttp源碼徹底解析(二)OkHttp架構及API源碼

OkHttp源碼徹底解析(三)OkHttp3.0攔截器原理——責任鏈模式

OkHttp源碼徹底解析(四)OkHttp攔截器的作用

OkHttp源碼徹底解析(五)OkHttp連接池


目錄

 

 

攔截器 

OkHttp中的攔截器 

失敗重連以及重定向的攔截器:RetryAndFollowUpInterceptor

橋接攔截器BridgeInterceptor

緩存攔截器CacheInterceptor

連接攔截器 和 最後的請求服務器的攔截器

總結

 

 

 

自定義攔截器 

我們可以通過自定義攔截器做什麼

1.重寫請求

2.重寫響應

 

 

如何使用自定義攔截器

Application interceptors

Network interceptors

選擇使用Application或Network攔截器?


攔截器 

攔截器是OkHttp中提供一種強大機制,它可以實現網絡監聽、請求以及響應重寫、請求失敗重試等

功能。下面舉一個簡單打印日誌的栗子,此攔截器可以打印出網絡請求以及響應的信息。

 

有關攔截器的邏輯流程及原理,看OkHttp攔截器原理

 

 

OkHttp中的攔截器 

我們來看看OkHttp中各個攔截器的順序

這些Interceptor中每一個的職責

失敗重連以及重定向的攔截器:RetryAndFollowUpInterceptor

失敗重連攔截器核心源碼:
一個循環來不停的獲取response。每循環一次都會獲取下一個request,如果沒有,則返回response,退出循環。而獲取下一個request的邏輯,是根據上一個response返回的狀態碼,分別作處理。


橋接攔截器BridgeInterceptor

橋接攔截器的主要作用是將:

  1. 請求從應用層數據類型類型轉化爲網絡調用層的數據類型。

     

  2. 將網絡層返回的數據類型 轉化爲 應用層數據類型。

    1. 保存最新的cookie(默認沒有cookie,需要應用程序自己創建,詳見
                     [Cookie的API]
                     (https://square.github.io/okhttp/3.x/okhttp/okhttp3/CookieJar.html)
                     和
                     [Cookie的持久化]
                     (https://segmentfault.com/a/1190000004345545));
         2. 如果request中使用了"gzip"壓縮,則進行Gzip解壓。解壓完畢後移除Header中的"Content-Encoding"和"Content-Length"(因爲Header中的長度對應的是壓縮前數據的長度,解壓後長度變了,所以Header中長度信息實效了);
         3. 返回response。
    
    

    簡單看一下一個正真的http請求都包含了什麼:

可以發現除了請求URL之外,瀏覽器還給你拼接了請求頭信息,所以既然上面所說BridgeInterceptor的第一步是將用戶請求創建真正的Network Request 

BridgeInterceptor爲Request設置User-Agent、Cookie、Accept-Encoding等相關請求頭信息。到此爲止一個完整的NetWork Request 就構建完畢,是時候發起真正的網絡請求了

 

 

 

 

 

 

 

補充:Keep-Alive 連接:
HTTP中的keepalive連接在網絡性能優化中,對於延遲降低與速度提升的有非常重要的作用。
通常我們進行http連接時,首先進行tcp握手,然後傳輸數據,最後釋放

http連接

這種方法的確簡單,但是在複雜的網絡內容中就不夠用了,創建socket需要進行3次握手,而釋放socket需要是4次。重複的連接與釋放tcp連接就像每次僅僅擠1mm的牙膏就合上牙膏蓋子接着再打開接着擠一樣。而每次連接大概是TTL一次的時間(也就是ping一次),在TLS環境下消耗的時間就更多了。很明顯,當訪問複雜網絡時,延時(而不是帶寬)將成爲非常重要的因素。
當然,上面的問題早已經解決了,在http中有一種叫做keepalive connections的機制,它可以在傳輸數據後仍然保持連接,當客戶端需要再次獲取數據時,直接使用剛剛空閒下來的連接而不需要再次握手

Keep-Alive連接

在現代瀏覽器中,一般同時開啓6~8個keepalive connections的socket連接,並保持一定的鏈路生命,當不需要時再關閉;而在服務器中,一般是由軟件根據負載情況決定是否主動關閉。

緩存攔截器CacheInterceptor

緩存攔截器的主要作用是將請求 和 返回 關連得保存到緩存中。客戶端與服務端根據一定的機制,在需要的時候使用緩存的數據作爲網絡請求的響應,節省了時間和帶寬。

客戶端與服務端之間的緩存機制:

  • 作用:將HTPTP和HTTPS的網絡返回數據緩存到文件系統中,以便在服務端數據沒發生變化的情況下複用,節省時間和帶寬;
  • 工作原理:客戶端發器網絡請求,如果緩存文件中有一份與該請求匹配(URL相同)的完整的返回數據(比如上一次請求返回的結果),那麼客戶端就會發起一個帶條件(例子,服務端第一次返回數據時,在response的Header中添加上次修改的時間信息:Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT;客戶端再次請求的時候,在request的Header中添加這個時間信息:If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT)的獲取資源的請求。此時服務端根據客戶端請求的條件,來判斷該請求對應的數據是否有更新。如果需要返回的數據沒有變化,那麼服務端直接返回 304 "not modified"。客戶端如果收到響應碼味304 的信息,則直接使用緩存數據。否則,服務端直接返回更新的數據。具體如下圖所示:

     

    帶緩存的網絡請求流程

客戶端緩存的實現:

OKHttp3的緩存類爲Cache類,它實際上是一層緩存邏輯的包裝類。內部有個專門負責緩存文件讀寫的類:DiskLruCache。於此同時,OKHttp3還定義了一個緩存接口:InternalCache。這個緩存接口類作爲Cache的成員變量其所有的實現,都是調用了Cahce類的函數實現的。它們間具體的關係如下:

緩存的工作機制

InternalCache接口:

    public interface InternalCache {
      Response get(Request request) throws IOException;
    
      CacheRequest put(Response response) throws IOException;
    
      /**
       * 移除request相關的緩存數據
       */
      void remove(Request request) throws IOException;
    
      /**
       * 用網絡返回的數據,更新緩存中數據
       */
      void update(Response cached, Response network);
    
      /** 統計網絡請求的數據 */
      void trackConditionalCacheHit();
    
      /** 統計網絡返回的數據 */
      void trackResponse(CacheStrategy cacheStrategy);
    }

Note:setInternalCache這個方法是不對應用程序開放的,應用程序只能使用cache(Cache cache)這個方法來設置緩存。並且,Cache內部的internalCache是final的,不能被修改。總結:internalCache這個接口雖然是public的,但實際上,應用程序是無法創建它,並附值到OkHttpClient中去的。

那麼問題來了,爲什麼不用Cahce實現InternalCache這個接口,而是以組合的方式,在它的內部實現都調用了Cache的方法呢?
因爲:

  • 在Cache中,InternalCache接口的兩個統計方法:trackConditionalCacheHit和trackResponse (之所以要統計,是爲了查看緩存的效率。比如總的請求次數與緩存的命中次數。)需要用內置鎖進行同步。
  • Cache中,將trackConditionalCacheHit和trackResponse方法 從public變爲爲privite了。不允許子類重寫,也不開放給應用程序。

Cache類:

  • 將網絡請求的文件讀寫操作委託給內部的DiskLruCache類來處理。

  • 負責將url轉換爲對應的key。

  • 負責數據統計:寫成功計數,寫中斷計數,網絡調用計數,請求數量計數,命中數量計數。

  • 負責網絡請求的數據對象Response 與 寫入文件系統中的文本數據 之間的轉換。使用內部類Entry來實現。具體如下:

      網絡請求的文件流文本格式如下:
              {
                   http://google.com/foo
                   GET
                   2
                   Accept-Language: fr-CA
                   Accept-Charset: UTF-8
                   HTTP/1.1 200 OK
                   3
                   Content-Type: image/png
                   Content-Length: 100
                   Cache-Control: max-age=600
                   ...
                }     
      內存對象的Entry格式如下.
              private static final class Entry {
                  private final String url;
                  private final Headers varyHeaders;
                  private final String requestMethod;
                  private final Protocol protocol;
                  private final int code;
                  private final String message;
                  ...
              }           
    

通過okio的讀寫API,實現它們之間靈活的切換。

DiskLruCache類:

簡介:一個有限空間的文件緩存。

  • 每個緩存數據都有一個string類型的key和一些固定數量的值。
  • 緩存的數據保存在文件系統的一個目錄下。這個目錄必須是該緩存獨佔的:因爲緩存運行時會刪除和修改該目錄下的文件,因而該緩存目錄不能被其他線程使用。
  • 緩存限制了總的文件大小。如果存儲的大小超過了限制,會以LRU算法來移除一些數據。
  • 可以通過edit,update方法來修改緩存數據。每次調用edit,都必須以commit或者abort結束。commit是原子操作。
  • 客戶端調用get方法來讀取一個緩存文件的快照(存儲了key,快照序列號,數據源和數據長度)。

緩存使用了日誌文件(文件名爲journal)來存儲緩存的數據目錄和操作記錄。一個典型的日誌文件的文本文檔如下:

     //第一行爲緩存的名字
     libcore.io.DiskLruCache                            
     1                                                    //緩存的版本
     100                                                 //應用版本
     2                                                   //值的數量          
     //緩存記錄:操作 key 第一個數據的長度 第二個數據的長度    
     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054    
     DIRTY 335c4c6028171cfddfbaae1a9c313c52
     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
     REMOVE 335c4c6028171cfddfbaae1a9c313c52
     DIRTY 1ab96a171faeeee38496d8b330771a7a
     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
     READ 335c4c6028171cfddfbaae1a9c313c52
     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6

操作記錄:狀態+key+額外擦數

  • CLEAN key param0 param1:該key的對應的數據爲最新的有效數據,後續爲額外的參數
  • DIRTY key:該key對應的數據被創建或被修改。
  • REMOVE key:該key對應的數據被刪除。
  • READ key:改key對應的數據被讀取的記錄。——用於LRU算法來統計哪些數據是最新的數據。

一些冗餘的操作記錄,比如DIRTY,REMOVE...比較多的時候(大於2000個,或者超過總數量),會發器線程對該日誌文件進行壓縮(刪除這些冗餘的日誌記錄)。此時,會創建一個journal.tmp文件作爲臨時的文件,供緩存繼續使用。同時還有個journal.bkp文件,用作journal文件的臨時備份。
換文文件結構如下:

緩存的文件mu lu

工作原理:

  • 每個緩存記錄在內存中的對象封裝爲Entry類

       private final class Entry {         
         private final String key; //緩存對應的key
         
         private final long[] lengths; //文件的長度
         private final File[] cleanFiles; //有效的數據文件
         private final File[] dirtyFiles; //正在修改的數據文件
     
         private boolean readable;//是否可讀
     
         private Editor currentEditor;//當前的編輯器。一個Entry一個時候只能被一個編輯器修改。
    
         private long sequenceNumber; //唯一序列號,相當於版本號,當內存中緩存數據發生變化時,該序列號會改變。
         
         ...    
         }
    
  • 創建緩存快照對象,作爲每次讀取緩存時的一個內容快照對象:

       public final class Snapshot implements Closeable {
         private final String key;   //緩存的key
         private final long sequenceNumber; //創建快照的時候的緩存序列號
         private final Source[] sources;//數據源,okio的API,可以直接讀取
         private final long[] lengths;//數據長度。
         ...
         }
    

    內容快照作爲讀去緩存的對象(而不是將Entry直接返回)的作用:
    (1). 內容快照的數據結構方式更便於數據的讀取(將file轉換爲source),並且隱藏了Entry的細節;
    (2). 內容快照在創建的時候記錄了當時的Entry的序列號。因而可以用快照的序列號與緩存的序列號對比,如果序列號不相同,則說明緩存數據發生了修改,該條數據就是失效的。

  • 緩存內容Entry的這種工作機制(單個editor,帶有序列號的內容快照)以最小的代價,實現了單線程修改,多線程讀寫的數據對象(否則則需要使用複雜的鎖機制)既添降低了邏輯的複雜性,又提高了性能(缺點就是高併發情況下,導致數據頻繁失效,導致緩存的命中率降低)。
  • 變化的序列號計數在很多涉及併發讀取的機制都有使用。比如:SQlite的連接。
  • DiskLruCache緩存文件工作流:

DiskLruCache緩存工作流

其返回的SnapShot數據快照,提供了Source接口(okio),供外部內類Cache直接轉化爲內存對象Cache.Entry。外部類進一步Canche.Entry轉化外OKHttpClient使用的Response。

OkHttpClient從緩存中獲取一個url對應的緩存數據的數據格式變化過程如下:

緩存數據格式變化過程

LRU的算法體現在:DiskLreCache的日誌操作過程中,每一次讀取緩存都產生一個READ的記錄。由於緩存的初始化是按照日誌文件的操作記錄順序來讀取的,所以這相當於把這條緩存數據放置到了緩存隊列的頂端,也就完成了LRU算法:last recent used,最近使用到的數據最新。

 

連接攔截器 和 最後的請求服務器的攔截器

這兩個連接器基本上完成了最後發起網絡請求的工作。追所以劃分爲兩個攔截器,除了解耦之外,更重要的是在這兩個流程之間還可以插入一個專門爲WebSocket服務的攔截器( WebSocket一種在單個 TCP 連接上進行全雙工通訊的協議,本文不做詳解)。

 

總結

1.失敗重連攔截器:
一個循環來不停的獲取response。每循環一次都會獲取下一個request,如果沒有,則返回response,退出循環。而獲取下一個request的邏輯,是根據上一個response返回的狀態碼,分別作處理。

2.橋接攔截器:

請求從應用層數據類型類型轉化爲網絡調用層的數據類型。將網絡層返回的數據類型 轉化爲 應用層數據類型。(補足缺失的請求頭等)

3.緩存攔截器:

CacheInterceptor主要作用是將請求 和 返回 關連得保存到緩存中。客戶端與服務端根據一定的機制,在需要的時候使用緩存的數據作爲網絡請求的響應,節省了時間和帶寬。

4.連接攔截器 

與請求服務器的攔截器是網絡交互的關鍵。爲請求服務器攔截器建立可用的連接,創建用於網絡IO流  的RealConnection對象

5.請求服務器的攔截器:

完成了最後發起網絡請求的工作。將HTTP請求寫入網絡IO流,從IO流讀取網絡數據

與連接攔截器要劃分爲兩個攔截器,除了解耦之外,更重要的是在這兩個流程之間還可以插入一個專門爲WebSocket服務的攔截器( WebSocket一種在單個 TCP 連接上進行全雙工通訊的協議,本文不做詳解)。

 

 

 

 

自定義攔截器 

 

 

我們可以通過自定義攔截器做什麼

1.重寫請求

攔截器可以添加、移除或者替換請求頭。甚至在有請求主體時候,可以改變請求主體。舉個栗子,你可以使用application interceptor添加經過壓縮之後的請求主體,當然,這需要你將要連接的服務端支持處理壓縮數據。

/** This interceptor compresses the HTTP request body. Many webservers can't handle this! */
final class GzipRequestInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request originalRequest = chain.request();
    if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
      return chain.proceed(originalRequest);
    }

    Request compressedRequest = originalRequest.newBuilder()
        .header("Content-Encoding", "gzip")
        .method(originalRequest.method(), gzip(originalRequest.body()))
        .build();
    return chain.proceed(compressedRequest);
  }

  private RequestBody gzip(final RequestBody body) {
    return new RequestBody() {
      @Override public MediaType contentType() {
        return body.contentType();
      }

      @Override public long contentLength() {
        return -1; // We don't know the compressed length in advance!
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
        body.writeTo(gzipSink);
        gzipSink.close();
      }
    };
  }
}

2.重寫響應

和重寫請求相似,攔截器可以重寫響應頭並且可以改變它的響應主體。相對於重寫請求而言,重寫響應通常是比較危險的一種做法,因爲這種操作可能會改變服務端所要傳遞的響應內容的意圖。
當然,如果你 比較奸詐 在不得已的情況下,比如不處理的話的客戶端程序接受到此響應的話會Crash等,以及你還可以保證解決重寫響應後可能出現的問題時,重新響應頭是一種非常有效的方式去解決這些導致項目Crash的問題。舉個栗子,你可以修改服務器返回的錯誤的響應頭Cache-Control信息,去更好地自定義配置響應緩存保存時間。

/** Dangerous interceptor that rewrites the server's cache-control header. */
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Response originalResponse = chain.proceed(chain.request());
    return originalResponse.newBuilder()
        .header("Cache-Control", "max-age=60")
        .build();
  }
};

 

OkHttp是由Square發佈的一個HTTP client,它支持高速緩存服務器響應.

緩存:

如果服務器支持緩存,請求返回的Response會帶有這樣的Header:Cache-Control, max-age=xxx,這種情況下我們只需要手動給okhttp設置緩存就可以讓okhttp自動幫你緩存了。這裏的max-age的值代表了緩存在你本地存放的時間。
 

OkHttpClient okHttpClient = new OkHttpClient();
OkHttpClient newClient = okHttpClient.newBuilder()
               .cache(new Cache(mContext.getCacheDir(), 10240*1024))
               .connectTimeout(20, TimeUnit.SECONDS)
               .readTimeout(20, TimeUnit.SECONDS)
               .build();

如果服務器不支持緩存就可能沒有指定這個頭部,這種情況下我們就需要使用Interceptor來重寫Respose的頭部信息,從而讓okhttp支持緩存。

Interceptor interceptor = new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            Response response = chain.proceed(request);
 
            String cacheControl = request.cacheControl().toString();
            if (TextUtils.isEmpty(cacheControl)) {
                cacheControl = "public, max-age=60";
            }
            return response.newBuilder()
                    .header("Cache-Control", cacheControl)
                    .removeHeader("Pragma")
                    .build();
        }
    };

 

 

如何使用自定義攔截器

class LoggingInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request request = chain.request();

    long t1 = System.nanoTime();
    logger.info(String.format("Sending request %s on %s%n%s",
        request.url(), chain.connection(), request.headers()));

    Response response = chain.proceed(request);

    long t2 = System.nanoTime();
    logger.info(String.format("Received response for %s in %.1fms%n%s",
        response.request().url(), (t2 - t1) / 1e6d, response.headers()));

    return response;
  }
}

上面自定義了一個攔截器

那麼如何添加攔截器呢?首先,我們要知道,攔截器分兩種:Application和Network攔截器

攔截器可以以application或者network兩種方式註冊,分別調用addInterceptor()以及addNetworkInterceptor方法進行註冊。我們使用上文中日誌攔截器的使用來體現出兩種註冊方式的不同點。

Application interceptors

首先通過調用addInterceptor()OkHttpClient.Builder鏈式代碼中註冊一個application攔截器:

OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new LoggingInterceptor())
    .build();

Request request = new Request.Builder()
    .url("http://www.publicobject.com/helloworld.txt")
    .header("User-Agent", "OkHttp Example")
    .build();

Response response = client.newCall(request).execute();
response.body().close();

請求的URLhttp://www.publicobject.com/helloworld.txt被重定向成https://publicobject.com/helloworld.txt,OkHttp支持自動重定向。注意,我們的application攔截器只會被調用一次,並且調用chain.proceed()之後獲得到的是重定向之後的最終的響應信息,並不會獲得中間過程的響應信息:

INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example

INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive

我們可以看到請求的URL被重定向了,因爲response.request().url()request.url()是不一樣的。日誌打印出來的信息顯示兩個不同的URL。客戶端第一次請求執行的url爲http://www.publicobject.com/helloworld.txt,而響應數據的url爲https://publicobject.com/helloworld.txt

 

Network interceptors

註冊一個Network攔截器和註冊Application攔截器方法是非常相似的。註冊Application攔截器調用的是addInterceptor(),而註冊Network攔截器調用的是addNetworkInterceptor()

OkHttpClient client = new OkHttpClient.Builder()
    .addNetworkInterceptor(new LoggingInterceptor())
    .build();

Request request = new Request.Builder()
    .url("http://www.publicobject.com/helloworld.txt")
    .header("User-Agent", "OkHttp Example")
    .build();

Response response = client.newCall(request).execute();
response.body().close();

我們運行這段代碼,發現這個攔截被執行了兩次。一次是初始化也就是客戶端第一次向URL爲http://www.publicobject.com/helloworld.txt發出請求,另外一次則是URL被重定向之後客戶端再次向https://publicobject.com/helloworld.txt發出請求。

INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}
User-Agent: OkHttp Example
Host: www.publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip

INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/html
Content-Length: 193
Connection: keep-alive
Location: https://publicobject.com/helloworld.txt

INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1}
User-Agent: OkHttp Example
Host: publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip

INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive

NetWork請求包含了更多信息,比如OkHttp爲了減少數據的傳輸時間以及傳輸流量而自動添加的請求頭Accept-Encoding: gzip希望服務器能返回經過壓縮過的響應數據。Network 攔截器調用Chain方法後會返回一個非空的Connection對象,它可以用來查詢客戶端所連接的服務器的IP地址以及TLS配置信息。

選擇使用Application或Network攔截器?

每一個攔截器都有它的優點。

Application interceptors

  • 無法操作中間的響應結果,比如當URL重定向發生以及請求重試等,只能操作客戶端主動第一次請求以及最終的響應結果。
  • 在任何情況下只會調用一次,即使這個響應來自於緩存。
  • 可以監聽觀察這個請求的最原始未經改變的意圖(請求頭,請求體等),無法操作OkHttp爲我們自動添加的額外的請求頭,比如If-None-Match
  • 允許short-circuit (短路)並且允許不去調用Chain.proceed()。(編者注:這句話的意思是Chain.proceed()不需要一定要調用去服務器請求,但是必須還是需要返回Respond實例。那麼實例從哪裏來?答案是緩存。如果本地有緩存,可以從本地緩存中獲取響應實例返回給客戶端。這就是short-circuit (短路)的意思。。囧)
  • 允許請求失敗重試以及多次調用Chain.proceed()

Network Interceptors

  • 允許操作中間響應,比如當請求操作發生重定向或者重試等。
  • 不允許調用緩存來short-circuit (短路)這個請求。(編者注:意思就是說不能從緩存池中獲取緩存對象返回給客戶端,必須通過請求服務的方式獲取響應,也就是Chain.proceed()
  • 可以監聽數據的傳輸
  • 允許Connection對象裝載這個請求對象。(編者注:Connection是通過Chain.proceed()獲取的非空對象)


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章