Okhttp 请求流程梳理 (超详细版)
最近在看 Okhttp 的源碼。不得不說源碼設計的很巧妙,從中能學到很多。其實網上關于 Okhttp 的文章已經很多了,自己也看了很多。但是俗話說得好,好記性不如爛筆頭,當你動手的時候,你會發現你在看的時候沒有注意到的很多細節。
本次要分析的 Okhttp 版本是 3.8.1,在 gradle 中引用如下:
implementation 'com.squareup.okhttp3:okhttp:3.8.1' implementation 'com.squareup.okio:okio:1.7.0'
之所以選擇分析3.8.1,是因為最新版是采用 Kotlin 寫的,因為本人 Kotlin 實力不允許,所以只能分析 Java 版本。
使用示例
1、發起一個異步 GET 請求,代碼具體如下:
String url = "http://wwww.baidu.com";
OkHttpClient okHttpClient = new OkHttpClient();
final Request request = new Request.Builder()
.url(url)
.get()//默認就是GET請求,可以不寫
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.d(TAG, "onFailure: ");
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.d(TAG, "onResponse: " + response.body().string());
}
});
2、發起一個同步 GET 請求,代碼具體如下:
String url = "http://wwww.baidu.com";
OkHttpClient okHttpClient = new OkHttpClient();
final Request request = new Request.Builder()
.url(url)
.build();
final Call call = okHttpClient.newCall(request);
new Thread(new Runnable() {
@Override
public void run() {
try {
Response response = call.execute();
Log.d(TAG, "run: " + response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
可以看到兩個請求基本大同小異,總結下過程如下:
先創建 OkHttpClient 實例;
構造 Request 實例,傳入 url 等相關參數;
通過前兩步中的實例對象構建 Call 對象;
異步請求通過 Call#enqueue(Callback) 方法來提交異步請求,同步請求通過 Call#execute() 直接獲取 Reponse ;
通過示例,大家簡單了解 Okhttp 中的一些對象,下面開始梳理整個請求邏輯。先從 OkHttpClient 開始。
OkHttpClient
當我們發起請求的時候,需要先構造okHttpClient 對象,代碼具體如下:
public OkHttpClient() {
this(new Builder());
}
可以發現是使用了 builder 建造者模式;來看看里面的內容
public Builder() {
dispatcher = new Dispatcher(); // 調度器
protocols = DEFAULT_PROTOCOLS; // 協議
connectionSpecs = DEFAULT_CONNECTION_SPECS; //傳輸層版本和連接協議
eventListenerFactory = EventListener.factory(EventListener.NONE);
proxySelector = ProxySelector.getDefault();
cookieJar = CookieJar.NO_COOKIES; //cookie
socketFactory = SocketFactory.getDefault();
hostnameVerifier = OkHostnameVerifier.INSTANCE;
certificatePinner = CertificatePinner.DEFAULT; //證書鏈
proxyAuthenticator = Authenticator.NONE; //代理身份驗證
authenticator = Authenticator.NONE; //本地身份驗證
connectionPool = new ConnectionPool(); //鏈接池 復用連接
dns = Dns.SYSTEM; //域名
followSslRedirects = true;
followRedirects = true; //本地重定向
retryOnConnectionFailure = true;
connectTimeout = 10_000;
readTimeout = 10_000; //讀取超時
writeTimeout = 10_000; //寫入超時
pingInterval = 0;
}
OkHttpClient 內部已經實現了 OkHttpClient(Builder builder),如果我們不需要配置 client,okhttp 已將幫我們默認實現了配置。總結起來主要有以下幾點:
里面包含了很多對象,其實 OKhttp 的很多功能模塊都包裝進這個類,讓這個類單獨提供對外的 API,這種外觀模式的設計十分的優雅,叫做外觀模式。
而內部模塊比較多,就使用了 Builder 模式(建造器模式),通常用于參數比較多情況。
它的方法只有一個:newCall 返回一個 Call 對象(一個準備好了的可以執行和取消的請求)。
Request
接下來,我們看 Request 請求類,主要包含下面幾個屬性: url,請求方法名,請求頭部,請求體,從屬性就可以判斷出 Request 主要作用。
Request(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = builder.headers.build();
this.body = builder.body;
this.tag = builder.tag != null ? builder.tag : this;
}
Call
HTTP請求任務封裝。可以說我們能用到的操縱基本上都定義在這個接口里面了,所以也可以說這個類是 Okhttp 類的核心類了。我們可以通過 Call 對象來操作請求了。而 Call 接口內部提供了 Factory 工廠方法模式(將對象的創建延遲到工廠類的子類去進行,從而實現動態配置),下面是 Call 接口的具體內容:
public interface Call extends Cloneable {
/** Returns the original request that initiated this call. */
Request request();
/**
* Invokes the request immediately, and blocks until the response can be processed or is in
* error.*/
Response execute() throws IOException;
/**
* Schedules the request to be executed at some point in the future.*/
void enqueue(Callback responseCallback);
/** Cancels the request, if possible. Requests that are already complete cannot be canceled. */
void cancel();
/**
* Returns true if this call has been either {@linkplain #execute() executed} or {@linkplain
* #enqueue(Callback) enqueued}. It is an error to execute a call more than once.
*/
boolean isExecuted();
boolean isCanceled();
/**
* Create a new, identical call to this one which can be enqueued or executed even if this call
* has already been.
*/
Call clone();
interface Factory {
Call newCall(Request request);
}
}
RealCall
RealCall 繼承自 Call,是真正發起請求的的實體類。RealCall 主要方法:
同步請求 :client.newCall(request).execute();
異步請求:client.newCall(request).enqueue();
下面我們來看看里面具體的內容:
RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
final EventListener.Factory eventListenerFactory = client.eventListenerFactory();
this.client = client;
this.originalRequest = originalRequest;
this.forWebSocket = forWebSocket;
this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
// TODO(jwilson): this is unsafe publication and not threadsafe.
this.eventListener = eventListenerFactory.create(this);
}
可以發現,其內部持有了 client,原始請求,以及請求事件回調 Listener 等。我們看下請求的回調 Listener 的具體內容:
public void fetchStart(Call call) {
}
public void dnsStart(Call call, String domainName) {
}
public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList,
Throwable throwable) {
}
public void connectStart(Call call, InetAddress address, int port) {
}
public void secureConnectStart(Call call) {
}
public void secureConnectEnd(Call call, Handshake handshake,
Throwable throwable) {
}
public void connectEnd(Call call, InetAddress address, int port, String protocol,
Throwable throwable) {
}
public void requestHeadersStart(Call call) {
}
public void requestHeadersEnd(Call call, Throwable throwable) {
}
public void requestBodyStart(Call call) {
}
public void requestBodyEnd(Call call, Throwable throwable) {
}
public void responseHeadersStart(Call call) {
}
public void responseHeadersEnd(Call call, Throwable throwable) {
}
public void responseBodyStart(Call call) {
}
public void responseBodyEnd(Call call, Throwable throwable) {
}
public void fetchEnd(Call call, Throwable throwable) {
}
可以看到 OkHttp 的回調做得非常細致,有各種各樣的回調,不管你想不想用,都幫你考慮到了呢。這樣我們可以監聽具體的回調,然后做一些操作。
接下去就要開始講異步請求的具體步驟呢。先從異步請求講起,這也是我們最常用的。
// RealCall
@Override public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
可以看到上述代碼做了幾件事:
synchronized (this)確保每個call只能被執行一次不能重復執行,如果想要完全相同的 call,可以調用如下方法:進行克隆
@SuppressWarnings("CloneDoesntCallSuperClone") // We are a final type & this saves clearing state.
@Override public RealCall clone() {
return RealCall.newRealCall(client, originalRequest, forWebSocket);
}
利用 dispatcher 調度器,來進行實際的執行client.dispatcher().enqueue(new AsyncCall(responseCallback)),在上面的 OkHttpClient.Builder 可以看出已經初始化了 Dispatcher。
心細的讀者可能發現一個問題了,那就是這里 enqueue 明明是一個封裝了 responseCallback 的 AsyncCall ,怎么就會變成加入隊列執行請求了呢?這個下面我會進行解釋。
Dispatcher
Dispatcher 是 Okhttp 的調度器,用來管理控制請求的隊列。內部通過線程池來確保隊列的有序運行。先看下 enqueue 方法的具體內容:
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
可以看到內部存在兩個隊列,一個是正在運行的隊列runningAsyncCalls,另一個是readyAsyncCalls 隊列。如果當前運行數小于最大運行數,并且當前請求的host小于最大請求個數,那么就會直接加入運行隊列,并運行。如果超了,就會加入準備隊列。
/** Ready async calls in the order they'll be run. */ private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>(); /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */ private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>(); /** Running synchronous calls. Includes canceled calls that haven't finished yet. */ private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
實際上還有一個同步隊列,沒有給同步隊列做限制,只要一加入就開始執行請求。
當請求隊列完成請求后需要進行移除,看下 finished 的代碼邏輯:
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();
}
}
可以看到,這是使用了泛型,不用關心具體傳入的隊列是哪一個,直接就可以移除。promoteCalls 為 true 代表是異步請求隊列,還得從 readyAsyncCalls 隊列里面取出一個隊列添加到 runningAsyncCalls 隊列里面去執行請求。
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.
}
}
通過上述代碼,關于調度器的功能作用就基本理清了。
AsyncCall
AsyncCall 是 RealCall 里面的內部類,繼承自NamedRunnable,是自定義的Runnable,可以為線程設置 name。內部代碼具體如下:
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();
}
可以發現,在 run 方法內部調用了execute 方法,這個方法就是真正的發起請求的邏輯。下面我們看下AsyncCall 中的該方法得具體內容:
@Override protected void execute() {
boolean signalledCallback = false;
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) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
responseCallback.onFailure(RealCall.this, e);
}
} finally {
client.dispatcher().finished(this);
}
}
}
獲取響應數據最終是是通過getResponseWithInterceptorChain() 來獲取的。然后通過回調將 Response 返回給用戶。
值得注意的 finally 執行了client.dispatcher().finished(this) 通過調度器移除隊列。移除得邏輯在前面也已經講過了。
下面看下getResponseWithInterceptorChain 方法內部的具體邏輯:
//Realcall 核心代碼 開始真正的執行網絡請求
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
// 責任鏈
List<Interceptor> interceptors = new ArrayList<>();
// 在配置okhttpClient 時設置的intercept 由用戶自己設置
interceptors.addAll(client.interceptors());
// 負責處理失敗后的重試與重定向
interceptors.add(retryAndFollowUpInterceptor);
// 負責把用戶構造的請求轉換為發送到服務器的請求 、把服務器返回的響應轉換為用戶友好的響應 處理 配置請求頭等信息
// 從應用程序代碼到網絡代碼的橋梁。首先,它根據用戶請求構建網絡請求。然后它繼續呼叫網絡。最后,它根據網絡響應構建用戶響應。
interceptors.add(new BridgeInterceptor(client.cookieJar()));
// 處理 緩存配置 根據條件(存在響應緩存并被設置為不變的或者響應在有效期內)返回緩存響應
// 設置請求頭(If-None-Match、If-Modified-Since等) 服務器可能返回304(未修改)
// 可配置用戶自己設置的緩存攔截器
interceptors.add(new CacheInterceptor(client.internalCache()));
// 連接服務器 負責和服務器建立連接 這里才是真正的請求網絡
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
// 配置okhttpClient 時設置的networkInterceptors
// 返回觀察單個網絡請求和響應的不可變攔截器列表。
interceptors.addAll(client.networkInterceptors());
}
// 執行流操作(寫出請求體、獲得響應數據) 負責向服務器發送請求數據、從服務器讀取響應數據
// 進行http請求報文的封裝與請求報文的解析
interceptors.add(new CallServerInterceptor(forWebSocket));
// 創建責任鏈
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
//TODO 執行責任鏈
return chain.proceed(originalRequest);
}
從上述代碼中,可以看出都實現了 Interceptor 接口,這是 Okhttp 最核心的部分,采用責任鏈的模式來使每個功能分開,每個 Interceptor 自行完成自己的任務,并且將不屬于自己的任務交給下一個,簡化了各自的責任和邏輯。
RealInterceptorChain
那責任鏈是怎么實現的呢?下面具體分析下相關邏輯:
public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation,
HttpCodec httpCodec, RealConnection connection, int index, Request request) {
this.interceptors = interceptors;
this.connection = connection;
this.streamAllocation = streamAllocation;
this.httpCodec = httpCodec;
this.index = index;
this.request = request;
}
// 責任鏈處理
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
calls++;
// If we already have a stream, confirm that the incoming request will use it.
if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
+ " must retain the same host and port");
}
// If we already have a stream, confirm that this is the only call to chain.proceed().
if (this.httpCodec != null && calls > 1) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
+ " must call proceed() exactly once");
}
// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(
interceptors, streamAllocation, httpCodec, connection, index + 1, request);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
// Confirm that the next interceptor made its required call to chain.proceed().
if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
throw new IllegalStateException("network interceptor " + interceptor
+ " must call proceed() exactly once");
}
// Confirm that the intercepted response isn't null.
if (response == null) {
throw new NullPointerException("interceptor " + interceptor + " returned null");
}
return response;
}
首先是看構造函數,內部持有了當前責任鏈的所有攔截器list,還包括RealConnection,index (當前正在處理攔截器索引)等。
接下去看proceed 方法里的邏輯,歸來起來就是如下:
首先是通過 index 和 calls 來做了一些安全判斷,避免重復處理,。
將索引號 index +1,新創建一個 chain。
根據目前的 index 獲取攔截器,然后將新的 chain 傳入到獲取攔截器中。
攔截器做完自己的操作后,會調用新創建的 chain 的proceed 方法,交由下一個攔截器來處理。
當數據返回后,從后往前,攔截器會依次對數據做一些處理,最終用戶獲得請求的數據。
通過上述往復循環,最終所有的攔截器都會走兩遍,一次是對請求體做操作,一次是對返回體做操作,最終用戶獲得處理后的數據。
下面來看一個具體的攔截器 。
CacheInterceptor
CacheInterceptor代碼比較長,我們一步一步的來進行分析。
首先我們先分析上部分代碼當沒有網絡的情況下是如何處理獲取緩存的。
@Override public Response intercept(Chain chain) throws IOException
{
// 獲取request對應緩存的Response 如果用戶沒有配置緩存攔截器 cacheCandidate == null
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
// 執行響應緩存策略
long now = System.currentTimeMillis();
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
// 如果networkRequest == null 則說明不使用網絡請求
Request networkRequest = strategy.networkRequest;
// 獲取緩存中(CacheStrategy)的Response
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 we're forbidden from using the network and the cache is insufficient, fail.
// networkRequest == null 不實用網路請求 且沒有緩存 cacheResponse == null 返回失敗
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 we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
}
上述的代碼,主要做了幾件事:
如果用戶自己配置了緩存攔截器,cacheCandidate = cache.Response 獲取用戶自己存儲的 Response,否則 cacheCandidate = null,同時從 CacheStrategy 獲取cacheResponse 和 networkRequest;
如果 cacheCandidate != null 而 cacheResponse == null 說明緩存無效清楚 cacheCandidate 緩存。
如果 networkRequest == null 說明沒有網絡,cacheResponse == null 沒有緩存,返回失敗的信息,責任鏈此時也就終止,不會在往下繼續執行。
如果 networkRequest == null 說明沒有網絡,cacheResponse != null 有緩存,返回緩存的信息,責任鏈此時也就終止,不會在往下繼續執行。
上部分代碼,其實就是沒有網絡的時候的處理。那么下部分代碼肯定是,有網絡的時候處理:
// 執行下一個攔截器
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 we have a cache response too, then we're doing a conditional get.
// 如果存在緩存 更新
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();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
// 緩存Response
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;
}
上面的代碼主要做了這幾件事:
執行下一個攔截器,也就是請求網絡
責任鏈執行完畢后,會返回最終響應數據,如果緩存存在更新緩存,如果緩存不存在加入到緩存中去。
這就跟前面講的對應上了,請求前做一些處理,比如判斷緩存是否存在,網絡是否可用等操作;數據回來之后,更新緩存,在傳給上一個攔截器去做處理。
這樣就體現出了責任鏈的好處了,當責任鏈執行完畢,如果攔截器想要拿到最終的數據做其他的邏輯處理等,這樣就不用在做其他的調用方法邏輯了,直接在當前的攔截器就可以拿到最終的數據。這也是okhttp設計的最優雅最核心的功能。
到這里,異步請求邏輯基本就梳理完了。
同步請求
同步請求會直接調用 Call#ececute 方法,記住這個 execute 方法的返回實體是 Reponse,所以它直接返回了請求。
// RealCall
// 同步執行請求 直接返回一個請求的結果
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
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);
}
}
主要做了幾件事:
synchronized (this) 避免重復執行,上面的文章部分有講。
client.dispatcher().executed(this),實際上調度器只是將 call 加入到了同步執行隊列中。
getResponseWithInterceptorChain() 最核心的代碼,相當于同步請求直接就開始運行,請求網絡得到響應數據,返回給用戶
client.dispatcher().finished(this); 執行調度器的完成方法 移除隊列
可以看出,在同步請求的方法中,涉及到 dispatcher 只是告知了執行狀態,開始執行了(調用 executed),執行完畢了(調用 finished)其他的并沒有涉及到。dispatcher 更多的是服務異步請求。
以上就是對 Okhttp 請求流程的梳理,后面附一張盜的流程圖
總結
以上是生活随笔為你收集整理的Okhttp 请求流程梳理 (超详细版)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: rte_tcp_hdr rte_ipv
- 下一篇: mac定时执行python_Python