OkHttp 源碼簡要分析三 (攔截器及其細節)

上一章提到了 OkHttp 的 RealCall 類,獲取報文是通過 getResponseWithInterceptorChain() 方法,這裏面用到了責任鏈模式,看看代碼

  Response getResponseWithInterceptorChain() throws IOException {
    List<Interceptor> interceptors = new ArrayList<>();
    // 0 OkHttpClient 創建時,我們自定義的攔截器
    interceptors.addAll(client.interceptors());
    // 1 失敗重試以及重定向
    interceptors.add(retryAndFollowUpInterceptor);
    // 2 把用戶構造的請求轉換爲發送到服務器的請求、把服務器返回的響應轉換爲請求的響應的
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    // 3 讀取緩存及更新緩存
    interceptors.add(new CacheInterceptor(client.internalCache()));
    // 4 與服務器建立連接
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      // 5 不是webSocket,網絡攔截器
      interceptors.addAll(client.networkInterceptors());
    }
    // 6 向服務器發送請求數據,消息頭就是在這一步設置進去的;從服務器讀取響應數據
    interceptors.add(new CallServerInterceptor(forWebSocket));
    // 責任鏈模式
    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }

責任鏈模式是一種設計模式,對此不太瞭解的可以搜一下相關資料,它會形成一個串,從上到下,然後再把結果依次從下往上返回,我們可以任意添加攔截器按照規範去做任意操作。

0 是我們自定義的攔截器,這個最後再講;

1 是失敗重試以及重定向,我們取消請求時也是把取消的屬性存儲在這個類裏,同時對外暴露方法;這個攔截器中,創建了 StreamAllocation 對象,createAddress() 這個方法創建了 Address 對象,它是 StreamAllocation 其中的一個屬性,這裏是初始化一個連接對象,然後開啓while循環,在循環中,把它傳給下一個攔截器中;接收到 Response 後,會調用followUpRequest() 方法檢查是否需要重定向,不需要的話跳出while循環;如果需要,會有個次數檢查,不能超過20次,把它傳給下一個攔截器中,繼續第一次的步驟;

2 BridgeInterceptor 這個有點意思,Request 中有我們添加的消息頭 Headers,在 BridgeInterceptor 中,會對 Request 中消息頭做些修改,比如 Post 請求中,判斷 body.contentLength() 的長度,添加 Content-Length 或 Transfer-Encoding 中的一個,添加一個就會把另外一個移除;添加 Content-Type、Connection 等屬性,如果我們沒有設置 Accept-Encoding 屬性,這裏會自動添加爲 gzip 壓縮格式;然後就是把修改頭部信息後的 Request 傳給下一個連接器,接收回傳的 Response,在獲取 Response 後會判斷報文如果是以 gzip壓縮過的,則會先進行解壓縮,移除響應中的 Content-Encoding 和 Content-Length,通過 Okio 來解壓報文,創建新的 ResponseBody;

3 是緩存攔截,這個裏面判斷挺多的,就是爲了節省流量,可以考慮使用本地緩存,減少請求次數同時也減輕了服務端的壓力,這個需要在創建 OkHttpClient 時設置 Cache 值。

4  ConnectInterceptor 中的邏輯很簡單,這裏獲取了 RetryAndFollowUpInterceptor 中創建的 StreamAllocation 對象,然後創建了 HttpStream 對象,使用http1.1,對應的是 Http1xStream 這個實現類,然後把這些對象傳遞給了下一個攔截器;

5 這個也是外面傳進來的自定義的攔截器,這個叫網絡攔截器,比如常見的是 FaceBook 開源的 StethoInterceptor 這個攔截器。

