轉載請標明出處:【顧林海的博客】
本平臺的文章更新會有延遲,大家可以關注微信公衆號-顧林海,更有Android、後端、Python、PHP、IOS以及React Native等等相關視頻教程,如果大家想獲取最新教程,請關注微信公衆號,謝謝!
前言
OkHttp是目前非常火的網絡庫,支持HTTP/2,允許所有同一個主機地址的請求共享同一個socket連接,連接池減少請求延時,透明的GZIP壓縮減少響應數據的大小,緩存響應內容,避免一些完全重複的請求。
OkHttpClient
private OkHttpClient mHttpClient = null;
private void initHttpClient() {
if (null == mHttpClient) {
mHttpClient = new OkHttpClient.Builder()
.readTimeout(5, TimeUnit.SECONDS)//設置讀超時
.writeTimeout(5,TimeUnit.SECONDS)////設置寫超時
.connectTimeout(15,TimeUnit.SECONDS)//設置連接超時
.retryOnConnectionFailure(true)//是否自動重連
.build();
}
}
在使用OkHttp請求網絡時,需要先獲取一個OkHttp的客戶端對象OkHttpClient,OkHttpClient可以直接通過new來創建,也可以通過OkHttpClient靜態內部類Builder來創建,日常開發最常用的是通過build的方式(建造者模式+鏈式調用)來創建,靜態內部Builder提供了很多方法,比如readTimeout代表讀時間、writeTimeout代表寫時間、connectTimeout代表連接超時時間以及retryOnConnectionFailure代表是否重連等等方法,有了OkHttpClient之後就可以進行網絡的同步和異步請求。
同步請求
private void synRequest() {
Request request=new Request.Builder()
.url("http://www.baidu.com")
.get()
.build();
Call call=mHttpClient.newCall(request);
try {
Response response=call.execute();
System.out.println(request.body().toString());
} catch (IOException e) {
e.printStackTrace();
}
}
進行網絡請求時,需要先創建一個請求對象Request,Request對象也是通過build方式創建,在Request的靜態內部類Builder中定義了設置請求地址、請求方式、請求頭的方法。
接着創建Call對象,Call對象可以理解爲Request和Response之間的一個橋樑,最後通過Call對象的execute方法完成Response的讀取,
總結同步請求的三個步驟如下:
-
創建OkHttpClient和Request對象。
-
將Request對象封裝成Call對象。
-
調用Call的execute()方法發送同步請求。
注意:OkHttp的同步請求會阻塞當前線程,因此不能在UI線程中請求,需要開啓子線程,在子線程中發送請求。
異步請求
private void asyRequest() {
final Request request=new Request.Builder()
.url("http://www.baidu.com")
.get()
.build();
Call call=mHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
System.out.println(request.body().toString());
}
});
}
異步請求與同步請求的前兩步都是一樣的創建OkHttpClient和Request對象以及將Request對象封裝成Call對象,通過Call對象的enqueue方法執行異步請求,enqueue傳入一個Callback對象,Callback提供了兩個回調方法,分別是成功和失敗。
總結異步請求的三個步驟如下:
-
創建OkHttpClient和Request對象。
-
將Request對象封裝成Call對象。
-
調用Call的enqueue方法進行異步請求。
注意:OkHttp的異步請求,其中兩個回調方法onResponse和onFailure都是在工作線程中執行的,執行結果可以通過Handler來發送。
解析同步請求
無論是同步請求還是異步請求,都需要一個OkHttpClient。
private OkHttpClient mHttpClient = null;
private void initHttpClient() {
if (null == mHttpClient) {
mHttpClient = new OkHttpClient.Builder()
.readTimeout(5, TimeUnit.SECONDS)//設置讀超時
.writeTimeout(5,TimeUnit.SECONDS)////設置寫超時
.connectTimeout(15,TimeUnit.SECONDS)//設置連接超時
.retryOnConnectionFailure(true)//是否自動重連
.build();
}
}
進入OkHttpClient的靜態內部類Builder:
public static final class Builder {
Dispatcher dispatcher;
ConnectionPool connectionPool;
public Builder() {
//分發器
dispatcher = new Dispatcher();
//連接池
connectionPool = new ConnectionPool();
...
}
public OkHttpClient build() {
return new OkHttpClient(this);
}
}
Builder構造器進行初始化操作,這裏先介紹幾個比較重要參數
-
Dispatcher分發器:它的作用是用於在異步請求時是直接進行處理,還是進行緩存等待,對於同步請求,Dispatcher會將請求放入隊列中。
-
ConnectionPool連接池:可以將客戶端與服務器的連接理解爲一個connection,每一個connection都會放在ConnectionPool這個連接池中,當請求的URL相同時,可以複用連接池中的connection,並且ConnectionPool實現了哪些網絡連接可以保持打開狀態以及哪些網絡連接可以複用的相應策略的設置。
通過Builder進行了一些參數的初始化,最後通過build方法創建OkHttpClient對象。
創建OkHttpClient對象時使用到了設計模式中的Builder模式,將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。
private void synRequest() {
Request request=new Request.Builder()
.url("http://www.baidu.com")
.get()
.build();
Call call=mHttpClient.newCall(request);
try {
Response response=call.execute();
System.out.println(request.body().toString());
} catch (IOException e) {
e.printStackTrace();
}
}
創建完OkHttpClient對象後,接着創建Request對象,也就是我們的請求對象。Request的創建方式和OkHttpClient的創建方式一樣,都使用了Builder模式。
Request內部類Builder的構造方法:
public static class Builder {
String method;
Headers.Builder headers;
public Builder() {
this.method = "GET";
this.headers = new Headers.Builder();
}
}
Request內部類的Builder構造方法非常的簡單,初始化了請求方式,默認是GET請求,接着初始化了頭部信息。
接着看它的build方法:
public Request build() {
if (url == null) throw new IllegalStateException("url == null");
return new Request(this);
}
build方法中先校驗傳入的url是否爲空,再將當前配置的請求參數傳給Request。
Request的構造方法:
Request(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = builder.headers.build();
this.body = builder.body;
this.tags = Util.immutableMap(builder.tags);
}
將我們配置好的參數,比如請求地址url、請求方式、請求頭部信息、請求體以及請求標誌傳給了Request。
總結同步請求的前兩步:
-
創建一個OkHttpClient對象。
-
構建了攜帶請求信息的Request對象。
完成上面兩步後,就可以通過OkHttpClient的newCall方法將Request包裝成Call對象。
Call call=mHttpClient.newCall(request);
進入OkHttpClient的newCall方法:
@Override public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false /* for web socket */);
}
Call只是一個接口,因此它的所有操作都在RealCall中實現的。
看一下RealCall的newRealCall方法:
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}
可以看到newRealCall方法只有三行代碼,第一行創建RealCall對象,第二行設置RealCall的eventListener也就是監聽事件,最後返回RealCall對象,我們看下RealCall的構造方法。
private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
this.client = client;
this.originalRequest = originalRequest;
this.forWebSocket = forWebSocket;
//重定向攔截器
this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
}
RealCall的構造方法中,設置了前兩步創建的對象OkHttpClient和Request並設置了重定向攔截器(攔截器概念後面會進行詳解)。
到這裏Call對象也創建完畢了,最後通過Call的execute方法來完成同步請求,看一下execute方法到底做了哪些操作,由於Call是接口,execute方法的具體實現在RealCall中:
@Override public Response execute() throws IOException {
//第一步:判斷同一Http是否請求過
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
//捕捉Http請求的異常堆棧信息
captureCallStackTrace();
//監聽請求開始
eventListener.callStart(this);
try {
//第二步:同步請求添加到同步隊列中
client.dispatcher().executed(this);
//第三步:
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
eventListener.callFailed(this, e);
throw e;
} finally {
//第四步:回收請求
client.dispatcher().finished(this);
}
}
第一步,在同步代碼塊中判斷標誌位executed是否爲ture,爲true拋出異常,也就是說同一個Http請求只能執行一次。
第二步,調用Dispatcher分發器的executed方法,我們進入Dispatcher的executed方法中。
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
Dispatcher的executed方法只是將我們的RealCall對象也就是請求添加到runningSyncCalls同步隊列中。
Dispatcher的作用就是維持Call請求發給它的一些狀態,同時維護一個線程池,用於執行網絡請求,Call這個請求在執行任務時通過Dispatcher分發器,將它的任務添加到執行隊列中進行操作。
第三步,通過getResponseWithInterceptorChain方法獲取Response對象,getResponseWithInterceptorChain方法的作用是通過一系列攔截器進行操作,最終獲取請求結果。
第四步,在finally塊中,主動回收同步請求,進入Dispatcher的finished方法:
void finished(RealCall call) {
finished(runningSyncCalls, call, false);
}
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
//移除請求
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
if (promoteCalls) promoteCalls();
//計算同步請求隊列+異步請求隊列的總數
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}
if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}
將我們正在執行的同步請求RealCall對象通過finished方法傳了進去,接着從當前的同步隊列中移除本次同步請求,promoteCalls默認傳入是false,也就是promoteCalls方法不會執行到,但如果是異步請求,這裏傳入的是ture,會執行promoteCalls方法,關於異步請求後面進行講解。從同步隊列中清除當前請求RealCall後,重新計算當前請求總數,我們可以看下runningCallsCount方法的具體實現。
public synchronized int runningCallsCount() {
return runningAsyncCalls.size() + runningSyncCalls.size();
}
方法非常簡單,就是計算正在執行的同步請求和異步請求的總和。
在finished方法最後判斷runningCallsCount,如果正在執行的請求數爲0並且idleaCallback不爲null,就執行idleaCallback的回調方法run。
到這裏同步請求已經介紹完了,在同步請求中Dispatcher分發器做的工作很簡單,就做了保存同步請求和移除同步請求的操作。
總結流程圖如下:
解析異步請求
其實從OkHttp的同步和異步的調用來看差別不是很大,在剖析OkHttp中的同步請求一節中知道同步是通過Call對象的execute()方法,而這節的異步請求調用的是Call對象的enqueue方法,但異步請求機制與同步請求相比,還是有所區別,這節就來分析異步請求的流程以及源碼分析。
還是先貼出異步請求的代碼:
private OkHttpClient mHttpClient = null;
private void initHttpClient() {
if (null == mHttpClient) {
mHttpClient = new OkHttpClient.Builder()
.readTimeout(5, TimeUnit.SECONDS)//設置讀超時
.writeTimeout(5, TimeUnit.SECONDS)////設置寫超時
.connectTimeout(15, TimeUnit.SECONDS)//設置連接超時
.retryOnConnectionFailure(true)//是否自動重連
.build();
}
}
private void asyRequest() {
final Request request = new Request.Builder()
.url("http://www.baidu.com")
.get()
.build();
Call call = mHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
System.out.println(request.body().toString());
}
});
}
這段代碼很熟悉,我們快速過一下流程:
-
創建OkHttpClient對象。
-
創建Request對象。
-
通過OkHttpClient的newCall方法將Request對象封裝Http實際請求的Call對象。
-
最後通過Call對象的enqueue方法傳入Callback對象並實現兩個回調方法。
這裏最大的區別就是最後一步調用的是enqueue方法,前三步都沒有發起真正的網絡請求,真正的網絡請求是在第四步,所以我們着重看最後一步。
在enqueue方法中,會傳入Callback對象進來,這個Callback對象就是用於請求結束後對結果進行回調的,進入enqueu方法。
public interface Call extends Cloneable {
...
void enqueue(Callback responseCallback);
...
}
發現這個Call只是接口,在剖析OkHttp中的同步請求一節中知道RealCall纔是真正實現Call的類。
點擊進入RealCall的enqueue方法:
@Override public void enqueue(Callback responseCallback) {
//判斷同一Http是否請求過
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
//捕捉Http請求的異常堆棧信息
captureCallStackTrace();
eventListener.callStart(this);
//重點1
client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
}
在enqueue方法中會先判斷RealCall這個Http請求是否請求過,請求過會拋出異常。
接着看最後一行代碼重點1:
1、傳入的Callback對象被封裝成了AsyncCall對象,點進去看一下AsyncCall對象到底是幹什麼的。
final class AsyncCall extends NamedRunnable {
private final Callback responseCallback;
AsyncCall(Callback responseCallback) {
super("OkHttp %s", redactedUrl());
this.responseCallback = responseCallback;
}
...
}
這個AsyncCall繼承了NameRunnable,這個NameRunnable又是什麼呢?點進去看一下:
public abstract class NamedRunnable implements Runnable {
}
原來NameRunnable就是一個Runnable對象。回過頭來總結一下,也就是說我們傳入的Callback對象被封裝成AsyncCall對象,這個AsyncCall對象本質就是一個Runnable對象。
2、獲取Dispatcher分發器,調用Dispatcher對象的enqueue方法並將AsyncCall對象作爲參數傳遞過去,最終完成異步請求,我們看下Dispatcher的enqueue方法。
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
synchronized void enqueue(RealCall.AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
//第一步
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
//第二步
readyAsyncCalls.add(call);
}
}
在enqueue方法前使用了synchronized關鍵字進行修飾,也就是爲這個方法加了個同步鎖,繼續往下看第一步,先是判斷當前異步請求總數是否小於設定的最大請求數(默認是64),以及正在運行的每個主機請求數是否小於設定的主機最大請求數(默認是5),如果滿足這兩個條件,就會把傳遞進來的AsyncCall對象添加到正在運行的異步請求隊列中,然後通過線程池執行這個請求。如果滿足不了上面的兩個條件就會走第二步,將AsyncCall對象存入readyAsyncCalls隊列中,這個readyAsyncCalls就是用來存放等待請求的一個隊列。
總結RealCall的enqueue方法:
-
判斷當前Call:實際的Http請求是否只執行一次,如果不是拋出異常。
-
封裝成一個AsyncCall對象:將Callback對象封裝成一個AsyncCall對象,AsyncCall對象就是一個Runnable對象。
-
client.dispatcher().enqueue():構建完AsyncCall也就是Runnable對象後,調用Dispatcher對象的enqueue方法來進行異步的網絡請求,並判斷當前請求數小於64以及當前host請求數小於5的情況下,將Runnable對象放入正在請求的異步隊列中並通過線程池執行RealCall請求。如果不滿足條件,將Runnable添加到等待就緒的異步請求隊列當中。
在上面總結的第三步中,滿足條件會將AsyncCall對象通過線程池執行,我們看一下線程池方法executorService():
private @Nullable ExecutorService executorService;
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;
}
executorService方法只是返回一個線程池對象executorService。
獲取線程池對象後,就可以調用它的execute方法,execute方法需要傳入一個Runnable對象,AsyncCall對象繼承NamedRunnable對象,而NamedRunnable又繼承了Runnable對象,那麼AsyncCall就是一個Runnable對象,這裏就會將AsyncCall對象傳入。
在源碼中發現AsyncCall並沒有實現run方法,那麼這個run方法一定就是在它的父類NamedRunnable中,我們點擊進去看下:
public abstract class NamedRunnable implements Runnable {
protected final String name;
public NamedRunnable(String format, Object... args) {
this.name = Util.format(format, args);
}
@Override public final void run() {
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
try {
execute();
} finally {
Thread.currentThread().setName(oldName);
}
}
protected abstract void execute();
}
發現NamedRunnable是一個抽象類,在run方法中並沒有做實際操作,只是調用了抽象方法execute,這是一個典型的模板方法模式。既然AsyncCall繼承了NamedRunnable這個抽象類,那麼抽象方法execute的具體實現就交由AsyncCall來實現了。
進入AsyncCall中的execute方法:
@Override protected void execute() {
boolean signalledCallback = false;
try {
//重點1
Response response = getResponseWithInterceptorChain();
if (retryAndFollowUpInterceptor.isCanceled()) {
//重點2:重定向和重試攔截器
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 {
eventListener.callFailed(RealCall.this, e);
responseCallback.onFailure(RealCall.this, e);
}
} finally {
//重點3:請求當前的異步請求
client.dispatcher().finished(this);
}
}
在重點1處通過getResponseWithInterceptorChain()方法獲取返回的Response對象,getResponseWithInterceptorChain方法的作用是通過一系列的攔截器獲取Response對象。
在重點2處判斷重定向和重試攔截器是否取消,如果取消,調用responseCallback的onFailure回調,responseCallback就是我們通過enqueue方法傳入的Callback對象。如果沒取消,調用responseCallback的onResponse回調。
由於execute方法是在run方法中執行的,所以onFailure和onResponse回調都是在子線程當中。
在重點3處finally塊中,調用Dispatcher的finished方法。
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) {
//移除請求
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
if (promoteCalls) promoteCalls();
//計算同步請求隊列+異步請求隊列的總數
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}
if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}
finished方法內部會將本次的異步請求RealCall從正在請求的異步請求隊列中移除,由於promoteCalls傳入的是true,接着調用promoteCalls()方法,接着統計正在請求的同步和異步的請求總數,以及判斷當前總的請求數如果等於0並且idleCallback對象不爲空的情況下執行idleCallback對象的run方法。
finished方法的介紹在剖析OkHttp中的同步請求一節中其實已經介紹過了,唯一有區別的就是promoteCalls參數,同步的時候傳入的是false,但在異步請求時傳入的是true,也就是會執行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.
}
}
在完成異步請求後,需要將當前的異步請求RealCall從正在請求的異步隊列中移除,移除完畢後會通過promoteCalls方法,將等待就緒的異步隊列中的請求添加到正在請求的異步請求隊列中去並通過線程池來執行異步請求。
任務調度器Dispatcher
OkHttp發送的同步或異步請求隊列的狀態會在dispatcher中進行管理,dispatcher的作用就是用於維護同步和異步請求的狀態,內部維護一個線程池,用於執行相應的請求。
在dispatcher內部維護着三個隊列,這三個隊列如下:
private final Deque<RealCall.AsyncCall> readyAsyncCalls = new ArrayDeque<>();
private final Deque<RealCall.AsyncCall> runningAsyncCalls = new ArrayDeque<>();
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
runningAsyncCalls表示的是正在執行的異步請求隊列。
readyAsyncCalls表示就緒狀態的異步請求隊列,如果當前的請求不滿足某種條件時,當前的異步請求會進入就緒等待的異步請求隊列中,當滿足某種條件時,會從就緒等待的異步請求隊列中取出異步請求,放入正在執行的異步請求隊列中。
runningSyncCalls表示的正在執行的同步請求隊列。
除了上面的三個隊列,還有一個參數也是非常重要的,如下:
private @Nullable ExecutorService executorService;
executorService就是一個線程池對象,OkHttp的異步請求操作會放入這個線程池中。
OkHttp的異步請求在dispatcher中的一系列操作可以理解爲生產者消費者模式,其中Dispatcher是生產者,它是運行在主線程中的,ExecutorService代表消費者池。
當同步和異步請求結束後,會調用dispatcher的finished方法,將當前的請求從隊列中移除。
client.dispatcher().finished(this);
這段代碼是在finally塊中,也就是說,無論請求成不成功,還是出現異常,這段代碼都會執行,保證了請求的整個生命週期從開始到銷燬。
接下來,我們重新看看同步請求和異步請求在dispatcher中的操作。
1、同步請求會執行dispatcher的executed方法:
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
在executed方法中,只是將當前請求RealCall存入正在執行的同步請求隊列中。
2、異步請求會執行dispatcher的enqueue方法;
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
synchronized void enqueue(RealCall.AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
//第一步
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
//第二步
readyAsyncCalls.add(call);
}
}
上一節在講解異步請求時已經解釋過了,這裏直接複製過來:在enqueue方法前使用了synchronized關鍵字進行修飾,也就是爲這個方法加了個同步鎖,繼續往下看第一步,先是判斷當前異步請求總數是否小於設定的最大請求數(默認是64),以及正在運行的每個主機請求數是否小於設定的主機最大請求數(默認是5),如果滿足這兩個條件,就會把傳遞進來的AsyncCall對象添加到正在運行的異步請求隊列中,然後通過線程池執行這個請求。如果滿足不了上面的兩個條件就會走第二步,將AsyncCall對象存入readyAsyncCalls隊列中,這個readyAsyncCalls就是用來存放等待請求的一個異步隊列。
其中線程池的創建和獲取代碼如下:
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;
}
創建線程池時,第一個參數表示核心線程數,設置爲0,當線程池空閒一段時間後,就會將線程池中的所有線程進行銷燬;第二個參數表示最大線程數,設置爲整型的最大值,具體多少線程數還是受限OkHttp中的maxRequests這個參數;第三個參數表示當我們的線程數大於核心線程數的時候,多餘的空閒線程存活的最大時間爲60秒,結合第一個核心線程數爲0,也就是說OkHttp中的線程在工作完畢後,會在60秒之內進行關閉。
攔截器介紹
在OkHttp中執行同步請求會阻塞當前線程,直到HTTP響應返回,同步請求使用的是execute()方法;而異步請求類似於非阻塞式的請求,它的執行結果一般通過接口回調的方式告知調用者,異步請求使用的是enqueue(Callback)方法;
OkHttp中不管是同步還是異步,都是通過攔截器完成網絡的獲取。
官網對攔截器的解釋是:攔截器是OkHttp中提供的一種強大機制,它可以實現網絡監聽、請求以及響應重寫、請求失敗重試等功能。
看下面這張圖:
在這張圖中可以看到有兩種攔截器,一種是APPLICATION INTERCEPTORS,也就是應用攔截器;第二種是NETWORK INTERCEPTORS,表示網絡攔截器。除了這兩種攔截器,重要的是中間OkHttp core這塊,這是OkHttp提供的內部攔截器。
看下圖:
這是OkHttp提供給我們的攔截器,內部是以攔截器的鏈的形式執行HTTP的請求,其中RetryAndFollowUpInterceptor是重試和失敗重定向攔截器,BridgeInterceptor是橋接和適配攔截器,CacheInterceptor是緩存攔截器,ConnectInterceptor是連接攔截器,負責建立可用的連接,CallServerInterceptor負責將HTTP的請求寫入網絡的IO流中,並且從網絡IO流中讀取服務端返回給客戶端的數據。
看過前面幾節的同學應該知道,無論是同步請求還是異步請求,最終執行網絡請求並獲取的Response都是通過getResponseWithInterceptorChain()方法獲取的,代碼如下。
//異步請求
@Override protected void execute() {
boolean signalledCallback = false;
try {
//重點1 使用攔截器鏈
Response response = getResponseWithInterceptorChain();
...
} catch (IOException e) {
...
} finally {
回收請求
client.dispatcher().finished(this);
}
}
//同步請求
@Override public Response execute() throws IOException {
//第一步:判斷同一Http是否請求過
...
//捕捉Http請求的異常堆棧信息
...
//監聽請求開始
...
try {
//第二步:同步請求添加到同步隊列中
...
//第三步:使用攔截器鏈
Response result = getResponseWithInterceptorChain();
...
} catch (IOException e) {
...
} finally {
//第四步:回收請求
client.dispatcher().finished(this);
}
}
getResponseWithInterceptorChain()方法返回的就是我們網絡請求的響應結果Response對象。
進入getResponseWithInterceptorChain()方法:
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
//用戶自定義的攔截器
interceptors.addAll(client.interceptors());
//添加OkHttp提供的五個攔截器以及networkInterceptors
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));
//標記1
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
//標記2
return chain.proceed(originalRequest);
}
getResponseWithInterceptorChain方法一開始將我們需要的攔截器添加到一個集合中,其中就包括我們自定義的攔截器以及上面提到的幾種攔截器。
接着在標記1處創建了一個RealInterceptorChain對象,傳入的第一個參數就是上面的添加的一系列攔截器,創建完畢後,在標記2處執行RealInterceptorChain對象的proceed方法。
進入RealInterceptorChain的proceed方法:
@Override public Response proceed(Request request) throws IOException {
return proceed(request, streamAllocation, httpCodec, connection);
}
繼續往下看:
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
...
//標記1
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
//標記2:取出index位置的攔截器
Interceptor interceptor = interceptors.get(index);
//標記3
Response response = interceptor.intercept(next);
...
return response;
}
在標記1處又創建了一個RealInterceptorChain對象,在創建對象時,傳入的第五個參數是index+1,這樣的話在下次訪問時,只能從下一個攔截器開始進行訪問,而不能從當前攔截器。
在標記2處取出第index位置的攔截器。
在標記3處將代表下一個攔截器的鏈的RealInterceptorChain對象傳入當前位置的攔截器中,在當前攔截器鏈中執行請求,獲取Response後依次返回給它的上一個攔截器,如果當前攔截器沒有獲取Response就繼續調用RealInterceptorChain對象的prceed方法來創建下一個攔截器鏈,就這樣攔截器鏈一層一層的調用,這樣所有的攔截器鏈構成了一個完整的鏈條。
到目前爲止,總結如下:
-
創建一系列攔截器,並將其放入一個攔截器list集合中。
-
創建一個攔截器鏈RealInterceptorChain,並執行攔截器鏈的proceed方法,這個proceed方法的核心是繼續創建下一個攔截器鏈。
我們看下RetryAndFollowUpInterceptor這個攔截器,它是重試和失敗重定向攔截器。
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
...
RealInterceptorChain realChain = (RealInterceptorChain) chain;
...
response = realChain.proceed(request, streamAllocation, null, null);
...
}
可以看到RetryAndFollowUpInterceptor攔截器的intercept方法,內部又執行了傳遞進來的RealInterceptorChain對象的proceed方法,而proceed方法在上面介紹過了,作用是創建下一個攔截器鏈,這樣就說明了整個攔截器鏈的執行過程就像鏈條一樣,一環扣一環。
RetryAndFollowUpInterceptor攔截器
RetryAndFollowupInterceptor是重試重定向攔截器,它的主要作用是負責失敗重連。OkHttp中的重定向功能是默認開啓的。
該攔截器方法如下:
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
...
//標記1
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
...
while (true) {
//取消,釋放
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
Response response;
boolean releaseConnection = true;
...
//標記2
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
...
Request followUp;
//標記3
followUp = followUpRequest(response, streamAllocation.route());
...
//標記4
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
...
}
}
在intercept方法中省略了一些代碼,我們只看核心代碼:
在標記1處創建了StreamAllocation對象,創建的StreamAllocation對象在RetryAndFollowupInterceptor攔截器中並沒有使用到。
StreamAllocation對象會通過標記2處的proceed方法傳遞給下一個攔截器,直到ConnectInterceptor攔截器,StreamAllocation的主要作用是提供建立HTTP請求所需要的網絡組件,ConnectInterceptor從RealInterceptorChain獲取前面的Interceptor傳過來的StreamAllocation對象,執行StreamAllocation對象的newStream()方法完成所有的連接建立工作,並將這個過程中創建的用於網絡IO的RealConnection對象,以及與服務器交互最爲關鍵的HttpCodec等對象傳遞給後面的CallServerInterceptor攔截器。
在標記3處,會對返回的Response進行檢查,通過followUpRequest方法對Response返回的狀態碼判斷,並創建重定向需要發出的Request對象。
followUpRequest代碼如下:
private Request followUpRequest(Response userResponse, Route route) throws IOException {
if (userResponse == null) throw new IllegalStateException();
int responseCode = userResponse.code();
final String method = userResponse.request().method();
switch (responseCode) {
case HTTP_PROXY_AUTH://407 代理服務器認證
...
case HTTP_UNAUTHORIZED://401 身份未認證
...
case HTTP_PERM_REDIRECT://308
case HTTP_TEMP_REDIRECT://307
//當請求的method不爲GET和HEAD時不進行重定向
...
case HTTP_MULT_CHOICE://300 多種選擇
case HTTP_MOVED_PERM://301 永久移除
case HTTP_MOVED_TEMP://302 臨時重定向
case HTTP_SEE_OTHER://303 其他問題
...
case HTTP_CLIENT_TIMEOUT://408 請求超時
...
case HTTP_UNAVAILABLE://503 服務不可用
...
default:
return null;
}
}
followUpRequest方法主要是對返回的狀態碼進行判斷,根據特定的狀態碼創建重定向需要的Request對象。
回到上面攔截器intercept方法,在標記4判斷followUpCount是否大於MAX_FOLLOW_UPS(20),也就是說重定向次數上限爲20,當重試次數超過20的時候,會釋放StreamAllocation這個對象,這樣做是爲了防止無限制的重試網絡請求,從而造成資源的浪費,關於重定向的次數建議可以按照Chrome遵循21個重定向;Firefox、CURL和WGET遵循20;Safari遵循16;HTTP/1推薦5。
總結RetryAndFollowupInterceptor攔截器:
-
創建StreamAllocation對象。
-
調用RealInterceptorChain.proceed()進行網絡請求。
-
根據異常結果或響應結果判斷是否進行重新請求。
-
調用下一個攔截器,對Response進行處理,返回給上一個攔截器。
BridgeInterceptor攔截器
BridgeInterceptor是橋接和適配攔截器,它的作用是設置內容長度、編碼方式以及壓縮等等一系列操作,主要是添加頭部的作用。
該攔截器方法如下:
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
//====================添加頭部信息========================
RequestBody body = userRequest.body();
...
if (userRequest.header("Connection") == null) {
//標記1
requestBuilder.header("Connection", "Keep-Alive");
}
...
//標記2
Response networkResponse = chain.proceed(requestBuilder.build());
//標記3
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
//標記4
if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
//標記5
GzipSource responseBody = new GzipSource(networkResponse.body().source());
...
}
return responseBuilder.build();
}
intercept方法主要是給Request添加一些頭部信息,在標記1處的Connection設置爲Keep-Alive,Keep-Alive的作用是當我們開啓一個HTTP連接後,在一定時間內保持它的連接狀態。
在標記2處調用了攔截器鏈的proceed方法向服務器發起請求。
在標記3處通過HttpHeaders的receiveHeaders方法,通過網絡請求,服務器返回的Response轉化爲客戶端可以識別的Response,如果HTTP默認支持gzip,那麼BridgeInterceptor攔截器將會對這個Response進行解壓,最終得到客戶端使用的Response。
在標記4處,對transparentGzip標誌位進行判斷,如果transparentGzip爲true就表面Accept-Encoding支持gzip壓縮;再判斷頭部的Content-Encoding是否爲gzip,保證服務端返回的響應體內容是經過了gzip壓縮的;最後判斷HTTP頭部是否有Body。當滿足這些條件後在標記5處將Response的body轉換爲GzipSource類型,這樣的話client端直接以解壓的方式讀取數據。
總結BridgeInterceptor攔截器:
-
負責將用戶構建的一個Request請求轉化爲能夠進行網絡訪問的請求,通過給頭部添加信息。
-
將這個符合網絡請求的Request進行網絡請求。
-
將網絡請求回來的響應Response轉化爲用戶可用的Response。
CacheInterceptor攔截器
緩存策略
mHttpClient = new OkHttpClient.Builder()
.cache(new Cache(new File("cache"),30*1024*1024))//使用緩存策略
.build();
在OkHttp中使用緩存可以通過OkHttpClient的靜態內部類Builder的cache方法進行配置,cache方法傳入一個Cache對象。
public Cache(File directory, long maxSize) {
this(directory, maxSize, FileSystem.SYSTEM);
}
創建Cache對象時需要傳入兩個參數,第一個參數directory代表的是緩存目錄,第二個參數maxSize代表的是緩存大小。
在Chache有一個很重要的接口:
public final class Cache implements Closeable, Flushable {
final InternalCache internalCache = new InternalCache() {
@Override
public Response get(Request request) throws IOException {
return Cache.this.get(request);
}
@Override
public CacheRequest put(Response response) throws IOException {
return Cache.this.put(response);
}
@Override
public void remove(Request request) throws IOException {
Cache.this.remove(request);
}
@Override
public void update(Response cached, Response network) {
Cache.this.update(cached, network);
}
@Override
public void trackConditionalCacheHit() {
Cache.this.trackConditionalCacheHit();
}
@Override
public void trackResponse(CacheStrategy cacheStrategy) {
Cache.this.trackResponse(cacheStrategy);
}
};
...
}
通過InternalCache這個接口實現了緩存的一系列操作,接着我們一步步看它是如何實現的,接下來分析緩存的get和put操作。
先看InternalCache的put方法,也就是存儲緩存:
public final class Cache implements Closeable, Flushable {
final InternalCache internalCache = new InternalCache() {
...
@Override
public CacheRequest put(Response response) throws IOException {
return Cache.this.put(response);
}
...
};
...
}
InternalCache的put方法調用的是Cache的put方法,往下看:
@Nullable CacheRequest put(Response response) {
//標記1:獲取請求方法
String requestMethod = response.request().method();
//標記2:判斷緩存是否有效
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
}
return null;
}
//標記3:非GET請求不使用緩存
if (!requestMethod.equals("GET")) {
return null;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
//標記4:創建緩存體類
Cache.Entry entry = new Cache.Entry(response);
//標記5:使用DiskLruCache緩存策略
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
entry.writeTo(editor);
return new Cache.CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
首先在標記1處獲取我們的請求方式,接着在標記2處根據請求方式判斷緩存是否有效,通過HttpMethod的靜態方法invalidatesCache。
public static boolean invalidatesCache(String method) {
return method.equals("POST")
|| method.equals("PATCH")
|| method.equals("PUT")
|| method.equals("DELETE")
|| method.equals("MOVE"); // WebDAV
}
通過invalidatesCache方法,如果請求方式是POST、PATCH、PUT、DELETE以及MOVE中一個,就會將當前請求的緩存移除。
在標記3處會判斷如果當前請求不是GET請求,就不會進行緩存。
在標記4處創建Entry對象,Entry的構造器如下:
Entry(Response response) {
this.url = response.request().url().toString();
this.varyHeaders = HttpHeaders.varyHeaders(response);
this.requestMethod = response.request().method();
this.protocol = response.protocol();
this.code = response.code();
this.message = response.message();
this.responseHeaders = response.headers();
this.handshake = response.handshake();
this.sentRequestMillis = response.sentRequestAtMillis();
this.receivedResponseMillis = response.receivedResponseAtMillis();
}
創建的Entry對象在內部會保存我們的請求url、頭部、請求方式、協議、響應碼等一系列參數。
在標記5處可以看到原來OkHttp的緩存策略使用的是DiskLruCache,DiskLruCache是用於磁盤緩存的一套解決框架,OkHttp對DiskLruCache稍微做了點修改,並且OkHttp內部維護着清理內存的線程池,通過這個線程池完成緩存的自動清理和管理工作,這裏不做過多介紹。
拿到DiskLruCache的Editor對象後,通過它的edit方法創建緩存文件,edit方法傳入的是緩存的文件名,通過key方法將請求url進行MD5加密並獲取它的十六進制表示形式。
接着執行Entry對象的writeTo方法並傳入Editor對象,writeTo方法的目的是將我們的緩存信息存儲在本地。
點進writeTo方法:
public void writeTo(DiskLruCache.Editor editor) throws IOException {
BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
//緩存URL
sink.writeUtf8(url)
.writeByte('\n');
//緩存請求方式
sink.writeUtf8(requestMethod)
.writeByte('\n');
//緩存頭部
sink.writeDecimalLong(varyHeaders.size())
.writeByte('\n');
for (int i = 0, size = varyHeaders.size(); i < size; i++) {
sink.writeUtf8(varyHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(varyHeaders.value(i))
.writeByte('\n');
}
//緩存協議,響應碼,消息
sink.writeUtf8(new StatusLine(protocol, code, message).toString())
.writeByte('\n');
sink.writeDecimalLong(responseHeaders.size() + 2)
.writeByte('\n');
for (int i = 0, size = responseHeaders.size(); i < size; i++) {
sink.writeUtf8(responseHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(responseHeaders.value(i))
.writeByte('\n');
}
//緩存時間
sink.writeUtf8(SENT_MILLIS)
.writeUtf8(": ")
.writeDecimalLong(sentRequestMillis)
.writeByte('\n');
sink.writeUtf8(RECEIVED_MILLIS)
.writeUtf8(": ")
.writeDecimalLong(receivedResponseMillis)
.writeByte('\n');
//判斷https
if (isHttps()) {
sink.writeByte('\n');
sink.writeUtf8(handshake.cipherSuite().javaName())
.writeByte('\n');
writeCertList(sink, handshake.peerCertificates());
writeCertList(sink, handshake.localCertificates());
sink.writeUtf8(handshake.tlsVersion().javaName()).writeByte('\n');
}
sink.close();
}
writeTo方法內部對Response的相關信息進行緩存,並判斷是否是https請求並緩存Https相關信息,從上面的writeTo方法中發現,返回的響應主體body並沒有在這裏進行緩存,最後返回一個CacheRequestImpl對象。
private final class CacheRequestImpl implements CacheRequest {
private final DiskLruCache.Editor editor;
private Sink cacheOut;
private Sink body;
boolean done;
CacheRequestImpl(final DiskLruCache.Editor editor) {
this.editor = editor;
this.cacheOut = editor.newSink(ENTRY_BODY);
this.body = new ForwardingSink(cacheOut) {
@Override public void close() throws IOException {
synchronized (Cache.this) {
if (done) {
return;
}
done = true;
writeSuccessCount++;
}
super.close();
editor.commit();
}
};
}
}
在CacheRequestImpl類中有一個body對象,這個就是我們的響應主體。CacheRequestImpl實現了CacheRequest接口,用於暴露給緩存攔截器,這樣的話緩存攔截器就可以直接通過這個類來更新或寫入緩存數據。
看完了put方法,繼續看get方法:
public final class Cache implements Closeable, Flushable {
final InternalCache internalCache = new InternalCache() {
...
@Override public Response get(Request request) throws IOException {
return Cache.this.get(request);
}
...
};
...
}
查看Cache的get方法:
@Nullable Response get(Request request) {
//獲取緩存的key
String key = key(request.url());
//創建快照
DiskLruCache.Snapshot snapshot;
Cache.Entry entry;
try {
//更加key從緩存獲取
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
return null;
}
try {
//從快照中獲取緩存
entry = new Cache.Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
Response response = entry.response(snapshot);
if (!entry.matches(request, response)) {
//響應和請求不是成對出現
Util.closeQuietly(response.body());
return null;
}
return response;
}
get方法比較簡單,先是根據請求的url獲取緩存key,創建snapshot目標緩存中的快照,根據key獲取快照,當目標緩存中沒有這個key對應的快照,說明沒有緩存返回null;如果目標緩存中有這個key對應的快照,那麼根據快照創建緩存Entry對象,再從Entry中取出Response。
Entry的response方法:
public Response response(DiskLruCache.Snapshot snapshot) {
String contentType = responseHeaders.get("Content-Type");
String contentLength = responseHeaders.get("Content-Length");
//根據頭部信息創建緩存請求
Request cacheRequest = new Request.Builder()
.url(url)
.method(requestMethod, null)
.headers(varyHeaders)
.build();
//創建Response
return new Response.Builder()
.request(cacheRequest)
.protocol(protocol)
.code(code)
.message(message)
.headers(responseHeaders)
.body(new Cache.CacheResponseBody(snapshot, contentType, contentLength))
.handshake(handshake)
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(receivedResponseMillis)
.build();
}
Entry的response方法中會根據頭部信息創建緩存請求,然後創建Response對象並返回。
回到get方法,接着判斷響應和請求是否成對出現,如果不是成對出現,關閉流並返回null,否則返回Response。
到這裏緩存的get和put方法的整體流程已經介紹完畢,接下來介紹緩存攔截器。
CacheInterceptor
進入CacheInterceptor的intercept方法,下面貼出部分重要的代碼。
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
...
}
第一步先嚐試從緩存中獲取Response,這裏分兩種情況,要麼獲取緩存Response,要麼cacheCandidate爲null。
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
...
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
...
}
第二步,獲取緩存策略CacheStrategy對象,CacheStrategy內部維護了一個Request和一個Response,也就是說CacheStrategy能指定到底是通過網絡還是緩存,亦或是兩者同時使用獲取Response。
CacheStrategy內部工廠類Factory的get方法如下:
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
// We're forbidden from using the network and the cache is insufficient.
return new CacheStrategy(null, null);
}
return candidate;
}
方法中通過getCandidate方法獲取CacheStrategy對象,繼續點進去:
private CacheStrategy getCandidate() {
//標記1:沒有緩存Response
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
//標記2
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
...
CacheControl requestCaching = request.cacheControl();
//標記3
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
//標記4
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
...
//標記5
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());
}
...
}
標記1處,可以看到先對cacheResponse進行判斷,如果爲空,說明沒有緩存對象,這時創建CacheStrategy對象並且第二個參數Response傳入null。
標記2處,判斷請求是否是https請求,如果是https請求但沒有經過握手操作 ,創建CacheStrategy對象並且第二個參數Response傳入null。
標記3處,判斷如果不使用緩存或者服務端資源改變,亦或者驗證服務端發過來的最後修改的時間戳,同樣創建CacheStrategy對象並且第二個參數Response傳入null。
標記4處,判斷緩存是否受影響,如果不受影響,創建CacheStrategy對象時,第一個參數Request爲null,第二個參數Response直接使用cacheResponse。
標記5處,根據一些信息添加頭部信息 ,最後創建CacheStrategy對象。
回到CacheInterceptor的intercept方法:
@Override public Response intercept(Chain chain) throws IOException {
...
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
...
}
第三步,判斷當前如果不能使用網絡同時又沒有找到緩存,這時會創建一個Response對象,code爲504的錯誤。
@Override public Response intercept(Chain chain) throws IOException {
...
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
...
}
第四步,如果當前不能使用網絡,就直接返回緩存結果。
@Override public Response intercept(Chain chain) throws IOException {
...
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
...
}
...
}
第五步,調用下一個攔截器進行網絡請求。
@Override public Response intercept(Chain chain) throws IOException {
...
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
...
}
第六步,當通過下個攔截器獲取Response後,判斷當前如果有緩存Response,並且網絡返回的Response的響應碼爲304,代表從緩存中獲取。
@Override public Response intercept(Chain chain) throws IOException {
...
if (cache != null) {
//標記1
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
//標記2
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
}
}
}
return response;
}
第七步,標記1判斷http頭部有沒有響應體,並且緩存策略可以被緩存的,滿足這兩個條件後,網絡獲取的Response通過cache的put方法寫入到緩存中,這樣下次取的時候就可以從緩存中獲取;標記2處判斷請求方法是否是無效的請求方法,如果是的話,從緩存池中刪除這個Request。最後返回Response給上一個攔截器。
ConnectInterceptor攔截器
ConnectInterceptor是網絡連接攔截器,我們知道在OkHttp當中真正的網絡請求都是通過攔截器鏈來實現的,通過依次執行這個攔截器鏈上不同功能的攔截器來完成數據的響應,ConnectInterceptor的作用就是打開與服務器之間的連接,正式開啓OkHttp的網絡請求。
走進ConnectInterceptor的intercept方法:
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
//標記1
StreamAllocation streamAllocation = realChain.streamAllocation();
...
}
在標記1處可以看到從上一個攔截器中獲取StreamAllocation對象,在講解第一個攔截器RetryAndFollowUpInterceptor重試重定向的時候已經介紹過StreamAllocation,在RetryAndFollowUpInterceptor中只是創建了這個對象並沒有使用,真正使用它的是在ConnectInterceptor中,StreamAllocation是用來建立執行HTTP請求所需要的網絡組件,既然我們拿到了StreamAllocation,接下來看這個StreamAllocation到底做了哪些操作。
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
...
//標記1
StreamAllocation streamAllocation = realChain.streamAllocation();
..
//標記2
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
...
}
在標記2處通過StreamAllocation對象的newStream方法創建了一個HttpCodec對象,HttpCodec的作用是用來編碼我們的Request以及解碼我們的Response。
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
...
//標記1
StreamAllocation streamAllocation = realChain.streamAllocation();
...
//標記2
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
//標記3
RealConnection connection = streamAllocation.connection();
...
}
在標記3處通過StreamAllocation對象的connection方法獲取到RealConnection對象,這個RealConnection對象是用來進行實際的網絡IO傳輸的。
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
...
//標記1
StreamAllocation streamAllocation = realChain.streamAllocation();
...
//標記2
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
//標記3
RealConnection connection = streamAllocation.connection();
//標記4
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
標記4處是我們非常熟悉的代碼了,繼續調用攔截器鏈的下一個攔截器並將Request、StreamAllocation、HttpCodec以及RealConnection對象傳遞過去。
總結:
-
首先ConnectInterceptor攔截器從攔截器鏈獲取到前面傳遞過來的StreamAllocation,接着執行StreamAllocation的newStream方法創建HttpCodec,HttpCodec對象是用於處理我們的Request和Response。
-
最後將剛纔創建的用於網絡IO的RealConnection對象,以及對於服務器交互最爲關鍵的HttpCodec等對象傳遞給後面的攔截器。
從上面我們瞭解了ConnectInterceptor攔截器的intercept方法的整體流程,從前一個攔截器中獲取StreamAllocation對象,通過StreamAllocation對象的newStream方法創建了一個HttpCodec對象,我們看看這個newStream方法具體做了哪些操作。
public HttpCodec newStream(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
...
try {
//標記1
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
...
} catch (IOException e) {
throw new RouteException(e);
}
}
我們可以看到在標記1處創建了一個RealConnection對象,以及HttpCodec對象,這兩個對象在上面已經介紹過了,RealConnection對象是用來進行實際的網絡IO傳輸的,HttpCodec是用來編碼我們的Request以及解碼我們的Response。
通過findHealthyConnection方法生成一個RealConnection對象,來進行實際的網絡連接。
findHealthyConnection方法:
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
boolean doExtensiveHealthChecks) throws IOException {
while (true) {
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
pingIntervalMillis, connectionRetryEnabled);
...
synchronized (connectionPool) {
if (candidate.successCount == 0) {
return candidate;
}
}
...
return candidate;
}
}
在方法中開啓了while循環,內部的同步代碼塊中判斷candidate的successCount如果等於0,說明整個網絡連接結束並直接返回candidate,而這個candidate是通過同步代碼塊上面的findConnection方法獲取的。
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
boolean doExtensiveHealthChecks) throws IOException {
while (true) {
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
pingIntervalMillis, connectionRetryEnabled);
synchronized (connectionPool) {
if (candidate.successCount == 0) {
return candidate;
}
}
//標記1
if (!candidate.isHealthy(doExtensiveHealthChecks)) {
noNewStreams();
continue;
}
return candidate;
}
}
往下看標記1,這邊會判斷這個連接是否健康(比如Socket沒有關閉、或者它的輸入輸出流沒有關閉等等),如果不健康就調用noNewStreams方法從連接池中取出並銷燬,接着調用continue,繼續循環調用findConnection方法獲取RealConnection對象。
通過不停的循環調用findConnection方法來獲取RealConnection對象,接着看這個findConnection方法做了哪些操作。
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
...
RealConnection result = null;
Connection releasedConnection;
...
synchronized (connectionPool) {
...
//標記1
releasedConnection = this.connection;
...
if (this.connection != null) {
result = this.connection;
releasedConnection = null;
}
...
}
...
return result;
}
在findConnection方法的標記1處,嘗試將connection賦值給releasedConnection,然後判斷這個connection能不能複用,如果能複用,就將connection賦值給result,最後返回這個複用的連接。如果不能複用,那麼result就爲null,我們繼續往下看。
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
...
RealConnection result = null;
Connection releasedConnection;
...
synchronized (connectionPool) {
...
//標記1
...
//標記2
if (result == null) {
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
...
}
...
return result;
}
如果result爲null說明不能複用這個connection,那麼就從連接池connectionPool中獲取一個實際的RealConnection並賦值給connection,接着判斷connection是否爲空,不爲空賦值給result。
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
...
RealConnection result = null;
Connection releasedConnection;
...
synchronized (connectionPool) {
...
//標記1
...
//標記2
if (result == null) {
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
...
}
...
//標記3
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
...
return result;
}
標記3處,拿到我們的RealConnection對象result之後,調用它的connect方法來進行實際的網絡連接。
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
...
RealConnection result = null;
Connection releasedConnection;
...
synchronized (connectionPool) {
...
//標記1
...
//標記2
if (result == null) {
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
...
}
...
//標記3
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
...
//標記4
Internal.instance.put(connectionPool, result);
...
return result;
}
在標記4處,進行真正的網絡連接後,將連接成功後的RealConnection對象result放入connectionPool連接池,方便後面複用。
上面我們介紹了StreamAllocation對象的newStream方法的具體操作,接下來看看ConnectInterceptor攔截器中一個很重要的概念-連接池。
不管HTTP協議是1.1還是2.0,它們的Keep-Alive機制,或者2.0的多路複用機制在實現上都需要引入一個連接池的概念,來維護整個網絡連接。OkHttp中將客戶端與服務端之間的鏈接抽象成一個Connection類,而RealConnection是它的實現類,爲了管理所有的Connection,OkHttp提供了一個ConnectionPool這個類,它的主要作用就是在時間範圍內複用Connection。
接下來主要介紹它的get和put方法。
@Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection, true);
return connection;
}
}
return null;
}
在get方法中遍歷連接池中的Connection,通過isEligible方法判斷Connection是否可用,如果可以使用就會調用streamAllocation的acquire方法來獲取所用的連接。
進入StreamAllocation的acquire方法:
public void acquire(RealConnection connection, boolean reportedAcquired) {
assert (Thread.holdsLock(connectionPool));
if (this.connection != null) throw new IllegalStateException();
//標記1
this.connection = connection;
this.reportedAcquired = reportedAcquired;
//標記2
connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}
標記1處,從連接池中獲取的RealConnection對象賦值給StreamAllocation的成員變量connection。
標記2處,將StreamAllocation對象的弱引用添加到RealConnection的allocations集合中去,這樣做的用處是通過allocations集合的大小來判斷網絡連接次數是否超過OkHttp指定的連接次數。
put方法:
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
put方法中在添加連接到連接池之前,會處理清理任務,做完清理任務後,將我們的connection添加到連接池中。
connection自動回收l利用了GC的回收算法,當StreamAllocation數量爲0時,會被線程池檢測到,然後進行回收,在ConnectionPool中有一個獨立的線程,它會開啓cleanupRunnable來清理連接池。
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
//標記1
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
try {
//標記2
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
在run方法中是一個死循環,內部標記1處首次進行清理時,需要返回下次清理的間隔時間。標記2處調用了wait方法進行等待,等待釋放鎖和時間片,當等待時間過了之後會再次調用Runnable進行清理,同時返回下次要清理的間隔時間waitNanos。
標記2處的cleanup方法內部實現了具體的GC回收算法,該算法類似Java GC當中的標記清除算法;cleanup方法循環標記出最不活躍的connection,通過響應的判斷來進行清理。
CallServerInterceptor攔截器
CallServerInterceptor攔截器主要作用是負責向服務器發起真正的網絡請求,並獲取返回結果。
CallServerInterceptor的intercept方法:
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
HttpCodec httpCodec = realChain.httpStream();
StreamAllocation streamAllocation = realChain.streamAllocation();
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();
...
}
intercept方法中先是獲取五個對象,下面分別介紹這5個對象的含義。
RealInterceptorChain:攔截器鏈,真正進行請求的地方。
HttpCodec:在OkHttp中,它把所有的流對象都封裝成了HttpCodec這個類,作用是編碼Request,解碼Response。
StreamAllocation:建立HTTP連接所需要的網絡組件。
RealConnection:服務器與客戶端的具體連接。
Request:網絡請求。
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
HttpCodec httpCodec = realChain.httpStream();
StreamAllocation streamAllocation = realChain.streamAllocation();
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();
...
//標記1
httpCodec.finishRequest();
...
}
標記1處,調用httpCodec的finishRequest方法,表面網絡請求的寫入工作已經完成,具體網絡請求的寫入工作大家可以看源碼,也就是標記1之上的代碼。
網絡請求的寫入工作完成後,接下來就進行網絡請求的讀取工作。
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
HttpCodec httpCodec = realChain.httpStream();
StreamAllocation streamAllocation = realChain.streamAllocation();
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();
...
//網絡請求一系列寫入工作
...
//向socket當中寫入請求的body信息
request.body().writeTo(bufferedRequestBody);
...
//標記1:寫入結束
httpCodec.finishRequest();
...
if (responseBuilder == null) {
realChain.eventListener().responseHeadersStart(realChain.call());
//讀取網絡寫入的頭部信息
responseBuilder = httpCodec.readResponseHeaders(false);
}
Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
...
//讀取Response
//標記1
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
...
return response;
}
我們只取核心代碼標記1,通過httpCodec的openResponseBody方法獲取body,並通過build創建Response對象,最終返回Response對象。
到這裏OkHttp的同步和異步請求、分發器,以及五個攔截器都已經介紹一邊了,怎麼說呢,OkHttp的源碼實在太龐大了,要想全部理解需要花費很長時間,我只是整理出了OkHttp中幾個比較重要的概念,瞭解它的整體脈絡,這樣你纔能有條理的分析它的源碼。
搜索微信“顧林海”公衆號,定期推送優質文章。