Android網絡編程(七)源碼解析OkHttp前篇[請求網絡]

前言

學會了OkHttp3的用法后,我們當然有必要來了解下OkHttp3的源碼,當然現在網上的文章很多,我仍舊希望我這一系列文章篇是最簡潔易懂的。

1.從請求處理開始分析

首先OKHttp3如何使用這里就不在贅述了,不明白的同學可以查看Android網絡編程(五)OkHttp2.x用法全解析
Android網絡編程(六)OkHttp3用法全解析這兩篇文章。當我們要請求網絡的時候我們需要用OkHttpClient.newCall(request)進行execute或者enqueue操作,當我們調用newCall時:

  @Override public Call newCall(Request request) {
    return new RealCall(this, request);
  }

實際返回的是一個RealCall類,我們調用enqueue異步請求網絡實際上是調用了RealCall的enqueue方法:

  void enqueue(Callback responseCallback, boolean forWebSocket) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    client.dispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket));
  }

可以看到最終的請求是dispatcher來完成的。

2.Dispatcher任務調度

主要的變量

Dispatcher主要用于控制并發的請求,它主要維護了以下變量:

  /** 最大并發請求數*/
  private int maxRequests = 64;
  /** 每個主機最大請求數*/
  private int maxRequestsPerHost = 5;
  /** 消費者線程池 */
  private ExecutorService executorService;
  /** 將要運行的異步請求隊列 */
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
  /**正在運行的異步請求隊列 */
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
  /** 正在運行的同步請求隊列 */
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

構造函數

 public Dispatcher(ExecutorService executorService) {
    this.executorService = executorService;
  }

  public Dispatcher() {
  }

  public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }

Dispatcher有兩個構造函數,可以使用自己設定線程池,如果沒有設定線程池則會在請求網絡前自己創建線程池,這個線程池類似于CachedThreadPool比較適合執行大量的耗時比較少的任務。不了解線程池的同學可以查看Android多線程(一)線程池這篇文章。其中用到了SynchronousQueue,不了解它的同學可以查看Java并發編程(六)阻塞隊列這篇文章。

異步請求

  synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }

當正在運行的異步請求隊列中的數量小于64并且正在運行的請求主機數小于5時則把請求加載到runningAsyncCalls中并在線程池中執行,否則就再入到readyAsyncCalls中進行緩存等待。

AsyncCall

線程池中傳進來的參數就是AsyncCall它是RealCall的內部類,內部也實現了execute方法:

    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain(forWebSocket);
        if (canceled) {
          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!
          logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }

首先我們來看看最后一行, 無論這個請求的結果如何都會執行client.dispatcher().finished(this);

 synchronized void finished(AsyncCall call) {
    if (!runningAsyncCalls.remove(call)) throw new AssertionError("AsyncCall wasn't running!");
    promoteCalls();
  }

finished方法將此次請求從runningAsyncCalls移除后還執行了promoteCalls方法:

  private void promoteCalls() {
    if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
    if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.

    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall call = i.next();

      if (runningCallsForHost(call) < maxRequestsPerHost) {
        i.remove();
        runningAsyncCalls.add(call);
        executorService().execute(call);
      }

      if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    }
  }

可以看到最關鍵的點就是會從readyAsyncCalls取出下一個請求,并加入runningAsyncCalls中并交由線程池處理。好了讓我們再回到上面的AsyncCall的execute方法,我們會發getResponseWithInterceptorChain方法返回了Response,很明顯這是在請求網絡。

3.Interceptor攔截器

  private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException {
    Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket);
    return chain.proceed(originalRequest);
  }

getResponseWithInterceptorChain方法,創建了ApplicationInterceptorChain,它是一個攔截器鏈,這個類也是RealCall的內部類,接下來執行了它的proceed方法:

    @Override public Response proceed(Request request) throws IOException {
      // If there's another interceptor in the chain, call that.
      if (index < client.interceptors().size()) {
        Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
        //從攔截器列表取出攔截器
        Interceptor interceptor = client.interceptors().get(index);
        Response interceptedResponse = interceptor.intercept(chain);

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

        return interceptedResponse;
      }

      // No more interceptors. Do HTTP.
      return getResponse(request, forWebSocket);
    }

proceed方法每次從攔截器列表中取出攔截器,當存在多個攔截器時都會在第七行阻塞,并等待下一個攔截器的調用返回。下面分別以 攔截器鏈中有1個、2個攔截器的場景加以模擬:


