1. 程式人生 > >Okhttp之同步和非同步請求簡單分析

Okhttp之同步和非同步請求簡單分析

在讀這篇部落格之前,如果想了解okhttp更多原理,可移步博主的okhttp分類部落格。用過okhttp的應該都瞭解,Okhttp是支援同步和非同步請求的,本篇就就對其原理做一個簡單的梳理。算是加深okhttp的理解。
同步請求使用方式如下:

Response response = okhttpClient.newCall(request).execute();

非同步請求使用方式如下:

Response response = okhttpClient.newCall(request).enqueue(callback);

可以發現在請求之前會通過OkhttpClient物件的newCall方法將Request物件轉換成一個RealCall物件:

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

同步execute方法簡單說明

先看看同步請求執行了什麼,RealCall的execute方法如下:

public Response execute() throws IOException {
      //省略了部分程式碼
      client.dispatcher().executed(this);
      Response result = getResponseWithInterceptorChain();
      client.dispatcher().finished(this
); }

1、把RealCall交給Okhttp的Dispathcer,放到一個佇列裡:

final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
void executed(RealCall call) {
    runningSyncCalls.add(call);
  }

2、呼叫getResponseWithInterceptorChain發起完整的網路請求流程。 對於此方法再此不會做說明,詳細可參考博主的此部落格
3、執行完畢後,從佇列裡刪除該物件(這個在非同步請求中再做說明)

非同步請求的簡單說明

其實非同步請求當然也很簡單,無非就是將getResponseWithInterceptorChain 放到執行緒中去執行而已,這其實是很有道理的廢話。因為執行緒建立是比較好資源的,所以有了執行緒池,okhttp也不例外。執行緒池執行的單元為runanble,所以Okhttp也將非同步請求封裝了一個Ruannble,這個Ruannalbe就是AsyncCall.需要注意的是這個AsyncCall可不是RealCall的子類,也不是Call介面的實現類。囉嗦了這麼多所以讓我們看看okhttp是怎麼實現非同步請求的吧:

public void enqueue(Callback responseCallback) {
    //省略部分程式碼
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

所以在看下Dispatcher的enqueue方法:

//正在執行的非同步請求
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
  //等待非同步執行的執行緒
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
  synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }

上面的邏輯很清晰,當請求過多的時候放入放入等待佇列中,否則放在正在執行的runningAsyncCalls佇列中並用executorService來執行這個AsyncCall. 執行AsyncCall主要是讓執行緒池的某個執行緒執行其run方法,而AsyncCall是一個NamedRunnable,所以分析AsyncCall的execute即可:

protected void execute() {
      try {
        Response response = getResponseWithInterceptorChain();
        //當客戶端主動取消請求的時候執行
        if (retryAndFollowUpInterceptor.isCanceled()) {
          //請求失敗回撥
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          //請求成功回撥
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
         //省略部分程式碼
      } finally {
       //從runningAsyncCalls移除,並且從readyAsyncCalls取一個AsyncCall執行
        client.dispatcher().finished(this);
      }
    }
  }

首先如同步請求那樣呼叫getResponseWithInterceptorChain()進行網路請求,然後返回Response物件。如果當前請求取消的話,那麼就回調onFailure方法,否則就執行onResponse方法。最後執行了Dispathcer來標誌當前請求的結束!先看看這個finish方法執行了神馬,這個跟同步的請求還是有區別的:

void finished(AsyncCall call) {
    finished(runningAsyncCalls, call, true);
  }
///////////////////////////////
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
    int runningCallsCount;

    Runnable idleCallback;
    synchronized (this) {
      //刪除執行完畢的Call
      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
      //從等待佇列中獲取一個AsncCall物件並執行值
      if (promoteCalls) promoteCalls();
      //獲取正在執行的請求個數
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }

   //當所有請求都執行完畢的時候執行這個runnable
    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }

finished方法總的來說做了如下幾個工作:
1、從runningAsyncCalls刪除已經結束的AsycCall物件。
2、執行promoteCalls()方法:

private void promoteCalls() {
    //省略部分程式碼
    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall call = i.next();

      if (runningCallsForHost(call) < maxRequestsPerHost) {
        //從等待隊刪除call物件
        i.remove();
        //將AsycnCall物件放入runningAsyncCalls中
        runningAsyncCalls.add(call);
        //執行AsycnCall
        executorService().execute(call);
      }
      //省略部分程式碼
    }
  }

promoteCalls方法主要就是從等待佇列裡獲取AsycCall,然後放入runningAsyncCalls,並且交給執行緒池裡的執行緒執行。
3、呼叫runningCallsCount()獲取當前正在執行的網路而請求個數,如果為0話說明此時OkhttpClient處於空閒狀態,並且客戶端在初始化Dispatcher物件的時候呼叫了setIdleCallback(@Nullable Runnable idleCallback) 方法,則直接呼叫者run方法。這個idleCallback可以用來告知我們OkhttpClient什麼時候全部請求完畢,然後處理我們自己的業務邏輯。

