今天來說一說最近比較流行的一個網絡請求庫OKHttp,從Android4.4開始,系統內置了OKHttp,可見OKHttp功能的強大。
1.OkHttp的基本用法
1.1 使用前的準備
首先配置gradle,如下所示:
implementation ‘com.squareup.okhttp3:okhttp:3.12.0’
不要忘了在manifest中添加網絡權限.
1.2 異步GET請求
最簡單的GET請求,請求我的CSDN博客地址,代碼如下所示:
Request.Builder requestBuilder = new Request.Builder().url("https://blog.csdn.net/walkeryudev");
requestBuilder.method("GET", null);
Request request = requestBuilder.build();
OkHttpClient mOkHttpClient = new OkHttpClient();
Call mcall = mOkHttpClient.newCall(request);
mcall.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String str = response.body().string();
Log.d(Tag, str);
//通過handler或者runOnUiThread方式切換線程.
runOnUiThread(new Runnable() {
@Override
public void run() {
tv_text_content.setText("sdfasddsf");
}
});
/*
*如果獲取的是文件/圖片
*/
//1:通過獲取流
//InputStream inputStream = response.body().byteStream();
//2:通過獲取字節數組
//byte[] bytes = response.body().bytes();
}
});
注意:CallBack的回調方法是在子線程執行的,如果直接更新UI會報ndroid.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
所以,更新UI需要通過runOnUiThread或者handler切換到主線程
1.3 異步POST請求
OkHttp3異步請求和OkHttp2有一些差別,就是沒有FormEncodingBuilder這個類,替代它的是功能更強大的FromBody。代碼如下:
String url = "http://ip.taobao.com/service/getIpInfo.php";
RequestBody formBody = new FormBody.Builder().
add("appkey","b59bc3eref6191etrb9f747dd4e83c99f2aer")
.add("appid","353453453453")
.build();
Request request = new Request.Builder().url(url).post(formBody).build();
OkHttpClient mOkHttpClient = new OkHttpClient();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new okhttp3.Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String str = response.body().string();
Log.d(Tag, str);
}
});
這個與異步GET請求類似,只是多了FormBody來封裝請求的參數,並傳遞給Request。
1.4異步上傳文件
上傳文件本身也是一個POST請求,首先定義上傳文件的類型:
public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown;charset=utf-8");
在SD卡根目錄下創建一個test.md文件,裏面的內容爲"123457889"
String filepath = "";
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
filepath=Environment.getExternalStorageDirectory().getAbsolutePath()
}else{
return;
}
File file = new File(filepath, "test.md");
OkHttpClient mOkHttpClient = new OkHttpClient();
Request request = new Request.Builder().url("https://165.254.23.14/markdown").build();
mOkHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.d(TAG, response.body().string());
}
});
1.5 異步下載文件
下載一張圖片,得到Response後將流寫入我們指定的圖片文件中,代碼如下所示:
String url = "http://img.my.csdn.net/uploads/201812/12/5646875615_3453.jpg";
final Request request = new Request.Builder().url(url).build();
OkHttpClient mOkHttpClient = new OkHttpClient();
mOkHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
InputStream inputStream = response.body().byteStream();
FileOutputStream fileOutputStream = null;
String filepath = "";
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
filepath = Environment.getExternalStorageDirectory().getAbsolutePath();
}else{
filepath = getFilesDir().getAbsolutePath();
}
File file = new File(filepath, "test.jpg");
if(null!=file){
fileOutputStream = new FileOutputStream(file);
byte[] buffer = new byte[2048];
int len = 0;
while ((len=inputStream.read(buffer,0,len))!=-1){
fileOutputStream.write(buffer,0,len);
}
fileOutputStream.flush();
}
}
});
2、源碼解析OkHttp
下面我們就開始對 OkHttp 的請求過程進行源碼分析(源碼版本:3.12.0)
2.1 OkHttp的請求網絡流程
(1)從請求處理開始分析
當我們要請求網絡的時候需要用OkHttp.newCall(request)進行execute或者enqueue操作;當調用newCall方法時,會調用如下代碼:
@Override
public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false /* for web socket */);
}
然後,RealCall 調用內部的靜態方法 newRealCall 在其中創建一個 RealCall 實例並將其返回:
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
// Safely publish the Call instance to the EventListener.
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}
然後,當返回了 RealCall 之後,我們又會調用它的 execute() 方法來獲取響應結果,下面是這個方法的定義:
@Override
public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
timeout.enter();
eventListener.callStart(this);
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
e = timeoutExit(e);
eventListener.callFailed(this, e);
throw e;
} finally {
client.dispatcher().finished(this);
}
}
這裏我們會用 client 對象(實際也就是上面創建 RealCall 的時候傳入的 OkHttpClient)的 dispatcher() 方法來獲取一個 Dispatcher 對象,並調用它的 executed() 方法來將當前的 RealCall 加入到一個雙端隊列中,下面是 executed(RealCall) 方法的定義,這裏的 runningSyncCalls 的類型是 Deque:
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
讓我們回到上面的 execute() 方法,在把 RealCall 加入到雙端隊列之後,我們又調用了 getResponseWithInterceptorChain() 方法,下面就是該方法的定義。
Response getResponseWithInterceptorChain() throws IOException {
// 添加一些列攔截器,注意添加的順序
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
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));
//構建一條責任鏈
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());//A1
//處理責任鏈
return chain.proceed(originalRequest);
}
這裏,我們創建了一個列表對象之後把 client 中的攔截器、重連攔截器、橋攔截器、緩存攔截器、網絡連接攔截器和服務器請求攔截器等依次加入到列表中。然後,我們用這個列表創建了一個攔截器鏈。這裏使用了責任鏈設計模式,每當一個攔截器執行完畢之後會調用下一個攔截器或者不調用並返回結果。顯然,我們最終拿到的響應就是這個鏈條執行之後返回的結果。當我們自定義一個攔截器的時候,也會被加入到這個攔截器鏈條裏。
這裏我們遇到了很多的新類,比如 RealCall、Dispatcher 以及責任鏈等。下文中,我們會對這些類之間的關係以及責任鏈中的環節做一個分析,而這裏我們先對整個請求的流程做一個大致的梳理。下面是這個過程大致的時序圖:
實際返回的是一個RealCall類。我們調用enqueue異步請求網絡實際上是調用了RealCall的enqueue方法。查看RealCall的enqueue方法,如下所示:
@Override
public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
我們可以看到最終的請求是dispatcher來完成的,接下來就開始分析dispatcher。
(2)分發器Dispatcher的任務調度
上面我們提到了 Dispatcher 這個類,它的作用是對請求進行分發。以最開始的示例代碼爲例,在使用 OkHttp 的時候,我們會創建一個 RealCall 並將其加入到雙端隊列中。但是請注意這裏的雙端隊列的名稱是 runningSyncCalls,也就是說這種請求是同步請求,會在當前的線程中立即被執行。所以,下面的 getResponseWithInterceptorChain() 就是這個同步的執行過程。而當我們執行完畢的時候,又會調用 Dispatcher 的 finished(RealCall) 方法把該請求從隊列中移除。所以,這種同步的請求無法體現分發器的“分發”功能。
除了同步的請求,還有異步類型的請求:當我們拿到了 RealCall 的時候,調用它的 enqueue(Callback responseCallback) 方法並設置一個回調即可。該方法會執行下面這行代碼:
client.dispatcher().enqueue(new AsyncCall(responseCallback));
即使用上面的回調創建一個 AsyncCall 並調用 enqueue(AsyncCall)。這裏的 AsyncCall 間接繼承自 Runnable,是一個可執行的對象,並且會在 Runnable 的 run() 方法裏面調用 AsyncCall 的 execute() 方法。AsyncCall 的 execute() 方法與 RealCall 的 execute() 方法類似,都使用責任鏈來完成一個網絡請求。只是後者可以放在一個異步的線程中進行執行。
當我們調用了 Dispatcher 的 enqueue(AsyncCall) 方法的時候也會將 AsyncCall 加入到一個隊列中,並會在請求執行完畢的時候從該隊列中移除,只是這裏的隊列是 runningAsyncCalls 或者 readyAsyncCalls。它們都是一個雙端隊列,並用來存儲異步類型的請求。它們的區別是,runningAsyncCalls 是正在執行的隊列,當正在執行的隊列達到了限制的時候,就會將其放置到就緒隊列 readyAsyncCalls 中:
void enqueue(AsyncCall call) {
synchronized (this) {
readyAsyncCalls.add(call);
}
promoteAndExecute();
}
/**
* Promotes eligible calls from {@link #readyAsyncCalls} to {@link #runningAsyncCalls} and runs
* them on the executor service. Must not be called with synchronization because executing calls
* can call into user code.
*
* @return true if the dispatcher is currently running calls.
*/
private boolean promoteAndExecute() {
assert (!Thread.holdsLock(this));
List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next();
if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity.
i.remove();
executableCalls.add(asyncCall);
runningAsyncCalls.add(asyncCall);
}
isRunning = runningCallsCount() > 0;
}
for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
asyncCall.executeOn(executorService());
}
return isRunning;
}
當把該請求加入到了正在執行的隊列之後,我們會立即使用一個線程池來執行該 AsyncCall。這樣這個請求的責任鏈就會在一個線程池當中被異步地執行了。這裏的線程池由 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;
}
顯然,當線程池不存在的時候會去創建一個線程池。除了上面的這種方式,我們還可以在構建 OkHttpClient 的時候,自定義一個 Dispacher,並在其構造方法中爲其指定一個線程池。下面我們類比 OkHttp 的同步請求繪製了一個異步請求的時序圖。你可以通過將兩個圖對比來了解兩種實現方式的不同:
以上就是分發器 Dispacher 的邏輯,看上去並沒有那麼複雜。並且從上面的分析中,我們可以看出實際請求的執行過程並不是在這裏完成的,這裏只能決定在哪個線程當中執行請求並把請求用雙端隊列緩存下來,而實際的請求執行過程是在責任鏈中完成的。下面我們就來分析一下 OkHttp 裏的責任鏈的執行過程。
Dispatcher主要用於控制併發的請求,它主要維護了以下變量:
//最大併發請求數
private int maxRequests = 64;
//每個主機的最大請求數
private int maxRequestsPerHost = 5;
private @Nullable Runnable idleCallback;
/** 消費者線程池. */
private @Nullable ExecutorService executorService;
/** 將要運行的異步請求隊列 */
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
/** 正在請求的異步請求隊列 */
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
/** 正在請求的同步請求隊列 */
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
Dispatcher有兩個構造方法,可以使用自己設定的線程池。如果沒有設定線程池,則會在請求網絡前創建默認的線程池。這個線程池類似於CachedThreadPool,比較適合執行大量的耗時比較少的任務。前面講過,當調用RealCall的enqueue方法時,實際上是調用了Dispatcher的enqueue方法,它的代碼如下所示:
void enqueue(AsyncCall call) {
synchronized (this) {
readyAsyncCalls.add(call);
}
promoteAndExecute();
}
我們看到這裏用了同步,將這個請求加入了即將進入的請求隊列。之後調用了promoteAndExecute方法。我們接着看promoteAndExecute方法裏都做了什麼
private boolean promoteAndExecute() {
assert (!Thread.holdsLock(this));
List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next();
// 如果中的runningAsynCalls不滿,且call佔用的host小於最大數量,則將call加入到runningAsyncCalls中執行,
// 同時利用線程池執行call;否者將call加入到readyAsyncCalls中。
if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity.
i.remove();
executableCalls.add(asyncCall);
runningAsyncCalls.add(asyncCall);
}
isRunning = runningCallsCount() > 0;
}
for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
asyncCall.executeOn(executorService());
}
return isRunning;
}
下面看看AsynCall的源碼
final class AsyncCall extends NamedRunnable {
private final Callback responseCallback;
AsyncCall(Callback responseCallback) {
super("OkHttp %s", redactedUrl());
this.responseCallback = responseCallback;
}
String host() {
return originalRequest.url().host();
}
Request request() {
return originalRequest;
}
RealCall get() {
return RealCall.this;
}
/**
* Attempt to enqueue this async call on {@code executorService}. This will attempt to clean up
* if the executor has been shut down by reporting the call as failed.
*/
void executeOn(ExecutorService executorService) {
assert (!Thread.holdsLock(client.dispatcher()));
boolean success = false;
try {
executorService.execute(this);
success = true;
} catch (RejectedExecutionException e) {
InterruptedIOException ioException = new InterruptedIOException("executor rejected");
ioException.initCause(e);
eventListener.callFailed(RealCall.this, ioException);
responseCallback.onFailure(RealCall.this, ioException);
} finally {
if (!success) {
client.dispatcher().finished(this); // This call is no longer running!
}
}
}
@Override protected void execute() {
boolean signalledCallback = false;
timeout.enter();
try {
//和同步執行一樣,最後都會調到這裏
Response response = getResponseWithInterceptorChain();
if (retryAndFollowUpInterceptor.isCanceled()) {
signalledCallback = true;
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
e = timeoutExit(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 {
client.dispatcher().finished(this);
}
}
}
從上面的源碼可以看出,攔截鏈的處理過程中,OkHttp默認幫我們做了五步處理,其中RetryAndFollowUpInterceptor、BridgeInterceptor、CallServerInterceptor的源碼比較簡潔易懂,下面將對OKHttp最爲核心的兩部分緩存處理和連接處理(連接池)進行講解。
2.2 責任鏈的執行過程
在典型的責任鏈設計模式裏,很多對象由每一個對象對其下級的引用而連接起來形成一條鏈。請求在這個鏈上傳遞,直到鏈上的某一個對象決定處理此請求。發出這個請求的客戶端並不知道鏈上的哪一個對象最終處理這個請求,這使得系統可以在不影響客戶端的情況下動態地重新組織和分配責任。責任鏈在現實生活中的一種場景就是面試,當某輪面試官覺得你沒有資格進入下一輪的時候可以否定你,不然會讓下一輪的面試官繼續面試。
在 OkHttp 裏面,責任鏈的執行模式與之稍有不同。這裏我們主要來分析一下在 OkHttp 裏面,責任鏈是如何執行的,至於每個鏈條裏面的具體邏輯,我們會在隨後一一說明。
回到 (1)的註釋A1處 的代碼,有兩個地方需要我們注意:
1、是當創建一個責任鏈 RealInterceptorChain 的時候,我們傳入的第 5 個參數是 0。該參數名爲 index,會被賦值給 RealInterceptorChain 實例內部的同名全局變量。
2、當啓用責任鏈的時候,會調用它的 proceed(Request) 方法。
下面是 proceed(Request) 方法的定義:
@Override public Response proceed(Request request) throws IOException {
return proceed(request, streamAllocation, httpCodec, connection);
}
這裏又調用了內部的重載的 proceed() 方法。下面我們對該方法進行了簡化:
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
.
// ...
//調用責任鏈的下一個攔截器
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
//...
return response;
}
注意到這裏使用責任鏈進行處理的時候,會新建下一個責任鏈並把 index+! 作爲下一個責任鏈的 index。然後,我們使用 index 從攔截器列表中取出一個攔截器,調用它的 intercept() 方法,並把下一個執行鏈作爲參數傳遞進去。
這樣,當下一個攔截器希望自己的下一級繼續處理這個請求的時候,可以調用傳入的責任鏈的 proceed() 方法;如果自己處理完畢之後,下一級不需要繼續處理,那麼就直接返回一個 Response 實例即可。因爲,每次都是在當前的 index 基礎上面加 1,所以能在調用 proceed() 的時候準確地從攔截器列表中取出下一個攔截器進行處理。
我們還要注意的地方是之前提到過重試攔截器,這種攔截器會在內部啓動一個 while 循環,並在循環體中調用執行鏈的 proceed() 方法來實現請求的不斷重試。這是因爲在它那裏的攔截器鏈的 index 是固定的,所以能夠每次調用 proceed() 的時候,都能夠從自己的下一級執行一遍鏈條。下面就是這個責任鏈的執行過程:
清楚了 OkHttp 的攔截器鏈的執行過程之後,我們來看一下各個攔截器做了什麼邏輯。
2.3 重試和重定向:RetryAndFollowUpInterceptor
RetryAndFollowUpInterceptor 主要用來當請求失敗的時候進行重試,以及在需要的情況下進行重定向。我們上面說,責任鏈會在進行處理的時候調用第一個攔截器的 intercept() 方法。如果我們在創建 OkHttp 客戶端的時候沒有加入自定義攔截器,那麼
RetryAndFollowUpInterceptor 就是我們的責任鏈中最先被調用的攔截器。
@Override public Response intercept(Chain chain) throws IOException {
//...
//注意這裏我們初始化了一個 StreamAllocation 並賦值給全局變量,它的作用我們後面會提到
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
// 用來記錄重定向的次數
int followUpCount = 0;
Response priorResponse = null;
while (true) {
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
Response response;
boolean releaseConnection = true;
try {
// 這裏從當前的責任鏈開始執行一遍責任鏈,是一種重試的邏輯
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
// 調用 recover 方法從失敗中進行恢復,如果可以恢復就返回true,否則返回false
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getFirstConnectException();
}
releaseConnection = false;
continue;
} catch (IOException e) {
// 重試與服務器進行連接
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
// 如果 releaseConnection 爲 true 則表明中間出現了異常,需要釋放資源
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
// // 使用之前的響應 priorResponse 構建一個響應,這種響應的響應體 body 爲空
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}
// 根據得到的響應進行處理,可能會增加一些認證信息、重定向或者處理超時請求
// 如果該請求無法繼續被處理或者出現的錯誤不需要繼續處理,將會返回 null
Request followUp;
try {
followUp = followUpRequest(response, streamAllocation.route());
} catch (IOException e) {
streamAllocation.release();
throw e;
}
// 無法重定向,直接返回之前的響應
if (followUp == null) {
streamAllocation.release();
return response;
}
// 關閉資源
closeQuietly(response.body());
// 達到了重定向的最大次數,就拋出一個異常
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
if (followUp.body() instanceof UnrepeatableRequestBody) {
streamAllocation.release();
throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
}
// 這裏判斷新的請求是否能夠複用之前的連接,如果無法複用,則創建一個新的連接
if (!sameConnection(response, followUp.url())) {
streamAllocation.release();
streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(followUp.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
} else if (streamAllocation.codec() != null) {
throw new IllegalStateException("Closing the body of " + response
+ " didn't close its backing stream. Bad interceptor?");
}
request = followUp;
priorResponse = response;
}
}
以上的代碼主要用來根據錯誤的信息做一些處理,會根據服務器返回的信息判斷這個請求是否可以重定向,或者是否有必要進行重試。如果值得去重試就會新建或者複用之前的連接在下一次循環中進行請求重試,否則就將得到的請求包裝之後返回給用戶。這裏,我們提到了 StreamAllocation 對象,它相當於一個管理類,維護了服務器連接、併發流和請求之間的關係,該類還會初始化一個 Socket 連接對象,獲取輸入/輸出流對象。同時,還要注意這裏我們通過 client.connectionPool() 傳入了一個連接池對象 ConnectionPool。這裏我們只是初始化了這些類,但實際在當前的方法中並沒有真正用到這些類,而是把它們傳遞到下面的攔截器裏來從服務器中獲取請求的響應。稍後,我們會說明這些類的用途,以及之間的關係。
2.4 BridgeInterceptor
橋攔截器 BridgeInterceptor 用於從用戶的請求中構建網絡請求,然後使用該請求訪問網絡,最後從網絡響應當中構建用戶響應。相對來說這個攔截器的邏輯比較簡單,只是用來對請求進行包裝,並將服務器響應轉換成用戶友好的響應:
public final class BridgeInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
// 從用戶請求中獲取網絡請求構建者
Request.Builder requestBuilder = userRequest.newBuilder();
RequestBody body = userRequest.body();
// 執行網絡請求
Response networkResponse = chain.proceed(requestBuilder.build());
// ...
// 從網絡響應中獲取用戶響應構建者
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
// ...
// 返回用戶響應
}
return responseBuilder.build();
}
}
2.5 使用緩存:CacheInterceptor
緩存攔截器會根據請求的信息和緩存的響應的信息來判斷是否存在緩存可用,如果有可以使用的緩存,那麼就返回該緩存該用戶,否則就繼續責任鏈來從服務器中獲取響應。當獲取到響應的時候,又會把響應緩存到磁盤上面。以下是這部分的邏輯:
public final class CacheInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
// 根據請求和緩存的響應中的信息來判斷是否存在緩存可用
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;// 如果該請求沒有使用網絡就爲空
Response cacheResponse = strategy.cacheResponse; // 如果該請求沒有使用緩存就爲空
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// 請求不使用網絡並且不使用緩存,相當於在這裏就攔截了,沒必要交給下一級(網絡請求攔截器)來執行
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();
}
// // 該請求使用緩存,但是不使用網絡:從緩存中拿結果,沒必要交給下一級(網絡請求攔截器)執行
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
// 這裏調用了執行鏈的處理方法,實際就是交給自己的下一級來執行了
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// 這裏當拿到了網絡請求之後調用,下一級執行完畢會交給它繼續執行,如果使用了緩存就把請求結果更新到緩存裏
if (cacheResponse != null) {
// 服務器返回的結果是304,返回緩存中的結果
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();
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
// 更新緩存
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
// 把請求的結果放進緩存裏
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
}
對緩存,這裏我們使用的是全局變量 cache,它是 InternalCache 類型的變量。InternalCache 是一個接口,在 OkHttp 中只有一個實現類 Cache。在 Cache 內部,使用了 DiskLruCache 來將緩存的數據存到磁盤上。DiskLruCache 以及 LruCache 是 Android 上常用的兩種緩存策略。前者是基於磁盤來進行緩存的,後者是基於內存來進行緩存的,它們的核心思想都是 Least Recently Used,即最近最少使用算法。我們會在以後的文章中詳細介紹這兩種緩存框架,也請繼續關注我們的文章。
另外,上面我們根據請求和緩存的響應中的信息來判斷是否存在緩存可用的時候用到了 CacheStrategy 的兩個字段,得到這兩個字段的時候使用了非常多的判斷,其中涉及 Http 緩存相關的知識,感興趣的話可以自己參考源代碼。
2.6連接複用ConnectInterceptor
連接攔截器 ConnectInterceptor 用來打開到指定服務器的網絡連接,並交給下一個攔截器處理。這裏我們只打開了一個網絡連接,但是並沒有發送請求到服務器。從服務器獲取數據的邏輯交給下一級的攔截器來執行。雖然,這裏並沒有真正地從網絡中獲取數據,而僅僅是打開一個連接,但這裏有不少的內容值得我們去關注。因爲在獲取連接對象的時候,使用了連接池 ConnectionPool 來複用連接。
public final class ConnectInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}
這裏的 HttpCodec 用來編碼請求並解碼響應,RealConnection 用來向服務器發起連接。它們會在下一個攔截器中被用來從服務器中獲取響應信息。下一個攔截器的邏輯並不複雜,這裏萬事具備之後,只要它來從服務器中讀取數據即可。可以說,OkHttp 中的核心部分大概就在這裏,所以,我們就先好好分析一下,這裏在創建連接的時候如何藉助連接池來實現連接複用的。
根據上面的代碼,當我們調用 streamAllocation 的 newStream() 方法的時候,最終會經過一系列的判斷到達 StreamAllocation 中的 findConnection() 方法。
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
//...
synchronized (connectionPool) {
//...
//嘗試使用已分配的連接,如果改連接已經給限制創建新的流,就返回一個Socket以關閉連接
releasedConnection = this.connection;
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
// 已分配連接,並且該連接可用
result = this.connection;
releasedConnection = null;
}
if (!reportedAcquired) {
// 如果該連接從未被標記爲獲得,不要標記爲發佈狀態,reportedAcpuired通過acquire()方法修改
releasedConnection = null;
}
if (result == null) {
// 嘗試供連接池中獲取一個連接
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
}
//關閉連接
closeQuietly(toClose);
if (releasedConnection != null) {
eventListener.connectionReleased(call, releasedConnection);
}
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
}
if (result != null) {
// 如果已經從連接池中獲取到了一個連接,就將其返回
return result;
}
// If we need a route selection, make one. This is a blocking operation.
boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
newRouteSelection = true;
routeSelection = routeSelector.next();
}
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
if (newRouteSelection) {
// 根據一系列的IP地址從連接池中獲取一個鏈接
// the pool. This could match due to connection coalescing.
List<Route> routes = routeSelection.getAll();
for (int i = 0, size = routes.size(); i < size; i++) {
Route route = routes.get(i);
//從連接池中獲取一個連接
Internal.instance.get(connectionPool, address, this, route);
if (connection != null) {
foundPooledConnection = true;
result = connection;
this.route = route;
break;
}
}
}
if (!foundPooledConnection) {
if (selectedRoute == null) {
selectedRoute = routeSelection.next();
}
// 創建一個新的連接,並將其分配,這樣我們就可以在握手之前進行終端
route = selectedRoute;
refusedStreamCount = 0;
result = new RealConnection(connectionPool, selectedRoute);
acquire(result, false);
}
}
// 如果我們在第二次的時候發現了一個池連接,那麼我們就將其返回
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}
// 進行 TCP 和 TLS 握手
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
reportedAcquired = true;
// 將該連接放進連接池中
Internal.instance.put(connectionPool, result);
// 如果同時創建了另一個到同一地址的多路複用連接,釋放這個連接並獲取那個連接
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
closeQuietly(socket);
eventListener.connectionAcquired(call, result);
return result;
}
該方法會被放置在一個循環當中被不停地調用以得到一個可用的連接。它優先使用當前已經存在的連接,不然就使用連接池中存在的連接,再不行的話,就創建一個新的連接。所以,上面的代碼大致分成三個部分:
判斷當前的連接是否可以使用:流是否已經被關閉,並且已經被限制創建新的流;
如果當前的連接無法使用,就從連接池中獲取一個連接;
連接池中也沒有發現可用的連接,創建一個新的連接,並進行握手,然後將其放到連接池中。
在從連接池中獲取一個連接的時候,使用了 Internal 的 get() 方法。Internal 有一個靜態的實例,會在 OkHttpClient 的靜態代碼快中被初始化。我們會在 Internal 的 get() 中調用連接池的 get() 方法來得到一個連接。
從上面的代碼中我們也可以看出,實際上,我們使用連接複用的一個好處就是省去了進行 TCP 和 TLS 握手的一個過程。因爲建立連接本身也是需要消耗一些時間的,連接被複用之後可以提升我們網絡訪問的效率。那麼這些連接被放置在連接池之後是如何進行管理的呢?我們會在下文中分析 OkHttp 的 ConnectionPool 中是如何管理這些連接的。
2.7 CallServerInterceptor
服務器請求攔截器 CallServerInterceptor 用來向服務器發起請求並獲取數據。這是整個責任鏈的最後一個攔截器,這裏沒有再繼續調用執行鏈的處理方法,而是把拿到的響應處理之後直接返回給了上一級的攔截器:
public final class CallServerInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
// 獲取 ConnectInterceptor 中初始化的 HttpCodec
HttpCodec httpCodec = realChain.httpStream();
// 獲取 RetryAndFollowUpInterceptor 中初始化的 StreamAllocation
StreamAllocation streamAllocation = realChain.streamAllocation();
// 獲取 ConnectInterceptor 中初始化的 RealConnection
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
// 在這裏寫入請求頭 realChain.eventListener().requestHeadersStart(realChain.call());
httpCodec.writeRequestHeaders(request);
realChain.eventListener().requestHeadersEnd(realChain.call(), request);
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
// Continue" response before transmitting the request body. If we don't get that, return
// what we did get (such as a 4xx response) without ever transmitting the request body.
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(true);
}
// 在這裏寫入請求體
if (responseBuilder == null) {
// Write the request body if the "Expect: 100-continue" expectation was met.
realChain.eventListener().requestBodyStart(realChain.call());
long contentLength = request.body().contentLength();
CountingSink requestBodyOut =
new CountingSink(httpCodec.createRequestBody(request, contentLength));
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
// 寫入請求體
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
realChain.eventListener()
.requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
} else if (!connection.isMultiplexed()) {
// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
// from being reused. Otherwise we're still obligated to transmit the request body to
// leave the connection in a consistent state.
streamAllocation.noNewStreams();
}
}
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();
// 讀取響應體
int code = response.code();
if (code == 100) {
// server sent a 100-continue even though we did not request one.
// try again to read the actual response
responseBuilder = httpCodec.readResponseHeaders(false);
response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
//...
return response;
}
}
2.8 連接管理:ConnectionPool
與請求的緩存類似,OkHttp 的連接池也使用一個雙端隊列來緩存已經創建的連接
private final Deque<RealConnection> connections = new ArrayDeque<>();
OkHttp 的緩存管理分成兩個步驟,一邊當我們創建了一個新的連接的時候,我們要把它放進緩存裏面;另一邊,我們還要來對緩存進行清理。在 ConnectionPool 中,當我們向連接池中緩存一個連接的時候,只要調用雙端隊列的 add() 方法,將其加入到雙端隊列即可,而清理連接緩存的操作則交給線程池來定時執行。
在 ConnectionPool 中存在一個靜態的線程池:
private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
每當我們向連接池中插入一個連接的時候就會調用下面的方法,將連接插入到雙端隊列的同時,會調用上面的線程池來執行清理緩存的任務:
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
// 使用線程池執行清理任務
executor.execute(cleanupRunnable);
}
// 將新建的連接插入到雙端隊列中
connections.add(connection);
}
這裏的清理任務是 cleanupRunnable,是一個 Runnable 類型的實例。它會在方法內部調用 cleanup() 方法來清理無效的連接:
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
下面是 cleanup() 方法:
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
synchronized (this) {
// 遍歷所有的連接
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
// 當前的連接正在使用中
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
idleConnectionCount++;
// 如果找到了一個可以被清理的連接,會嘗試去尋找閒置時間最久的連接來釋放
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// 該連接的時長超出了最大的活躍時長或者閒置的連接數量超出了最大允許的範圍,直接移除
// of the synchronized block).
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// 閒置的連接的數量大於0,停頓指定的時間(等會兒會將其清理掉,現在還不是時候)
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// 所有的連接都在使用中,5分鐘後再清理
return keepAliveDurationNs;
} else {
// 沒有連接
cleanupRunning = false;
return -1;
}
}
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
return 0;
}
在從緩存的連接中取出連接來判斷是否應該將其釋放的時候使用到了兩個變量 maxIdleConnections 和 keepAliveDurationNs,分別表示最大允許的閒置的連接的數量和連接允許存活的最長的時間。默認空閒連接最大數目爲5個,keepalive 時間最長爲5分鐘。
上面的方法會對緩存中的連接進行遍歷,以尋找一個閒置時間最長的連接,然後根據該連接的閒置時長和最大允許的連接數量等參數來決定是否應該清理該連接。同時注意上面的方法的返回值是一個時間,如果閒置時間最長的連接仍然需要一段時間才能被清理的時候,會返回這段時間的時間差,然後會在這段時間之後再次對連接池進行清理。
2.9 小結
以上就是我們對 OkHttp 內部網絡訪問的源碼的分析。當我們發起一個請求的時候會初始化一個 Call 的實例,然後根據同步和異步的不同,分別調用它的 execute() 和 enqueue() 方法。雖然,兩個方法一個會在當前的線程中被立即執行,一個會在線程池當中執行,但是它們進行網絡訪問的邏輯都是一樣的:通過攔截器組成的責任鏈,依次經過重試、橋接、緩存、連接和訪問服務器等過程,來獲取到一個響應並交給用戶。其中,緩存和連接兩部分內容是重點,因爲前者涉及到了一些計算機網絡方面的知識,後者則是 OkHttp 效率和框架的核心。
OkHttp的連接池複用 核心就是用Deque來存儲連接,通過put、get、connectionBecameIdle和evictAll幾個操作對Deque進行操作,另外通過判斷連接中的技術對象StreamAllocation來進行自動回收。
參考資料:
1、OKHttp V3.12.0源碼
2、Android進階之光
3、Andriod 網絡框架 OkHttp 源碼解析