OkHttp的基本流程
android 開發大多用過Okhttp, 在使用過程中,大多也是同步異步兩種方式。一般使用方式如下(基於3.14.2版本):
public static final MediaType JSON
= MediaType.get("application/json; charset=utf-8");
OkHttpClient client = new OkHttpClient();
String post(String url, String json) throws IOException {
RequestBody body = RequestBody.create(json, JSON);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
//同步
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
//異步
client.newCall(request).enqueue(new Callback(){
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse( Call call, Response response) throws IOException {
}
});
}
}
由此可見,無論同步異步其實整個請求大致可以分爲三個模塊, Request,Call,Response。它們就簡單概括了一個網絡請求的流程:封裝請求Request,進行網絡請求Call,返回結果Response. 所以我們在使用過程中只需要對這三個模塊進行封裝。
OkHttp的線程控制
- client.newCall
雖然我們將一個網絡請求過程簡單的歸爲三個部分,但是其中的細節纔是我們需要清楚的。接下來看client.newCall的內容。
/**
* Prepares the {@code request} to be executed at some point in the future.
*/
@Override public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false /* for web socket */);
}
newCall 方法返回了一個RealCall這個類實現了Call的接口, 可以認爲這個RealCall是Ok中執行網絡請求的一個單位。在RealCall中實現了一個請求的同步異步的兩個方法execute()和enqueue().
- RealCall中的真正執行函數
我們看execute和enqueue的僞代碼(暫時不用的地方已經省略)
public Response execute() throws IOException {
try {
//調用okhttpclient中的分發器去執行
client.dispatcher().executed(this);
//將結果直接返回,無論同步異步最終都會調用到getResponseWithInterceptorChain
//這個函數可以看做是整個okhttp的核心
return getResponseWithInterceptorChain();
} finally {
//執行完需要調用分發器中的結束函數
client.dispatcher().finished(this);
}
}
public void enqueue(Callback responseCallback) {
//同樣是用dispatcher進行分發,這裏是將AsyncCall進行執行
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
AysncCall是RealCall的內部類,這樣是爲了使用RealCall中的資源,比如okhttpclient,封裝的request等等。AsyncCall繼承了NameRunnable接口,NameRunable爲Runnable的實現,代碼如下:
public final void run() {
//更換執行時候的線程名,主要是爲了調試的時候能根據name能明白線程的作用
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
try {
//執行此類的抽象方法,NameRunable的子類需要實現這個方法
execute();
} finally {
Thread.currentThread().setName(oldName);
}
}
OkHttpClient中的Dispatcher
在上面的code中我們發現,無論是同步異步都會通過 client.dispatcher()這個對象去執行,看這個類名我們也可以猜測出這個類應該是負責線程的分發,可以說這個類算是okhttp的調度中心。既然是線程的分發,那麼肯定是涉及到了線程池。我們接下來看看Dispather的廬山真面目。
//最大併發請求數,
private int maxRequests = 64;
// 同一個域名最大連接數是5
private int maxRequestsPerHost = 5;
//空閒線程,當dispatcher空閒的時候執行,類似Handler機制中的idleHandler,開發者可視情況選擇使用,本文不詳述
private @Nullable Runnable idleCallback;
//OkHttp 線程池
private @Nullable ExecutorService executorService;
//準備執行的保存異步Asyncall的有一個等待隊列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
//保存正在執行AsyncCall的隊列
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
//正在執行的同步RealCall隊列
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
Dispatcher就是通過這些內容控制整個過程中線程的併發問題。首先來看線程池的配置
public synchronized ExecutorService executorService() {
if (executorService == null) {
//核心線程爲0, 最大線程數爲MAX_VALUE理論上可以無限制的開闢線程,
//空閒線程存活60s
//SynchronousQueue爲有界隊列,大小爲0,只是進行生產消費的傳遞作用
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
其實看完發現和android提供的newCachedThreadPool很類似,只是改了線程名。我們可以簡單分析一下這個線程池的運行機制:
- 線程池收到runnable因爲沒有核心線程,所以不需要創建核心線程。所以就直接加入任務隊列。
- 因爲SynchronousQueue不保存Runnable,所有接收到線程,就會去檢查如果有空閒線程,直接傳遞給空閒線程,沒有的話就開闢一個新的線程去執行。
如果只是這樣就會存在一個很大的問題,如果一直這麼創建新的線程, 理論上就可以創建無數線程。 這顯然不合理。其實這只是okhttp調度線程的一部分,更重要的部分由readyAsyncCalls runningAsyncCalls runningSyncCalls這部分實現。無論是同步還是異步,httpclient封裝一個call傳遞給調度器。在上面提到的execute或者是enqueue函數我們追查下去都會最終到Dispatcher中的promoteAndExecute()方法中。我們以enqueue爲例。
Dispatcher.java
void enqueue(AsyncCall call) {
synchronized (this) {
//將傳遞進來的call保存進入readyAsyncCalls隊列
readyAsyncCalls.add(call);
//找出同域名的請求,如果存在,那麼已經存在的call中的callsPerHost數將會被共享,
//callsPerHost數目表示同一個域名下鏈接的call的數目
//callsPerHost爲AtomicInteger保證多線程原子操作性,並且保證線程可見爲volatile類型
if (!call.get().forWebSocket) {
AsyncCall existingCall = findExistingCallWithHost(call.host());
if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
}
}
promoteAndExecute();
}
private boolean promoteAndExecute() {
//創建一個新的arraylist保存可執行的call
List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
//遍歷準備執行的隊列,如果有數據, 需要將數據視情況去執行並將可以執行的放到正在執行隊列
AsyncCall asyncCall = i.next();
//如果目前正在執行的call大於maxRequest默認是64,那麼直接停止退出循環
if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
//如果這次請求的域名下已經滿足最大連接個數(默認是5),那這個call暫時也不能執行
if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.
i.remove();
//將call中的表示域名連接數的變量自加1.
asyncCall.callsPerHost().incrementAndGet();
//將這個可以執行的call添加到executableCalls鏈表
executableCalls.add(asyncCall);
//同樣添加到正在執行的隊列
runningAsyncCalls.add(asyncCall);
}
isRunning = runningCallsCount() > 0;
}
//遍歷executableCalls列表,將每個ayncall添加到線程池去執行
for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
asyncCall.executeOn(executorService());
}
return isRunning;
}
由上面我們可以看出實際最大執行併發數是64, 並不會真的無限制的增加。最大併發線程數與每個域名同時可以連接數都是可以用戶自定義的。
Dispatcher.java
//設置最大併發線程數
public void setMaxRequests(int maxRequests)
//設置相同域名最大併發數
public void setMaxRequestsPerHost(int maxRequestsPerHost)
看到這裏我們已經清楚了,call是怎樣執行的,同時我們會有疑問,插入和移出必須是成對出現的,線程執行的時候添加到running隊列了。那麼什麼時候移出隊列呢?這就要我們繼續看AsyncCall的實際執行函數了。
AysncCall的父類NamedRunnable可以當做一個Runnable,從上文我們得知,在它的run方法中都需要執行 execute()方法,而這個方法是抽象方法,需要每個子類去實現。我們看看Ayncall中發生了什麼。
RealCall.AsyncCall
protected void execute() {
try {
//獲取執行結果
Response response = getResponseWithInterceptorChain();
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 {
//執行完之後,調用dispatcher中的finish函數
client.dispatcher().finished(this);
}
}
}
//我們進一步看看Dispatcher中的finish函數
Dispatcher.java
//這個類重載了多個finished方法,最終調用到這個私有函數裏面
private <T> void finished(Deque<T> calls, T call) {
Runnable idleCallback;
synchronized (this) {
//將隊列中的元素刪除,如果沒有這個元素會拋出異常
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
idleCallback = this.idleCallback;
}
boolean isRunning = promoteAndExecute();
//如果隊列沒有在執行的線程且idle線程不爲空則執行。 可以看出每個線程結束完都會檢查一遍是不是
//要執行空閒線程
if (!isRunning && idleCallback != null) {
idleCallback.run();
}
}
總結
以上就是okhttp大致的調度邏輯,本篇文章不涉及各種攔截器的分析。所以我們可以將請求的邏輯分爲三個模塊
- 封裝Request。
- 封裝RealCall提交到調度器Dispatcher去執行
- 將結果返回