常用輪子之Okhttp基本使用及原理

jasonhww發表於2018-12-02

不忘初心 砥礪前行, Tomorrow Is Another Day !

相關文章

本文概要:

  1. 基本使用
  2. 基本原理

一. 基本使用

okhttp的請求和響應大多數採用建造者模式設計.

1. GET同步請求

public void syncGetRequest() {
        String url = "http://api.k780.com/?app=weather.future&weaid=1&&appkey=10003" +
                "&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json";
        final OkHttpClient okHttpClient = new OkHttpClient();
        final Request request = new Request.Builder()
                .get()
                .url(url)
                .build();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Response response = okHttpClient.newCall(request).execute();
                    Log.d(TAG, "syncGetRequest: " + response.body().string());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).star
複製程式碼

2. GET非同步請求

public void asyncGetRequest() {
        String url = "http://api.k780.com/?app=weather.future&weaid=1&&appkey=10003" +
                "&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json";
        final OkHttpClient okHttpClient = new OkHttpClient();
        Request request = new Request.Builder()
                .get()
                .url(url)
                .build();
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.d(TAG, "Callback-Thread: " + Thread.currentThread());
                String result = response.body().string();
                Log.d(TAG, "asyncGetRequest: " + result);
                //通過handler或者runOnUiThread方式切換執行緒.
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        idtv.setText("jasonhww");
                    }
                });
                /*
                 *如果獲取的是檔案/圖片
                 */
                //方式一:通過獲取流
                //InputStream inputStream = response.body().byteStream();
                //方式二:通過獲取位元組陣列
                //byte[] bytes = response.body().bytes();
            }
        });
    }
複製程式碼

注意事項:

  • Callback的回撥方法是在子執行緒執行的,如需更新UI,可通過runjOnUiThread或者handler切換到主執行緒.

3. POST提交表單

可以使用FormBody.Builder構建一個表單請求體

  
    public void asyncPostForm(){
        String url = "http://api.k780.com/";
        final OkHttpClient okHttpClient = new OkHttpClient();
        //構建表單請求體
        RequestBody requestBody = new FormBody.Builder()
                .add("app","weather.future")
                .add("weaid","1")
                .add("appkey","10003")
                .add("sign","b59bc3ef6191eb9f747dd4e83c99f2a4")
                .add("format","json")
                .build();
        //建立POST請求
        Request request = new Request.Builder()
                .url(url)
                .post(requestBody)
                .build();
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.d(TAG, "asyncPostForm: "+response.body().string());
            }
        });
    }
複製程式碼

4. POST提交JSON字串

指定請求體的媒體型別為"application/json"


    public void asyncJson() {
        String url = "http://api.k780.com/?app=weather.future&weaid=1&&appkey=10003" +
                "&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json";
        String json = "{code:1,result:null}";
        OkHttpClient okHttpClient = new OkHttpClient();
        //指定媒體型別
        MediaType mediaType = MediaType.parse("application/json,charset=utf-8");
        RequestBody requestBody = RequestBody.create(mediaType, json);
        Request request = new Request.Builder()
                .url(url)
                .post(requestBody)
                .build();
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.d(TAG, "asyncJson: " + response.body().string());
            }
        });

    }

複製程式碼

5. 快取

快取使用相對比較簡單,只需指定一下快取目錄及大小即可.

 //虛擬碼
 Cache cache = new Cache(cacheDirectory,cacheSize);
 OkHttpClient okHttpClient = new OkHttpClient.Builder()
                      .cache(cache)
                      .build();
 
 
複製程式碼

6. 攔截器

通過攔截器我們可以很方便的去修改請求和響應的相關資訊,如修改請求頭,請求體等.

  • 應用攔截器
    攔截應用層與okhttp之間的請求和響應
  • 網路攔截器
    攔截okhttp與網路層之間的請求和響應,此時網路連線已經建立
public class LoggingInterceptor implements Interceptor {
    private static final String TAG = "LoggingInterceptor";

