okhttp是目前很火的網絡請求框架,Android4.4開始HttpURLConnection的底層就是採用okhttp實現的,其Github地址:github.com/square/okht…git
來自官方說明:github
OkHttp is an HTTP client that’s efficient by default:設計模式
- HTTP/2 support allows all requests to the same host to share a socket.
- Connection pooling reduces request latency (if HTTP/2 isn’t available).
- Transparent GZIP shrinks download sizes.
- Response caching avoids the network completely for repeat requests.
總結一下,OkHttp支持http2,固然須要你請求的服務端支持才行,針對http1.x,OkHttp採用了鏈接池下降網絡延遲,內部實現gzip透明傳輸,使用者無需關注,支持http協議上的緩存用於避免重複網絡請求。緩存
引入依賴服務器
implementation 'com.squareup.okhttp3:okhttp:3.14.4'
複製代碼
請求網絡cookie
OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder().url("http://mtancode.com/").build();
// 同步方式
Response response = okHttpClient.newCall(request).execute();
// 異步方式
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.i(TAG, "onFailure");
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) {
try {
Log.i(TAG, response.body().string());
} catch (Throwable t) {
t.printStackTrace();
}
}
});
複製代碼
能夠看到,使用起來很是簡單,並且支持同步和異步兩種方式請求網絡。這裏須要注意一下,回調的線程並非UI線程。網絡
同步和異步只是使用方式不一樣,但其原理都是同樣的,最終會走到相同的邏輯,所以這裏就直接從異步方式開始分析了,newCall方法會返回一個RealCall對象,看其enqueue方法:框架
@Override public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
transmitter.callStart();
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
複製代碼
這裏有個Dispatcher,顧名思義它就是專門分發和執行請求的,看它的enqueue方法:異步
void enqueue(AsyncCall call) {
synchronized (this) {
readyAsyncCalls.add(call);
// Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
// the same host.
if (!call.get().forWebSocket) {
AsyncCall existingCall = findExistingCallWithHost(call.host());
if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
}
}
promoteAndExecute();
}
複製代碼
把call添加到readyAsyncCalls列表中,看promoteAndExecute方法:socket
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 (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.
i.remove();
asyncCall.callsPerHost().incrementAndGet();
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;
}
複製代碼
把call搬到runningAsyncCalls中,遍歷列表,對每一個call調用executeOn方法:
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);
transmitter.noMoreExchanges(ioException);
responseCallback.onFailure(RealCall.this, ioException);
} finally {
if (!success) {
client.dispatcher().finished(this); // This call is no longer running!
}
}
}
複製代碼
看RealCall的execute方法:
@Override protected void execute() {
boolean signalledCallback = false;
transmitter.timeoutEnter();
try {
Response response = getResponseWithInterceptorChain();
responseCallback.onResponse(RealCall.this, response);
......
}
複製代碼
來到getResponseWithInterceptorChain方法,該方法內部會執行全部具體的處理邏輯,執行結束後,返回一個最終的response,而後回調給外部傳入的callback,看看getResponseWithInterceptorChain方法:
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(new RetryAndFollowUpInterceptor(client));
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, transmitter, null, 0,
originalRequest, this, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
boolean calledNoMoreExchanges = false;
try {
Response response = chain.proceed(originalRequest);
if (transmitter.isCanceled()) {
closeQuietly(response);
throw new IOException("Canceled");
}
return response;
} catch (IOException e) {
calledNoMoreExchanges = true;
throw transmitter.noMoreExchanges(e);
} finally {
if (!calledNoMoreExchanges) {
transmitter.noMoreExchanges(null);
}
}
}
複製代碼
能夠看到,這裏添加了一系列的攔截器,構成攔截器鏈,請求會沿着這條鏈依次調用其intercept方法,每一個攔截器都作本身該作的工做,最終完成請求,返回最終的response對象。
簡單說下鏈式調用的實現方法:建立一個RealInterceptorChain,傳入全部的interceptors,和當前index(從0開始),而後調用RealInterceptorChain的process方法,該方法裏,獲取到對應的interceptor,而後調用intercept方法,而在intercept方法中,會執行具體的處理邏輯,而後建立一個RealInterceptorChain,傳入全部的interceptors,和當前index+1,繼續調用RealInterceptorChain的process方法,如此重複直到index超過interceptors個數爲止。其實這種實現方式跟Task實現鏈式調用很相似,整個調用過程會建立一系列的中間對象。
繼續回到okhttp,這裏實際上是一種責任鏈設計模式,它的優勢有:
能夠下降邏輯的耦合,相互獨立的邏輯寫到本身的攔截器中,也無需關注其它攔截器所作的事情。
擴展性強,能夠添加新的攔截器。
固然它也有缺點:
對於OkHttp,咱們能夠添加本身的攔截器:
OkHttpClient.Builder builder = new OkHttpClient().newBuilder();
builder.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
// TODO 自定義邏輯
return chain.proceed(chain.request());
}
});
複製代碼
來到這裏,OkHttp的主流程就分析完了,至於具體的緩存邏輯,鏈接池邏輯,網絡請求這些,都是在對應的攔截器裏面實現的,下面對這些攔截器逐個進行分析。
代碼在CacheInterceptor類中,實現HTTP協議的緩存機制,OkHttp默認並不沒有開啓緩存,要本身傳入一個Cache對象。
先了解下HTTP協議的緩存機制:
首先緩存分爲三種:過時時間緩存、第一差別緩存和第二差別緩存,並且在優先級上,過時時間緩存 > 第一差別緩存 > 第二差別緩存。
過時時間緩存,就是經過HTTP響應頭部的字段控制:
expires:響應字段,絕對過時時間,HTTP1.0。
Cache-Control:響應字段,相對過時時間,HTTP1.1。注意若是值爲no-cache,表示跳過過時時間緩存邏輯,值爲no-store表示跳過過時時間緩存邏輯和差別緩存邏輯,也就是不使用緩存數據。
當客戶端請求時,發現緩存未過時,就直接返回緩存數據了,不請求網絡,不然,執行第一差別緩存邏輯:
If-None-Match:請求字段,值爲ETag。
ETag:響應字段,服務端會根據內容生成惟一的字符串。
若是服務端發現If-None-Match的值和當前ETag同樣,就說明數據內容沒有變化,就返回304,不然,執行第二差別緩存邏輯:
If-Modified-Since:請求字段,客戶端告訴服務端本地緩存的資源的上次修改時間。
Last-Modified:響應字段,服務端告訴客戶端資源的最後修改時間。
若是服務端發現If-Modified-Since的值就是資源的最後修改時間,就說明數據內容沒有變化,就返回304,不然,返回全部資源數據給客戶端,響應碼爲200。
回到OkHttp,CacheInterceptor攔截器處理的邏輯,其實就是上面所說的HTTP緩存邏輯,注意到OkHttp提供了一個現成的緩存類Cache,它採用DiskLruCache實現緩存策略,至於緩存的位置和大小,須要你本身指定。
這裏其實會有個問題,上面的緩存都是依賴HTTP協議自己的緩存機制的,若是咱們請求的服務器不支持這套緩存機制,或者須要實現更靈活的緩存管理,直接使用上面這套緩存機制就可能不太可行了,這時咱們能夠本身新增攔截器,自行實現緩存的管理。
鏈接池的邏輯在ConnectInterceptor攔截器中處理,看intercept方法:
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
Transmitter transmitter = realChain.transmitter();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);
return realChain.proceed(request, transmitter, exchange);
}
複製代碼
關鍵代碼就是調用了Transmitter的newExchange方法,最終會獲得一個Exchange對象,該對象表示一條鏈接,用於後面實現請求和讀取響應數據,爲了不陷入代碼中沒法自拔,這裏就不一步一步跟蹤newExchange方法了,它最後會調用ExchangeFinder的findConnection的方法,這個方法就是在鏈接池中尋找可複用的鏈接,固然若是沒找到,就建立一個新的鏈接,OkHttp對鏈接池的管理是在RealConnectionPool類中:
public final class RealConnectionPool {
/**
* Background threads are used to cleanup expired connections. There will be at most a single
* thread running per connection pool. The thread pool executor permits the pool itself to be
* garbage collected.
*/
private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
new SynchronousQueue<>(), Util.threadFactory("OkHttp ConnectionPool", true));
/** The maximum number of idle connections for each address. */
private final int maxIdleConnections;
private final long keepAliveDurationNs;
private final Runnable cleanupRunnable = () -> {
while (true) {
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (RealConnectionPool.this) {
try {
RealConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
};
private final Deque<RealConnection> connections = new ArrayDeque<>();
......
}
複製代碼
主要關注幾個重要的成員變量,maxIdleConnections表示鏈接池的最大緩存鏈接數,這裏外部傳入了5,也就是最多緩存5個鏈接,緩存的鏈接都被放到connections中,而keepAliveDurationNs表示鏈接的緩存時長,這裏爲5分鐘,咱們還看到這裏還有個executor,它就是用來清理過時鏈接。
在CallServerInterceptor攔截器中處理,採用okio實現,http請求和讀取響應最終是在Http1ExchangeCodec或Http2ExchangeCodec中實現的。
在BridgeInterceptor攔截器中處理,看一下intercept方法:
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
RequestBody body = userRequest.body();
......
// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
// the transfer stream.
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
......
if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
GzipSource responseBody = new GzipSource(networkResponse.body().source());
Headers strippedHeaders = networkResponse.headers().newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
String contentType = networkResponse.header("Content-Type");
responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
}
return responseBuilder.build();
}
複製代碼
能夠看到,OkHttp默認會爲咱們加上gzip頭部字段,若是服務端支持的話,就會返回gzip壓縮後的數據,這樣就能夠縮短傳輸時間和減小傳輸數據大小,接收到gzip壓縮後的數據後,Okhttp會自動幫咱們解壓縮,因此這一切對使用者來講都是透明的,無需關注,固然若是咱們本身明確指定了用gzip壓縮,解壓縮的事情就須要咱們本身來作了。
OkHttp2支持HTTP2協議,固然若是服務端不支持就沒辦法了,針對HTTP2的相關類都在okhttp3.internal.http2包下,有興趣能夠自行查看源碼。
關於HTTP2的優勢,主要有:
多路複用:就是針對同個域名的請求,均可以在同一條鏈接中並行進行,並且頭部和數據都進行了二進制封裝。
二進制分幀:傳輸都是基於字節流進行的,而不是文本,二進制分幀層處於應用層和傳輸層之間。
頭部壓縮:HTTP1.x每次請求都會攜帶完整的頭部字段,因此可能會出現重複傳輸,所以HTTP2採用HPACK對其進行壓縮優化,能夠節省很多的傳輸流量。
服務端推送:服務端能夠主動推送數據給客戶端。