前言:okhttp框架已經(jīng)使用了好多年了,本文基于3.12.13版本源碼分析,個人覺得里面的設計確實非常巧妙。目前官網(wǎng)最新已經(jīng)升級到4.12版本:https://github.com/square/okhttp 有興趣可以看下 能完美兼容3.x版本,主要變更為kt實現(xiàn)。
okhttp從設計上亮點
1.大量運用設計模式,調(diào)用方便可擴展性強
2.分發(fā)器實現(xiàn)比較巧妙,異步請求設計保證了高并發(fā)最大吞吐量
3.攔截器使用責任鏈模式,保證了各個功能模塊職責單一,暴露API方便用戶擴展
4.連接池實現(xiàn)也保證了整體框架鏈接的高性能和資源開銷
最簡單的使用
只需要下面四個步驟就可以發(fā)起網(wǎng)絡請求,但是真正去閱讀他源碼實現(xiàn) 會發(fā)現(xiàn)里面的很多細節(jié)很給力。
//建造者模式創(chuàng)建client
private OkHttpClient mOkHttpClient =mOkHttpClient.newBuilder()
.connectTimeout()
.readTimeout()
.writeTimeout()
.build();
//建造者模式創(chuàng)建request
Request.Builder builder = new Request.Builder().url(requestUrl);
builder.addHeader()
Request request = builder.build();
//把request給到 client
Call call = mOkHttpClient.newCall(request);
//執(zhí)行移步請求
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
client 和request都是用了建造者模式,方便用戶自定義擴展。思路也比較清晰,構(gòu)建請求然后給到okcliet對象,由對象調(diào)用newCall()返回一個Call對象,在調(diào)用Call對象的異步方法發(fā)起請求得到結(jié)果回調(diào)。Call對象是一個接口,里面定義同步、異步、是否取消、超時等方法,只有一個實現(xiàn)類:RealCall
執(zhí)行請求真正的實現(xiàn)類RealCall
上面的newCall方法調(diào)用如下,表明非websocket請求
@Override public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false /* for web socket */);
}
調(diào)用異步請求方法:
@Override public void enqueue(Callback responseCallback) {
// 加同步限制保證同一個請求不會執(zhí)行多次
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
//可以看到這個標記沒有設置false的地方,每次請求發(fā)出去就不管了這個標記主要保證沒執(zhí)行
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
// 核心方法
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
核心方法: client.dispatcher().enqueue(new AsyncCall(responseCallback)); 調(diào)用到Dispatcher類。這里用到了門面模式,Call里面的邏輯全部指向了這個分發(fā)器,里面設計也非常精妙。new AsyncCall(responseCallback) AsyncCall其實就是一個Runnable
public final class Dispatcher {
private int maxRequests = 64; // 最大請求數(shù)不能超過64個
private int maxRequestsPerHost = 5; //同一個host連接數(shù)不超過5
private @Nullable Runnable idleCallback;
/** Executes calls. Created lazily. */
private @Nullable ExecutorService executorService;
/** Ready async calls in the order they'll be run. */
// 等待被執(zhí)行的請求雙端隊列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
// 正在執(zhí)行的請求對列異步
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
/** Running synchronous calls. Includes canceled calls that haven't finished yet. */
// 正在執(zhí)行的請求對列同步
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
public Dispatcher(ExecutorService executorService) {
this.executorService = executorService;
}
public Dispatcher() {
}
}
從上面代碼分析,成員變量就定義了請求最大數(shù)量限制64,同一個host連接數(shù)限制5,里面定義了三個雙端隊列結(jié)構(gòu),兩個隊列用作異步請求,一個用作同步請求。還定義線程池,線程池的參數(shù)就比較有意思,能保證高并發(fā)下最大吞吐量。一個個來解釋下
1.執(zhí)行enqueue()時會調(diào)用如下邏輯,先把call加到等待隊列,在執(zhí)行promoteAndExcute方法
void enqueue(AsyncCall call) {
synchronized (this) {
readyAsyncCalls.add(call);
}
promoteAndExecute();
}
promoteAndExecute方法實現(xiàn)如下,核心邏輯添加了注釋
private boolean promoteAndExecute() {
List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next(); //獲取當前請求call
//正在運行的請求數(shù)量如果超過64 就break
if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
// 當前請求host連接數(shù)量如果超過5個則不執(zhí)行后續(xù)邏輯
if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity.
// 把當前call移除等待隊列
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;
}
當滿足條件后,會調(diào)用executeOn方法執(zhí)行,傳遞的參數(shù)很重要,參數(shù)是個線程池對象,為啥說這樣定義就可以保證高并發(fā) 最大吞吐量。
//同步線程池定義
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;
}
核心線程數(shù)0,最大線程數(shù)最大integer,存活時間60秒,隊列是SynchronousQueue?;氐骄€程池基礎。
1.當一個線程執(zhí)行提交任務時,首先判斷是否超過核心線程數(shù),如果不超過,會執(zhí)行任務,如果超過最大核心數(shù),則添加到隊列,
2.添加到隊列中如果如果隊列滿了添加不進去了,則會判斷是否超過最大線程數(shù),如果沒超過創(chuàng)建線程執(zhí)行任務,如果超過執(zhí)行拒絕策略
在看ok的設置,第一個提交發(fā)現(xiàn)核心線程為0,則把任務添加到隊列,而SynchronousQueue代表不存儲的隊列,也就會添加失敗,添加失敗后判斷正在運行的線程數(shù)量 是否超過最大線程數(shù),最大為integer最大值,不超過則創(chuàng)建線程執(zhí)行任務。每提交一次就會創(chuàng)建一個線程執(zhí)行。
這個不會有問題嗎?擴展一個線程池的知識點
阿里開發(fā)手冊為什么禁止使用線程池創(chuàng)建方式
newFixedThreadPool、newSingleThreadExecutor 方法創(chuàng)建線程
傳入的是一個無界的阻塞隊列,理論上可以無限添加任務到隊列中 很可能觸發(fā)OOM。
阻塞隊列有還有ArrayBlockingQueue,如果是這種隊列,需要指定大小,假如傳入一個固定數(shù)字1,表明只能往里面添加1個,如果任務執(zhí)行比較耗時,添加了一個任務到隊列,再來一個任務,發(fā)現(xiàn)沒有超過最大線程數(shù),則會創(chuàng)建一個線程執(zhí)行,這就導致先提交的任務后執(zhí)行。LinkedBlockingQueue還可以傳入數(shù)字就和ArrayBlockingQueue一樣了。
CachedThreadPool 和ok使用的一樣
任務會不斷被提交,當核心線程耗時很久,線程池會嘗試創(chuàng)建新的線程來執(zhí)行提交的任務,當內(nèi)存不足時就會報無法創(chuàng)建線程的錯誤而且安卓廠商一般還有限制,之前在做內(nèi)存優(yōu)化時候驗證華為手機有限制 mate20pro手機上超過500多個就可能出現(xiàn)OOM
okhttp實現(xiàn)則用到了最大吞吐量,配合兩個隊列限制最大請求數(shù)64和同個host最大連接數(shù)不超過5,來控制來保證不會一直創(chuàng)建線程
我們繼續(xù)看任務執(zhí)行方法:
void executeOn(ExecutorService executorService) {
// 把任務給到線程池執(zhí)行
try {
executorService.execute(this);
success = true;
} catch (RejectedExecutionException e) {
.....
} finally {
// 不管任務是否執(zhí)行成功,都執(zhí)行分發(fā)起的finish方法
if (!success) {
client.dispatcher().finished(this); // This call is no longer running!
}
}
不管任務是否執(zhí)行成功,都執(zhí)行分發(fā)起的finish方法
private <T> void finished(Deque<T> calls, T call) {
...
boolean isRunning = promoteAndExecute();
...
}
可以看到都會執(zhí)行promoteAndExecute() 這樣繼續(xù)保證從等待隊列里面去取任務給到運行中隊列執(zhí)行的機制。有沒有感覺有點類似handler機制里面取消息。而把任務給到線程池調(diào)度會執(zhí)行到runnable的run方法,run方法會執(zhí)行RealCall的excute方法,
@Override protected void execute() {
try {
// 交給攔截器 請求并返回數(shù)據(jù) 再回調(diào)
Response response = getResponseWithInterceptorChain();
signalledCallback = true;
responseCallback.onResponse(RealCall.this, response);
} catch (IOException e) {
eventListener.callFailed(RealCall.this, e);
responseCallback.onFailure(RealCall.this, e);
} catch (Throwable t) {
cancel();
if (!signalledCallback) {
IOException canceledException = new IOException("canceled due to " + t);
responseCallback.onFailure(RealCall.this, canceledException);
}
throw t;
} finally {
client.dispatcher().finished(this);
}
}
攔截器
上面的分析是整個分發(fā)器的實現(xiàn)。這里就到了攔截器的實現(xiàn)了,攔截器用到了責任鏈模式。里面源碼如下:
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
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));//添加socket連接攔截器
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors()); // 如果不是websockect 添加網(wǎng)絡攔截器
}
interceptors.add(new CallServerInterceptor(forWebSocket)); // 添加與服務器通信協(xié)議
// 傳入攔截器列表,返回攔截器鏈條
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
// 調(diào)用鏈條的proceed方法返回response
Response response = chain.proceed(originalRequest);
if (retryAndFollowUpInterceptor.isCanceled()) {
closeQuietly(response);
throw new IOException("Canceled");
}
return response;
}
核心代碼都加了注釋。里面實現(xiàn)是通過index+1,不斷取出下一個攔截器,并調(diào)用intercept方法。從鏈表頭部到尾部的攔截器執(zhí)行,等尾部攔截器返回response再一層一層返回到頭部的攔截器。
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
由上面源碼可以知道。第一個添加的是用戶自定義的攔截器列表到鏈條列表里。如果在里面做一些邏輯,可能會影響后續(xù)添加攔截器執(zhí)行。如果是非websockect 還會添加一個網(wǎng)絡攔截器,這兩個區(qū)別就是網(wǎng)絡攔截器不一定執(zhí)行,而用戶自定義的會執(zhí)行。網(wǎng)絡的最先拿到response。分別介紹下各個攔截器作用
重試重定向:
主要調(diào)用recover方法判斷是否能重試、重定向
判斷client是否設置過不允許重試,請求發(fā)送出沒請求體 是否是重試類型,比如證書失敗 這種就不會給重試了,網(wǎng)絡波動這種會重試
判斷重定向邏輯也是先校驗用戶是否允許,匹配301 302 303這種取 頭部的“l(fā)ocation”繼續(xù)重定向。最大重定向次數(shù)20次,參考了瀏覽器的實現(xiàn)
橋接攔截器:
這個攔截器主要是往頭部添加壓縮方式、內(nèi)容長度、編碼 cookie 添加UA 等處理頭部信息
緩存攔截器:
這個攔截器的實現(xiàn)比較復雜,主要總結(jié)起來就是根據(jù)請求頭、響應頭決定當前的請求是否需要請求數(shù)據(jù)還是直接返回緩存,里面用到DiskLrucache策略,用戶可以自定義緩存策略
連接攔截器:
RealConnection封裝了socket和一個socket連接池 主要就是獲取與目標服務器的連接,在連接上數(shù)據(jù)通信
關鍵代碼在ConnectionPool中 最多保存 5個 處于空閑狀態(tài)的連接,且連接的默認?;顣r間為 5分鐘。 加一個connection 執(zhí)行while (true) 執(zhí)行clean方法,如果多了或者有超過5分鐘異常最早的連接
主要工作就是負責建立 TCP 連接,建立 TCP 連接需要經(jīng)歷三次握手四次揮手等操作,如果每個 HTTP 請求都要新建一個 TCP 消耗資源比較多 而 Http1.1 已經(jīng)支持 keep-alive ,即多個 Http 請求復用一個 TCP 連接,OKHttp 也做了相應的優(yōu)化 這里用到享元模式
請求服務攔截器:
主要完成請求報文的封裝與解析
框架常見問題總結(jié)
答案都在上面源碼分析里面解釋了,后面再繼續(xù)補充下問題
1.添加網(wǎng)絡攔截器和添加攔截器誰先執(zhí)行 有什么區(qū)別,為什么
2.okhttp的連接池問題、攔截器、分發(fā)器核心邏輯是什么
連接池出現(xiàn)的背景:
頻繁的進行建立Sokcet連接和斷開Socket是非常消耗網(wǎng)絡資源和浪費時間的,所以HTTP中的keepalive連接對于降低延遲和提升速度有非常重要的作用。keepalive機制是什么呢?也就是可以在一次TCP連接中可以持續(xù)發(fā)送多份數(shù)據(jù)而不會斷開連接。
ConectionPool中維護了一個雙端隊列Deque 發(fā)起請求就會從當前連接池中get socket,如果沒有就會創(chuàng)建并加進去,加進去就會觸發(fā)清理 并返回下次清理的時間, 連接池最大可以存在5個連接 保留5分鐘。