攔截器主要用來觀察,修改以及可能短路的請求輸出和響應的回來。通常情況下攔截器用來添加,移除或者轉換請求或者響應的頭部信息。比如將域名替換為ip地址,將請求頭中添加host屬性,也可以添加我們應用中的一些公共參數,比如設備id、版本號等等。 不了解攔截器的可以查看Okhttp-wiki 之 Interceptors 攔截器這篇文章。
回到代碼上來,我們看最后一行 return getResponse(request, forWebSocket),如果沒有更多的攔截器的話,就會執行網絡請求,來看看getResponse方法做了些什么(RealCall.java):

Response getResponse(Request request, boolean forWebSocket) throws IOException {
 ...省略
    // Create the initial HTTP engine. Retries and redirects need new engine for each attempt.
    engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null);

    int followUpCount = 0;
    while (true) {
      if (canceled) {
        engine.releaseStreamAllocation();
        throw new IOException("Canceled");
      }

      boolean releaseConnection = true;
      try {
        engine.sendRequest();
        engine.readResponse();
        releaseConnection = false;
      } catch (RequestException e) {
        // The attempt to interpret the request failed. Give up.
        throw e.getCause();
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
  ...省略     
    }
  }

getResponse方法比較長我省略了一些代碼,可以看到創建了HttpEngine類并且調用HttpEngine的sendRequest方法和readResponse方法。

4.緩存策略