6 CallServerInterceptor 這個是真正的向服務器請求數據的類,這個裏面牽涉消息頭和消息體的寫入問題httpStream .writeRequestHeaders(request) 這行代碼就是把 Request 的請求方式及url和 HTTP/1.1 拼接在一起形成名爲 requestLine 的字符串,然後把requestLine添加到 BufferedSink 對象中,緊接着就是把消息頭裏面的信息也添加到 BufferedSink 中,我們添加在 Request 中的消息頭信息就這麼添加到Okio中,爲下一步做準備;然後就是判斷是否是post請求及是否存在RequestBody,如果有,則寫入請求體,把其中的內容寫入一個新的 Sink 中進行請求; httpStream.readResponseHeaders() 是用來讀取響應信息的,去讀取返回的信息,一旦有數據返回,馬上把它封裝到 Response.Builder 中,接着設置一些參數後調用 build() 方法創建 Response,如果不是WebSocket請求或響應碼不爲101,則調用httpStream.openResponseBody(response) 創建一個 RealResponseBody 對象,封裝了響應頭和報文,然後創建新的 Response 對象返回給上一層。


以上是攔截器模式,現在重點說說攔截器 0 ,也就是我們自己定義的攔截器。我們可以寫一個類,實現 Interceptor 接口,然後通過創建 OkHttpClient 時,使用 addInterceptor() 方法把它添加進去,我們需要自定義哪些攔截器呢?比如說想打印一下網絡請求的耗時、想打印一些請求頭、響應頭的日誌或者說想添加網絡請求的公共參數等,都可以用到。現在先說打印網絡耗時,如下

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

        long t1 = SystemClock.elapsedRealtime();
        Response response = chain.proceed(request);
        long t2 = SystemClock.elapsedRealtime();
        Log.e("LoggingInterceptor", "Received response for " + "   " +
                response.request().url() + "   " + (t2 - t1));

        return response;

    }
}

intercept(Chain chain) 方法中的參數 Chain 是 RealInterceptorChain 類型,就是 getResponseWithInterceptorChain() 方法中創建的 RealInterceptorChain 這種類型,由於本次只有這一個自定義的攔截器,所以在這裏, chain.request() 獲取的 Request 是 getResponseWithInterceptorChain() 方法中的 originalRequest 對象,也就是一開始最原始的 Request,如果我們在這裏對 request 修改了,把它傳遞給下一個攔截器,則下一個攔截器裏接收的就不是最原始的request數據了,而是上一個傳遞進來的數據。一開始就記錄個時間,這裏用的是開機到現在的時間,然後調用 chain.proceed(request) 開始責任鏈模式的傳遞,這裏是個耗時過程,等獲取到數據後,再記錄個時間,然後打印它的時間差,這就是網絡請求的耗時。

打印網絡請求的消息頭或請求參數等信息或者返回報文等信息,怎麼打印呢?在前面攔截器繼承上再擴展,舉個栗子

public class LoggingInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {
        long t1 = SystemClock.elapsedRealtime();

        Request request = chain.request();
        Headers headers = request.headers();
        Log.e("LoggingInterceptor", "method:  " + request.method() + "    " + "headers:  " + headers.toString());
        RequestBody requestBody = request.body();
        if(requestBody != null){
            MediaType mediaType = requestBody.contentType();
            long length = requestBody.contentLength();
            Log.e("LoggingInterceptor", "mediaType:  " + mediaType.toString() + "      " + "length:  " + length);

            Buffer buffer = new Buffer();
            requestBody.writeTo(buffer);
            String params = buffer.readUtf8();
            Log.e("LoggingInterceptor", "params:  " + params); // 注意,這裏是編碼後的字符串
        }
        
        Response response = chain.proceed(request);
        long t2 = SystemClock.elapsedRealtime();
        Log.e("LoggingInterceptor", "Received response for " + "   " +
                response.request().url() + "   " + (t2 - t1));

        return response;

    }
}

Request 這個類對外暴露了公共方法,我們可以根據它來獲取想要的值,如栗子中那樣,獲取信息頭及消息體,這裏面需要注意一點,如果是 Post 請求的話,一般都會需要添加一些參數,我們這裏使用了 Okio 的庫來讀取 RequestBody 中的參數,需要注意的是,讀出來的 String params = buffer.readUtf8() 是編碼的值,如果我們要看原版的需要使用 URLDecoder.decode(params) 來解碼,爲什麼會這樣呢? 我們一般通過 FormBody 來作爲 Post 請求體,添加參數會通過 HttpUrl.canonicalize() 方法給 key 和 value 進行編碼,如果參數的 key 和 value 都是字母的話沒影響,但如果是漢字則慘了,如果沒有相應的解碼,則我們打印的值自己也看不明白。網上有個開源的日誌攔截器 HttpLoggingInterceptor,可以瞭解一下。

