okhttp3.2源碼分析(一)

okhttp簡單使用

  1. 創(chuàng)建Request
  2. 創(chuàng)建Call胧华,將Request添加到Call中
  3. 使用異步enqueue驻谆,或者同步的execute方法獲得結(jié)果

okhttp網(wǎng)絡請求過程分析

Call

同步請求

@Override public Response execute() throws IOException {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  try {
    client.dispatcher().executed(this);
    Response result = getResponseWithInterceptorChain(false);
    if (result == null) throw new IOException("Canceled");
    return result;
  } finally {
    client.dispatcher().finished(this);
  }
}

首先加鎖置標志位氢橙,接著使用分配器的executed方法將call加入到同步隊列中潜腻,然后調(diào)用getResponseWithInterceptorChain方法(稍后分析)執(zhí)行http請求,最后調(diào)用finishied方法將call從同步隊列中刪除

異步請求

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

同樣先置標志位食茎,然后將封裝的一個執(zhí)行體放到異步執(zhí)行隊列中丹锹。這里面引入了一個新的類AsyncCall稀颁,這個類繼承于NamedRunnable,實現(xiàn)了Runnable接口楣黍。NamedRunnable可以給當前的線程設置名字匾灶,并且用模板方法將線程的執(zhí)行體放到了execute方法中,所以我們分析AsyncCall只需要看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);
  }
}

通過getResponseWithInterceptorChain方法來執(zhí)行http請求租漂,這個方法是不是很熟悉阶女,在同步請求中也是用的這個方法來執(zhí)行http請求。緊接著判斷call是否被cancel來執(zhí)行不同的回調(diào)窜锯,最后使用finished方法將call從異步執(zhí)行隊列中移除张肾。這里有個需要注意的地方芭析,onResponse回調(diào)被執(zhí)行的條件是本次http請求是完整的锚扎,也就是說即使服務器返回的是錯誤信息,依然會走onResponse回調(diào)馁启,我們在應用層使用的時候驾孔,可以自己再封裝一次芍秆。

OK,以上就是okhttp可以同時支持同步和異步請求的分析過程翠勉,而在getResponseWithInterceptorChain方法中我們將會分析okhttp的另一個重要模塊:攔截器

攔截器

這是我最喜歡okhttp的地方妖啥,你可以攔截當前正在發(fā)出的請求。我們可以使用攔截器做很多事情对碌,例如:添加log方便調(diào)試荆虱,在服務器還沒有ready的情況下模擬一個網(wǎng)絡應答等。在getResponseWithInterceptorChain方法中處理了攔截器的相關邏輯

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

這里ApplicationInterceptorChain實現(xiàn)了Interceptor.Chain接口朽们,然后在preceed方法中處理相應的邏輯怀读,preceed代碼如下

@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);
}

在index在getResponseWithInterceptorChain方法中被初始化為0,當我們添加了攔截器之后index < client.interceptors().size()就會走到true的代碼段骑脱,之后會從client.interceptors()中拿出一個攔截器菜枷,執(zhí)行我們的攔截回調(diào)。這里也可以看到在攔截回調(diào)中是必須要有個Response返回的叁丧,否則會出現(xiàn)異常啤誊。如果沒有自定義攔截器的話,將會調(diào)用getResponse方法執(zhí)行真正的網(wǎng)絡請求邏輯(相對于攔截器模塊來說是執(zhí)行了真正的網(wǎng)絡請求拥娄,其實后面還有緩存模塊)

有意思的是我們可以定義多個攔截器蚊锹,這就對應了ApplicationInterceptorChain類的名稱應用攔截鏈。只要我們在自定義的攔截器回調(diào)方法中調(diào)用chan.proceed稚瘾,攔截器就會鏈式的調(diào)用下去枫耳。如果我們不希望okhttp執(zhí)行真正的網(wǎng)絡請求,只需要在攔截器中虛擬一個response即可孟抗。需要注意的是迁杨,如果某個攔截器內(nèi)部沒有調(diào)用chan.proceed方法,那么在它之后添加的攔截器都不會再被執(zhí)行