//返回同步請求的個數和非同步請求的個數之和
 public synchronized int runningCallsCount() {
    return runningAsyncCalls.size() + runningSyncCalls.size();
  }

通過上面的分析,可以發現不論是同步請求還是非同步請求,都會交給Dispatcher統一管理,該Dispathcer有三個佇列:

  //等待執行的請求佇列
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

  //非同步執行的請求佇列
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

  //同步執行的請求佇列
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

Dispather也提供了相應的的方法來獲取這些佇列物件,比如我們獲取正在執行的請求,可以通過Dispatcher的runningCalls方法:

//將同步請求佇列和非同步請求佇列的資料組裝起來,返回一個集合
  public synchronized List<Call> runningCalls() {
    List<Call> result = new ArrayList<>();
    result.addAll(runningSyncCalls);
    for (AsyncCall asyncCall : runningAsyncCalls) {
      //get()方法返回的是一個realCall
      result.add(asyncCall.get());
    }
    return Collections.unmodifiableList(result);
  }

通過上文我們知道AsyncCall並不是Call介面的實現類,而是RealCall的內部類,它提供了get()方法,來返回一個RealCall物件:

RealCall get() {
      return RealCall.this;
    }

其實Call介面還有一個cancel()方法用來取消當前的Request請求,那麼我們就可以通過runningCalls返回的List來取消所有的請求:

List<Call> calls = runningCalls();
for(Call call : calls) {
  call.cancel();
}

其實Dispatcher本身就提供了取消所有請求的cancelAll方法:

 public synchronized void cancelAll() {
    //取消所有等待執行的非同步請求
    for (AsyncCall call : readyAsyncCalls) {
      call.get().cancel();
    }

    //取消所有的同步請求
    for (AsyncCall call : runningAsyncCalls) {
      call.get().cancel();
    }

   //取消正在執行的非同步請求
    for (RealCall call : runningSyncCalls) {
      call.cancel();
    }
  }

那麼這個cancel物件的cancel方法都做了什麼呢?進入RealCall物件檢視之:

public void cancel() {
    retryAndFollowUpInterceptor.cancel();
  }

很簡單就是將呼叫retryAndFollowUpInterceptor的cancel方法,取消攔截器鏈的執行。(想了解retryAndFollowUpInterceptor幹什麼的,點選此處

注意的是cancel方法並不能取消通過CallServerInterceptor攔截器傳送的資料(除非你能順著網線把傳送的資料在拉回來),如果該攔截器已經執行的話,伺服器會正常返回Resonse物件,此時對於非同步請求來說就會走callback的onFailure方法而已:

 //當客戶端主動取消請求的時候執行
        if (retryAndFollowUpInterceptor.isCanceled()) {
          //請求失敗回撥
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          //請求成功回撥
          responseCallback.onResponse(RealCall.this, response);
        }

那麼既然Dispatcher提供了cancelAll方法了,我們拿到runningCalls()還有什麼用呢?在Okhttp的api上官方建議我們使用OkhttpClient的時候最好就例項化一個OkhttpClient物件:
OkHttp performs best when you create a single instance and reuse it for all of your HTTP calls. This is because each client holds its own connection pool and thread pools.
那麼如果我們在Android應用中使用了OkhttpClient單利物件,在某Activity的onDestory方法呼叫了cancelAll()方法,很可能會導致如下問題:當一個頁面的A Activity onDestory的時候,另一個Activity B也已經建立且Activity B會通過OkhttpClient來訪問網路.有時候onDestroy的執行會比較之後,也就是說另B activity都已經啟動了,A 的onDestroy才執行,那麼B頁面因為A頁面的呼叫了callAll()方法就不會載入B頁面所需的資料了,尷了個尬!

那麼有沒有解決的方法呢?,大寫的有!
在我們構建Reqeust物件的時候,Reqeust.Buider物件提供瞭如下方法:

 public Builder tag(Object tag) {
      this.tag = tag;
      return this;
    }

可以通過tag方法為每一個Requset設定一個tag標識,此標識可以作為Request的唯一標識來用,且通過newCall(request)返回的RealCall也提供了獲取Request引用的方法:

//該方法是介面call提供的方法,由RealCall來實現
public Request request() {
    return originalRequest;
  }

那麼我們就可以通過為不同頁面的訪問請求來設定tag,在onDestroy取消掉自己的請求就可以了,程式碼如下:

     public void cancelByTag(Object tag) {
        Dispatcher dispatcher = client.dispatcher();
        //取消等待執行的非同步請求
        for (Call call : dispatcher .queuedCalls()) {
            if (tag.equals(call.request().tag())) {
                call.cancel();
            }
        }
        //取消所有執行的非同步和同步請求
        for (Call call : dispatcher .runningCalls()) {
            if (tag.equals(call.request().tag())) {
                call.cancel();
            }
        }
    }

其實這個cancelAll可以用在所有頁面或者應用退出的時候呼叫。
到此為止,其簡單的清流流程可以用如下來表示:
這裡寫圖片描述