我們先來看看sendRequest方法:

 public void sendRequest() throws RequestException, RouteException, IOException {
    if (cacheStrategy != null) return; // Already sent.
    if (httpStream != null) throw new IllegalStateException();
    //請求頭部添加
    Request request = networkRequest(userRequest);
    //獲取client中的Cache,同時Cache在初始化的時候會去讀取緩存目錄中關于曾經請求過的所有信息。
    InternalCache responseCache = Internal.instance.internalCache(client);
    //cacheCandidate為上次與服務器交互緩存的Response
    Response cacheCandidate = responseCache != null
        ? responseCache.get(request)
        : null;

    long now = System.currentTimeMillis();
   
    //創建CacheStrategy.Factory對象,進行緩存配置
    cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
    //網絡請求
    networkRequest = cacheStrategy.networkRequest;
    //緩存的響應
    cacheResponse = cacheStrategy.cacheResponse;

    if (responseCache != null) {
     //記錄當前請求是網絡發起還是緩存發起
      responseCache.trackResponse(cacheStrategy);
    }

    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    //不進行網絡請求并且緩存不存在或者過期則返回504錯誤
    if (networkRequest == null && cacheResponse == null) {
      userResponse = new Response.Builder()
          .request(userRequest)
          .priorResponse(stripBody(priorResponse))
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(EMPTY_BODY)
          .build();
      return;
    }
    
    // 不進行網絡請求,而且緩存可以使用,直接返回緩存
    if (networkRequest == null) {
      userResponse = cacheResponse.newBuilder()
          .request(userRequest)
          .priorResponse(stripBody(priorResponse))
          .cacheResponse(stripBody(cacheResponse))
          .build();
      userResponse = unzip(userResponse);
      return;
    }

    //需要訪問網絡時
    boolean success = false;
    try {
      httpStream = connect();
      httpStream.setHttpEngine(this);

      if (writeRequestHeadersEagerly()) {
        long contentLength = OkHeaders.contentLength(request);
        if (bufferRequestBody) {
          if (contentLength > Integer.MAX_VALUE) {
            throw new IllegalStateException("Use setFixedLengthStreamingMode() or "
                + "setChunkedStreamingMode() for requests larger than 2 GiB.");
          }

          if (contentLength != -1) {
            // Buffer a request body of a known length.
            httpStream.writeRequestHeaders(networkRequest);
            requestBodyOut = new RetryableSink((int) contentLength);
          } else {
            // Buffer a request body of an unknown length. Don't write request headers until the
            // entire body is ready; otherwise we can't set the Content-Length header correctly.
            requestBodyOut = new RetryableSink();
          }
        } else {
          httpStream.writeRequestHeaders(networkRequest);
          requestBodyOut = httpStream.createRequestBody(networkRequest, contentLength);
        }
      }
      success = true;
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (!success && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }
  }

上面的代碼顯然是在發送請求,但是最主要的是做了緩存的策略。cacheCandidate是上次與服務器交互緩存的Response,這里的緩存都是基于Map,key是請求中url的md5,value是在文件中查詢到的緩存,頁面置換基于LRU算法,我們現在只需要知道它是一個可以讀取緩存Header的Response即可。根據cacheStrategy的處理得到了networkRequest和cacheResponse這兩個值,根據這兩個值的數據是否為null來進行進一步的處理,當networkRequest和cacheResponse都為null的情況也就是不進行網絡請求并且緩存不存在或者過期,這時候則返回504錯誤;當networkRequest 為null時也就是不進行網絡請求,而且緩存可以使用時則直接返回緩存;其他的情況則請求網絡。
接下來我們查看readResponse方法:

  public void readResponse() throws IOException {
    ...省略
    else{
      //讀取網絡響應
      networkResponse = readNetworkResponse();
    }
    //將響應頭部存入Cookie中
    receiveHeaders(networkResponse.headers());

    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
    //檢查緩存是否可用,如果可用。那么就用當前緩存的Response,關閉網絡連接,釋放連接。
      if (validate(cacheResponse, networkResponse)) {
        userResponse = cacheResponse.newBuilder()
            .request(userRequest)
            .priorResponse(stripBody(priorResponse))
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();
        releaseStreamAllocation();

        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        InternalCache responseCache = Internal.instance.internalCache(client);
        responseCache.trackConditionalCacheHit();
        // 更新緩存
        responseCache.update(cacheResponse, stripBody(userResponse));
        userResponse = unzip(userResponse);
        return;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }

    userResponse = networkResponse.newBuilder()
        .request(userRequest)
        .priorResponse(stripBody(priorResponse))
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (hasBody(userResponse)) {
      maybeCache();
      userResponse = unzip(cacheWritingResponse(storeRequest, userResponse));
    }
  }

這個方法發起刷新請求頭部和請求體,解析HTTP響應頭部。如果有緩存并且可用則用緩存的數據并更新緩存,否則就用網絡請求返回的數據。
我們再來看看validate(cacheResponse, networkResponse)方法是如何判斷緩存是否可用的:

  private static boolean validate(Response cached, Response network) {
  //如果服務器返回304則緩存有效
    if (network.code() == HTTP_NOT_MODIFIED) {
      return true;
    }
   //通過緩存和網絡請求響應中的Last-Modified來計算是否是最新數據,如果是則緩存有效
    Date lastModified = cached.headers().getDate("Last-Modified");
    if (lastModified != null) {
      Date networkLastModified = network.headers().getDate("Last-Modified");
      if (networkLastModified != null
          && networkLastModified.getTime() < lastModified.getTime()) {
        return true;
      }
    }
    return false;
  }

如緩存果過期或者強制放棄緩存,在此情況下,緩存策略全部交給服務器判斷,客戶端只用發送條件get請求即可,如果緩存是有效的,則返回304 Not Modifiled,否則直接返回body。條件get請求有兩種方式一種是Last-Modified-Date,一種是 ETag。這里采用了Last-Modified-Date,通過緩存和網絡請求響應中的Last-Modified來計算是否是最新數據,如果是則緩存有效。

5.失敗重連

最后我們再回到RealCall的getResponse方法:

  Response getResponse(Request request, boolean forWebSocket) throws IOException {
  ...省略
      boolean releaseConnection = true;
      try {
        engine.sendRequest();
        engine.readResponse();
        releaseConnection = false;
      } catch (RequestException e) {
        // The attempt to interpret the request failed. Give up.
        throw e.getCause();
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        HttpEngine retryEngine = engine.recover(e.getLastConnectException(), null);
        if (retryEngine != null) {
          releaseConnection = false;
          engine = retryEngine;
          continue;
        }
        // Give up; recovery is not possible.
        throw e.getLastConnectException();
      } catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        HttpEngine retryEngine = engine.recover(e, null);
        if (retryEngine != null) {
          releaseConnection = false;
          engine = retryEngine;
          continue;
        }

        // Give up; recovery is not possible.
        throw e;
      } finally {
        // We're throwing an unchecked exception. Release any resources.
        if (releaseConnection) {
          StreamAllocation streamAllocation = engine.close();
          streamAllocation.release();
        }
      }
     ...省略
      engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null,
          response);
    }
  }

查看代碼第11行和21行當發生IOException或者RouteException時會執行HttpEngine的recover方法:

  public HttpEngine recover(IOException e, Sink requestBodyOut) {
    if (!streamAllocation.recover(e, requestBodyOut)) {
      return null;
    }

    if (!client.retryOnConnectionFailure()) {
      return null;
    }

    StreamAllocation streamAllocation = close();

    // For failure recovery, use the same route selector with a new connection.
    return new HttpEngine(client, userRequest, bufferRequestBody, callerWritesRequestBody,
        forWebSocket, streamAllocation, (RetryableSink) requestBodyOut, priorResponse);
  }

最后一行可以看到就是重新創建了HttpEngine并返回,用來完成重連。
到這里OkHttp請求網絡的流程基本上講完了,下面是關于OKHttp的請求流程圖:


參考資料:
http://www.lxweimin.com/p/aad5aacd79bf
http://www.lxweimin.com/p/64e256c1dbbf
http://www.cnblogs.com/LuLei1990/p/5534791.html
http://frodoking.github.io/2015/03/12/android-okhttp/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容