getResponse方法將會把網(wǎng)絡請求交給Engine處理

Response getResponse(Request request, boolean forWebSocket) throws IOException {
    // Copy body metadata to the appropriate request headers.
    RequestBody body = request.body();
    if (body != null) {
      Request.Builder requestBuilder = request.newBuilder();

      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");
      }

      request = requestBuilder.build();
    }

    // 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.
        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();
        }
      }

      Response response = engine.getResponse();
      Request followUp = engine.followUpRequest();

      if (followUp == null) {
        if (!forWebSocket) {
          engine.releaseStreamAllocation();
        }
        return response;
      }

      StreamAllocation streamAllocation = engine.close();

      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      if (!engine.sameConnection(followUp.url())) {
        streamAllocation.release();
        streamAllocation = null;
      }

      request = followUp;
      engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null,
          response);
    }
  }
}

getResponse會先根據(jù)request body的contentType來設置相應的header凄硼,Content-Length相信都比較熟悉铅协,在http header中Transfer-Encoding: chunked表示的是內(nèi)容長度不定,這里比較奇怪的是header的屬性分別在不同的設置摊沉,不清楚為何不放在一起設置狐史。接著會創(chuàng)建一個HttpEngine對象,設置追加發(fā)送的請求次數(shù)说墨,在HttpEngine中處理網(wǎng)絡請求代碼如下

engine.sendRequest();
engine.readResponse();
Response response = engine.getResponse();

緊接著是處理各種異常和發(fā)送追加請求骏全,獲取發(fā)送追加請求是在HttpEnginefollowUpRequest方法中處理,在三種情況下okhttp會發(fā)送追加請求尼斧,通過MAX_FOLLOW_UPS = 20控制最大追加請求

  1. 未授權(quán)(401):調(diào)用okhttpclient授權(quán)方法重新授權(quán)
  2. 重定向(3xx)
  3. 請求超時(408):重復發(fā)送原請求

未完待續(xù)...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末姜贡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子棺棵,更是在濱河造成了極大的恐慌楼咳,老刑警劉巖熄捍,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異母怜,居然都是意外死亡余耽,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門苹熏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碟贾,“玉大人,你說我怎么就攤上這事轨域÷粕拢” “怎么了?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵疙挺,是天一觀的道長扛邑。 經(jīng)常有香客問我,道長铐然,這世上最難降的妖魔是什么蔬崩? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮搀暑,結(jié)果婚禮上沥阳,老公的妹妹穿的比我還像新娘。我一直安慰自己自点,他們只是感情好桐罕,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著桂敛,像睡著了一般功炮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上术唬,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天薪伏,我揣著相機與錄音,去河邊找鬼粗仓。 笑死嫁怀,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的借浊。 我是一名探鬼主播塘淑,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蚂斤!你這毒婦竟也來了存捺?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤橡淆,失蹤者是張志新(化名)和其女友劉穎召噩,沒想到半個月后母赵,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逸爵,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡具滴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了师倔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片构韵。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖趋艘,靈堂內(nèi)的尸體忽然破棺而出疲恢,到底是詐尸還是另有隱情,我是刑警寧澤瓷胧,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布显拳,位于F島的核電站,受9級特大地震影響搓萧,放射性物質(zhì)發(fā)生泄漏杂数。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一瘸洛、第九天 我趴在偏房一處隱蔽的房頂上張望揍移。 院中可真熱鬧,春花似錦反肋、人聲如沸那伐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽罕邀。三九已至,卻和暖如春养距,著一層夾襖步出監(jiān)牢的瞬間燃少,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工铃在, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留阵具,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓定铜,卻偏偏與公主長得像阳液,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子揣炕,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容