    @Override
    public Response intercept(Chain chain) throws IOException {
        //獲取請求
        Request request = chain.request();
        //列印url,連線狀態,請求頭
        Log.d(TAG, "LoggingInterceptor: url = " + request.url() +
                "\nconnectionStatus = " + chain.connection() +
                "\nrequestHeaderInfo = " + request.headers());
        //執行請求
        Response response = chain.proceed(request);
         //列印url,響應頭
        Log.d(TAG, "LoggingInterceptor: url = " + response.request() +
                "\nresponseHeaderInfo = " + response.headers());
        return response;
    }
}


public class NetInterceptor implements Interceptor {
    private static final String TAG = "LoggingInterceptor";

    @Override
    public Response intercept(Chain chain) throws IOException {
    
        Request request = chain.request();
        Response response = chain.proceed(request);
        //演示設定響應頭
        CacheControl.Builder builder = new CacheControl.Builder()  
        .maxAge(10,TimeUnit.MINUTES);
        return response.newBuilder()
                        .header("Cache-Control",builder.build().toString)
                        .build();
    }
}



複製程式碼

    public void asyncCacheInterceptor() {
        String url = "http://api.k780.com/?app=weather.future&weaid=1&&appkey=10003" +
                "&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json";
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .addInterceptor(new LoggingInterceptor())//應用攔截
                .addNetworkInterceptor(new NetInterceptor())//網路攔截器
                .build();

        //通過攔截器修改請求頭
        Request request = new Request.Builder()
                .get()
                .header("user-Agent","Interceptor example")
                .url(url)
                .build();

        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.d(TAG, "asyncCacheInterceptor: " + response.body().string());
            }
        });
 
    }
    
複製程式碼

二. 基本原理

採用OkHttp原始碼版本為3.8.1

1. OkHttpClient的建立

OkHttpClient作為okhttp的入口,一般將OkHttpClient採用單例模式.
初始化兩種方式

  1. 採用直接"new"的方式
  2. 採用建造者模式

對應原始碼

//內部也是通過建造者進行初始化
OkHttpClient okhttpClient = new OkHttpClient();

OkHttpClient okhttpClient = new OkHttpClient.Builder()
                            .build();
複製程式碼

通過初始化okhttpclient,完成對許多成員變數的初始化工作.

對應原始碼

 OkHttpClient(Builder builder) {
    //分發器物件,記錄請求執行情況,內部維護一個執行緒池來執行非同步請求
    this.dispatcher = builder.dispatcher;
    this.proxy = builder.proxy;
    this.protocols = builder.protocols;
    this.connectionSpecs = builder.connectionSpecs;
    //應用攔截器集合
    this.interceptors = Util.immutableList(builder.interceptors);
    //網路攔截器集合
    this.networkInterceptors = Util.immutableList(builder.networkInterceptors);
    this.eventListenerFactory = builder.eventListenerFactory;
    this.proxySelector = builder.proxySelector;
    //Cookie瓶
    this.cookieJar = builder.cookieJar;
    //磁碟快取
    this.cache = builder.cache;
    this.internalCache = builder.internalCache;
    this.socketFactory = builder.socketFactory;
   
   //HTTPS相關成員變數初始化
    ......
    
    }
複製程式碼

2. Call的建立

當okhttpclient初始化後,再通過okHttpClient.newCall(request),建立一個Call物件,最終實際返回了一個RealCall物件.

對應原始碼

@Override public Call newCall(Request request) {
    
    return new RealCall(this, request, false /* for web socket */);
  }
複製程式碼
2.1 同步請求時

呼叫RealCall的execute方法

對應原始碼

  /**
   * 1. 首先看RealCall的execute方法.
   */
@Override public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    try {
      //實際呼叫了分發器的executed方法,傳入RealCall物件.
      client.dispatcher().executed(this);
      //最終通過攔截器鏈獲取響應.
      //最終通過攔截器鏈獲取響應.
      //最終通過攔截器鏈獲取響應.
      //重要的話說三遍.
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } finally {
      client.dispatcher().finished(this);
    }
  }
  
/**
 * 2. 接著看dispatcher的executed方法
 */
synchronized void executed(RealCall call) {
    //標識已經執行了請求
    runningSyncCalls.add(call);
  }
複製程式碼
2.1 非同步請求時

同樣呼叫RealCall的enqueue方法

對應原始碼

  /**
   * 1.首先看RealCall的enqueue方法.
   */
@Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    //實際呼叫了分發器的enqueue方法,傳入AsyncCall物件
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }
  
  /**
   * 2.接著看dispatcher物件的enqueue方法.
   */
  synchronized void enqueue(AsyncCall call) {
    //檢測請求是否超過最大請求數和一個host對應最大的請求數
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      //標識已經執行了請求(這裡和同步方法一樣操作,可以對比上一點方法)
      runningAsyncCalls.add(call);
      //使用執行緒池執行請求
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }
  
   /**
    * 3.最後我們看AsyncCall的實現.
    */
   //AsyncCall是RealCall的內部類,繼承自NamedRunnable
   //NamedRunnable的實現比較簡單就是修改執行緒名.提供一個execute方法在run用呼叫,這裡就不再曬出具體原始碼了.
   //AsyncCall的實現主是獲取響應,進行回撥.
   
   final class AsyncCall extends NamedRunnable {
    private final Callback responseCallback;

    //....省略部分程式碼
    
    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        //最終通過攔截器鏈獲取響應.
        //最終通過攔截器鏈獲取響應.
        //最終通過攔截器鏈獲取響應.
        //重要的話說三遍.
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
           //回撥失敗
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
           //回撥成功
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
         //回撥失敗
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
複製程式碼

最後將Realcall對同步與非同步請求處理流程做個小結:

  • 同步時.

    1. 呼叫RealCall的execute,實際呼叫了分發器的executed方法,傳入RealCall物件.
      • 分發器的executed方法,僅僅標識已經執行了請求.
    2. 最終通過攔截器鏈獲取響應.
  • 非同步時.

    • 呼叫RealCall的enqueue,實際呼叫了分發器的enqueue方法,傳入AsyncCall物件.
      • 分發器的executed方法,先檢查請求,滿足則標識已經執行了請求,最終使用執行緒池執行請求.不滿足則進人等待佇列
    • AsyncCall的實現
      • AsyncCall是一個NamedRunnable子類
      • 通過攔截器鏈獲取響應,回撥成功與失敗.

3.攔截器鏈的構建與啟動

整個okhttp的核心設計之處

  1. 遵循單一原則,每個攔截器只做一項處理.
  2. 攔截器之間通過攔截器鏈(RealInterceptorChain)進行連線,構建出處理請求和響應的流水鏈.

請求通過層層攔截器處理,最後傳送出去,反之響應.

對應原始碼

 //通過攔截器鏈獲取響應
 Response getResponseWithInterceptorChain() throws IOException {
 
    /**
     * 初始化攔截器集合
     */
    List<Interceptor> interceptors = new ArrayList<>();
    //新增7種型別攔截器
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));


    /**
     * 建立一個攔截器鏈
     /
    Interceptor.Chain chain = new 
    //第五個參數列示指向的攔截器位置,這裡指向了第一個.
    RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    //啟動攔截器鏈
    return chain.proceed(originalRequest);
  }
  
  /**
   * 攔截器鏈的啟動
   */
    public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
      
      //......省略部分程式碼
      
      
    // 建立一個新的攔截器鏈,攔截器位置指向將index+1.即下一個.
    
    RealInterceptorChain next = new RealInterceptorChain(
        interceptors, streamAllocation, httpCodec, connection, index + 1, request);
    
    //通過下標index獲取當前要執行的攔截器    
    Interceptor interceptor = interceptors.get(index);
    
    //執行攔截器intercept方法
    //如果當前攔截器執行完後,則呼叫傳入的next(新鏈物件)的proceed方法,執行下一個攔截器.依次類推.
    Response response = interceptor.intercept(next);

    
    if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }

   
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    return response;
    }
複製程式碼

這7種攔截器執行順序為:

  1. 應用攔截器
  2. retryAndFollowUpInterceptor
  3. BridgeInterceptor
  4. CacheInterceptor
  5. ConnectInterceptor
  6. 網路攔截器
  7. CallServerInterceptor

其中帶下劃線的攔截器,就是我們之前自定義的攔截器.接下來就依次講解7種攔截器的作用.

3.1 應用攔截器

不再講述

3.2 retryAndFollowUpInterceptor

處理錯誤重試和重定向

對應原始碼

@Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    
    while (true) {
      if (canceled) {
        streamAllocation.release();
        throw new IOException("Canceled");
      }

      Response response = null;
      boolean releaseConnection = true;
      try {
        //執行下一個攔截器,獲取響應
        response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.getLastConnectException(), false, request)) {
          throw e.getLastConnectException();
        }
        releaseConnection = false;
        //滿足條件,則重試
        continue;
      } catch (IOException e) {
      // An attempt to communicate with a server failed. The request may have been sent.
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, requestSendStarted, request)) throw e;
        releaseConnection = false;
        //滿足條件,則重試
        continue;
      } finally {
        //出現異常釋放資源,continue在finally語句塊執行後才執行
        // We're throwing an unchecked exception. Release any resources.

        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }
      
     //檢測是否滿足定向的要求程式碼
     ......
     
      //重新賦值為重定向的請求
      request = followUp;
      priorResponse = response;
    }
  }
複製程式碼
3.3 BridgeInterceptor

橋接應用層和網路層

A. 將請求進行深加工,使其成為真正請求

  • 對一些請求頭的設定

B. 並且也對網路響應做響應處理.

  • 解壓縮,去掉不必要的響應頭

對應原始碼

@Override public Response intercept(Chain chain) throws IOException {
    //獲取請求
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();
    //對一些請求頭的設定.
    RequestBody body = userRequest.body();
    if (body != null) {
      MediaType contentType = body.contentType();
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      if (contentLength != -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }

    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      //如果沒有配置,預設配置gzip.獲取響應時需要解壓縮.
      requestBuilder.header("Accept-Encoding", "gzip");
    }

    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }

    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }
    
    //執行下一個攔截器獲取響應
    Response networkResponse = chain.proceed(requestBuilder.build());

    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);

    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      //用於解壓縮
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      //返回應用層去掉不需要的響應頭
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
    }

    return responseBuilder.build();
  }
複製程式碼
3.4 CacheInterceptor

承擔快取的查詢和儲存職責

A. 不需要網路請求,快取可用,直接返回

B. 快取不可用,執行下一個攔截器獲取響應;
如果使用者配置了需要快取,將響應寫入快取,並返回.

對應原始碼

 @Override
  public Response intercept(Chain chain) throws IOException {
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    //檢查快取策略
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    //networkRequest不為空,需要傳送請求
    Request networkRequest = strategy.networkRequest;
    //cacheResponse不為空,則快取可用
    Response cacheResponse = strategy.cacheResponse;

   //......省略部分程式碼

    // If we don't need the network, we're done.
    //不需要網路請求,快取可用,直接返回
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    Response networkResponse = null;
    try {
      //快取不可用,執行下一個攔截器獲取響應
      networkResponse = chain.proceed(networkRequest);
    } finally {
      //......省略部分程式碼
    }
    //......省略部分程式碼
    
    //包裝網路響應
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();
    
    //使用者配置了快取
    if (cache != null) {
      //判斷條件
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        //網路響應寫入快取,並返回
        return cacheWritingResponse(cacheRequest, response);
      }

      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }
複製程式碼
3.5 ConnectInterceptor

給請求提供一個連線

  • TCP的連線,HTTPS的連線全部在此完成.

對應原始碼

 @Override 
 public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //獲取HTTP編碼解碼器
    HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
    //獲取一個連結
    RealConnection connection = streamAllocation.connection();
    //執行下一個攔截器
    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }
複製程式碼
3.6 網路攔截器

不再講述

3.7 CallServerInterceptor

將請求傳送出去

對應原始碼

 @Override 
  public Response intercept(Chain chain) throws IOException {
   
    //.......省略部分程式碼
  
    //傳送網路請求,httpCodec則是在上一個ConnectInterceptor攔截器獲取到的
    httpCodec.finishRequest();

    if (responseBuilder == null) {
      responseBuilder = httpCodec.readResponseHeaders(false);
    }
    
    //構建網路響應物件
    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    int code = response.code();
    if (forWebSocket && code == 101) {
      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      //寫入響應體
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }

    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      streamAllocation.noNewStreams();
    }

    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }

    return response;
  }
複製程式碼

由於本人技術有限,如有錯誤的地方,麻煩大家給我提出來,本人不勝感激,大家一起學習進步.

相關文章