Android小知識-剖析OkHttp中的非同步請求
本平臺的文章更新會有延遲,大家可以關注微信公眾號-顧林海,包括年底前會更新kotlin由淺入深系列教程,目前計劃在微信公眾號進行首發,如果大家想獲取最新教程,請關注微信公眾號,謝謝
其實從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方法,將等待就緒的非同步佇列中的請求新增到正在請求的非同步請求佇列中去並通過執行緒池來執行非同步請求。

838794-506ddad529df4cd4.webp.jpg
搜尋微信“顧林海”公眾號,定期推送優質文章。