Request 這個類使用了 Builder 模式,它可以很方便擴展字段或者創建新對象,或者把一個對象的屬性值複製給一個新的對象。因此像添加公共參數這種需求,一般可以使用攔截器來實現,我們需要給 Request 添加新的公共參數,舉個栗子

public class CommonParamsInterceptor implements Interceptor {


    public CommonParamsInterceptor() {
    }

    @Override
    public Response intercept(Chain chain) throws IOException {

        Request request = chain.request();
        Request.Builder requestBuilder = request.newBuilder();

        // 公共參數
        Map<String, String> paramsMap = new HashMap<>();
        paramsMap.put("token", "默蒼離");//添加鍵值對
        paramsMap.put("section", "玄狐");//添加鍵值對

        if (request.method().equals("POST") && request.body().contentType().subtype().equals("x-www-form-urlencoded")) {
            FormBody.Builder formBodyBuilder = new FormBody.Builder();
            if (paramsMap.size() > 0) {
                Iterator iterator = paramsMap.entrySet().iterator();
                while (iterator.hasNext()) {
                    Map.Entry entry = (Map.Entry) iterator.next();
                    formBodyBuilder.add((String) entry.getKey(), (String) entry.getValue());
                }
            }
            String postBodyString = bodyToString(request.body());
            RequestBody formBody = formBodyBuilder.build();
            String commonParam = bodyToString(formBody);
            if(!TextUtils.isEmpty(commonParam)){
                postBodyString +=  "&" + commonParam;
            }
            requestBuilder.post(RequestBody.create(MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8"), postBodyString));
        } else {
            injectParamsIntoUrl(request, requestBuilder, paramsMap);
        }
        request = requestBuilder.build();
        return chain.proceed(request);
    }

    /**
     * 把參數拼接在 url 後面。
     */
    private void injectParamsIntoUrl(Request request, Request.Builder requestBuilder, Map<String, String> paramsMap) {
        HttpUrl.Builder httpUrlBuilder = request.url().newBuilder();
        if (paramsMap.size() > 0) {
            Iterator iterator = paramsMap.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry entry = (Map.Entry) iterator.next();
                httpUrlBuilder.addQueryParameter((String) entry.getKey(), (String) entry.getValue());
            }
        }
        requestBuilder.url(httpUrlBuilder.build());

    }

    /**
     *  RequestBody中參數其轉化爲 String
     */
    private static String bodyToString(final RequestBody request) {
        try {
            final Buffer buffer = new Buffer();
            request.writeTo(buffer);
            return URLDecoder.decode(buffer.readUtf8());//name和value默認是會被編碼的,此處需要反編碼
        } catch (IOException e) {
            e.printStackTrace();
            return "";
        }
    }

}


注意攔截器中,paramsMap 是定義的公共參數的集合,可以由外部傳進來,這裏爲了簡便直接寫在方法裏。get 方式的參數是拼接在url後面,post 方式是添加到消息體中,所以這裏要判斷方式進行進行拼接;如果是 POST 並且是表單格式,則創建一個 FormBody 添加參數,然後調用 bodyToString() 方式,這裏是通過 Okio 把RequestBody中參數轉化爲 String 類型,把原 request 中的 RequestBody 參數也轉換爲 String 類型,然後通過  "&" 拼接處完整的參數 String,然後通過 RequestBody.create() 方式創建新的 RequestBody,並把它添加到 Request 中,調用 bodyToString() 方式要注意解碼。 如果是 GET 方式通過 injectParamsIntoUrl() 方法,把通過 HttpUrl 把參數添加進去,builder.addQueryParameter() 是用來添加參數的,最後創建 HttpUrl 後,把它添加到  Request 中,公共參數就這麼添加好了。如果想添加消息頭的方法類似,消息頭就不需要區分請求方式了,都一樣。


最終覺得 OkHttp 和 HttpUrlConnection 是一級的,都是用 socket 實現了網絡連接;HttpUrlConnection 直接使用了 IO 流,OkHttp 用的是 Okio,是對 IO 流的封裝,效率更高。
 

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