OkHttp 知識梳理(2) OkHttp 原始碼解析之非同步請求 & 執行緒排程

澤毛發表於2017-12-21

一、前言

OkHttp 知識梳理(1) - OkHttp 原始碼解析之入門 中,介紹了OkHttp的簡單使用及同步請求的實現流程,今天這篇文章,我們來一起學習一下非同步請求的內部實現原理及執行緒排程。

首先,讓我們回顧一下非同步請求的實現方式:

    private void startAsyncRequest() {
        //以下三步和同步請求的步驟相同。
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(URL).build();
        Call call = client.newCall(request);
        //區別在於拿到 RealCall 物件之後的處理方式。
        call.enqueue(new Callback() {

            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String result = response.body().string();
                //返回結果給主執行緒。
                Message message = mMainHandler.obtainMessage(MSG_UPDATE_UI, result);
                mMainHandler.sendMessage(message);
            }
        });
    }
複製程式碼

可以看到,對於非同步請求而言,前面三步和同步請求是相同的,區別在於發起請求時,同步請求使用的是call.execute(),只有當整個請求完成時才會從.execute()函式返回。

而對於非同步請求來說,.enqueue(Callback)方法只要呼叫完就立即返回了,當網路請求返回之後會回撥CallbackonResponse/onFailure方法,並且這兩個回撥方法是在子執行緒執行的,這也是非同步請求和同步請求之間最主要的差別。

下面我們就來分析一下非同步請求的內部實現邏輯。

二、非同步請求原始碼解析

對於前面三步的內部實現不再重複說明了,大家可以檢視 OkHttp 知識梳理(1) - OkHttp 原始碼解析之入門 中的分析。最終我們會得到一個RealCall例項,它代表了一個執行的任務。接下來看enqueue內部做了什麼。

    public void enqueue(Callback responseCallback) {
        //首先判斷該物件是否曾經被執行過。
        synchronized(this) {
            if(this.executed) {
                throw new IllegalStateException("Already Executed");
            }

            this.executed = true;
        }
        //捕獲堆疊資訊。
        this.captureCallStackTrace();
        //通知監聽者請求開始了。
        this.eventListener.callStart(this);
        //呼叫排程器的 enqueue 方法。
        this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
    }
複製程式碼

這裡,我們又見到了熟悉的dispatcher()類,它enqueue的實現為:

    private int maxRequests = 64;
    private int maxRequestsPerHost = 5;
    
    //執行任務的執行緒池。
    private ExecutorService executorService;

    //等待被執行的非同步請求任務佇列。
    private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque();
    //正在被執行的非同步請求任務佇列。
    private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque();
    
    public synchronized ExecutorService executorService() {
        if(this.executorService == null) {
            this.executorService = new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), Util.threadFactory("OkHttp Dispatcher", false));
        }
        return this.executorService;
    }

    synchronized void enqueue(AsyncCall call) {
        //如果當前正在請求的數量小於 64,並且對於同一 host 的請求小於 5,才發起請求。
        if(this.runningAsyncCalls.size() < this.maxRequests && this.runningCallsForHost(call) < this.maxRequestsPerHost) {
            //將該任務加入到正在請求的佇列當中。
            this.runningAsyncCalls.add(call); 
            //通過執行緒池執行任務。
            this.executorService().execute(call);
        //否則加入到等待佇列當中。
        } else {
            this.readyAsyncCalls.add(call);
        }
    }
複製程式碼

Dispatcherenqueue首先會判斷:如果當前正在請求的數量小於64,並且對於同一host的請求小於5,才發起請求。發起請求之前會將RealCall加入到runningAsyncCalls佇列當中,並通過ThreadPoolExecutor來執行該請求,

2.1 通過執行緒池執行任務

ThreadPoolExecutorJava提供的執行緒池,在 多執行緒知識梳理(6) - 執行緒池四部曲之 ThreadPoolExecutor 中我們已經介紹過它,這裡根據它的引數配置可以看出,它對應於CachedThreadPool,該執行緒池的特點是 執行緒池大小無界,適用於執行很多的短期非同步任務的程式或者是負載較輕的伺服器

OkHttp 知識梳理(2)   OkHttp 原始碼解析之非同步請求 & 執行緒排程
它的具體實現方式為:

  • 等待佇列使用的是SynchonousQueue,它的 每個插入操作都必須等待另一個執行緒的移除操作,對於執行緒池而言,也就是說:在新增任務到等待佇列時,必須要有一個空閒執行緒正在嘗試從等待佇列獲取任務,才有可能新增成功。
  • 因此,當一個任務被新增進入執行緒池時,會有以下兩種情況:
    • 如果當前有空閒執行緒正在嘗試從等待佇列中獲取任務,那麼這個 任務將會被交給這個空閒執行緒 進行處理
    • 如果當前沒有空閒執行緒嘗試從等待佇列中獲取任務,那麼將會 建立一個新執行緒來執行任務
  • 由於設定了等待超時時間,某個執行緒在60s內都無法獲取到新的任務將會被銷燬。

執行緒池的execute函式接收Runnable的介面實現類作為引數,在該任務被執行時將會呼叫它的run()方法,這上面的AsyncCall也是一樣的道理,它繼承了NamedRunnable抽象類,而NamedRunnable又實現了Runnable介面,當NamedRunnablerun()方法被回撥時,會呼叫AsyncCallexecute()方法。

final class AsyncCall extends NamedRunnable {
	private final Callback responseCallback;

    //responseCallback 就是呼叫 call.enqueue 方法時傳入的回撥。
	AsyncCall(Callback responseCallback) {
		super("OkHttp %s", new Object[]{RealCall.this.redactedUrl()});
		this.responseCallback = responseCallback;
	}
        
    //該函式是在子執行緒當中執行的。
	protected void execute() {
		boolean signalledCallback = false;

		try {
            //和同步請求的邏輯相同。
			Response response = RealCall.this.getResponseWithInterceptorChain();
			if(RealCall.this.retryAndFollowUpInterceptor.isCanceled()) {
				signalledCallback = true;
				this.responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
			} else {
				signalledCallback = true;
				this.responseCallback.onResponse(RealCall.this, response);
			}
		} catch (IOException var6) {
			if(signalledCallback) {
				Platform.get().log(4, "Callback failure for " + RealCall.this.toLoggableString(), var6);
			} else {
				RealCall.this.eventListener.callFailed(RealCall.this, var6);
				this.responseCallback.onFailure(RealCall.this, var6);
			}
		} finally {
                        //呼叫 Dispatcher 的 finished 方法
			RealCall.this.client.dispatcher().finished(this);
		}

	}
}

public abstract class NamedRunnable implements Runnable {
    protected final String name;

    public NamedRunnable(String format, Object... args) {
        this.name = Util.format(format, args);
    }

    public final void run() {
        String oldName = Thread.currentThread().getName();
        Thread.currentThread().setName(this.name);

        try {
            //呼叫子類的 execute() 方法。
            this.execute();
        } finally {
            Thread.currentThread().setName(oldName);
        }

    }
    protected abstract void execute();
}
複製程式碼

execute()方法最終是在 子執行緒當中執行的,這裡我們看到了熟悉的一句話:

Response response = RealCall.this.getResponseWithInterceptorChain();
複製程式碼

這裡面就是進行請求的核心邏輯,我們在 OkHttp 知識梳理(1) - OkHttp 原始碼解析之入門 中的3.4節中已經介紹過了,這裡會通過一系列的攔截器進行處理,重試請求、快取處理和網路請求都是在裡面完成的,最終得到返回的Response,並根據情況回撥最開始傳入的CallbackonResponse/onFailure方法。

2.2 任務執行完後的處理

當回撥完之後,最終會呼叫Dispatcherfinished方法:

    void finished(AsyncCall call) {
        //如果是非同步請求,那麼最後一個引數為 true。
        this.finished(this.runningAsyncCalls, call, true);
    }

    void finished(RealCall call) {
        //如果是同步請求,那麼最後一個引數為 false。
        this.finished(this.runningSyncCalls, call, false);
    }

    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) {
                this.promoteCalls();
            }
            runningCallsCount = this.runningCallsCount();
            idleCallback = this.idleCallback;
        }
        if (runningCallsCount == 0 && idleCallback != null) {
            idleCallback.run();
        }
    }

    private void promoteCalls() {
        if (this.runningAsyncCalls.size() < this.maxRequests) {
            if (!this.readyAsyncCalls.isEmpty()) {
                Iterator i = this.readyAsyncCalls.iterator();
                do {
                    if(!i.hasNext()) {
                        return;
                    }
                    AsyncCall call = (AsyncCall)i.next();
                    if (this.runningCallsForHost(call) < this.maxRequestsPerHost) {
                        i.remove();
                        //找到了等待佇列中符合執行的條件的任務,那麼就執行它。
                        this.runningAsyncCalls.add(call);
                        this.executorService().execute(call);
                    }
                } while(this.runningAsyncCalls.size() < this.maxRequests);

            }
        }
    }
複製程式碼

這裡和同步請求相同,都會走到finished方法當中,區別在於最後一次引數是true,而同步請求是false,也就是說會呼叫到promoteCalls方法中,promoteCalls的作用為:在最開始時,如果不滿足執行條件,那麼任務將會被加入到等待佇列readyAsyncCalls中,那麼當一個任務執行完之後,就需要去等待佇列中尋找符合執行條件的任務,並將它加入到任務佇列中執行,之後的邏輯和前面的相同。

promoteCalls函式除了在一個非同步請求執行完畢後會呼叫,當我們改變最大請求數量和對於同一個host的最大請求數量時,也會觸發該查詢過程。

    //改變了最大請求數量。
    public synchronized void setMaxRequests(int maxRequests) {
        if(maxRequests < 1) {
            throw new IllegalArgumentException("max < 1: " + maxRequests);
        } else {
            this.maxRequests = maxRequests;
            this.promoteCalls();
        }
    }
    //改變了同一個 Host 的最大請求數量。
    public synchronized void setMaxRequestsPerHost(int maxRequestsPerHost) {
        if(maxRequestsPerHost < 1) {
            throw new IllegalArgumentException("max < 1: " + maxRequestsPerHost);
        } else {
            this.maxRequestsPerHost = maxRequestsPerHost;
            this.promoteCalls();
        }
    }
複製程式碼

三、小結

以上就是對於非同步請求方式的原始碼分析,由此我們可以總結出OkHttp對於非同步請求的排程方式:

  • 通過兩個列表對非同步請求任務進行管理,runningAsyncCalls中存放的是正在執行任務的列表,readyAsyncCalls中則是等待被執行的任務。
  • 當一個任務執行完畢後,會去readyAsyncCalls查詢下一個可以被執行的任務。
  • 任務的執行是通過執行緒池ThreadPoolExecutor在子執行緒中來完成的,由它來負責正在執行任務的排程,內部的實現原理如 多執行緒知識梳理(6) - 執行緒池四部曲之 ThreadPoolExecutor 所分析。
  • 真正進行網路請求的核心程式碼在AsyncCallexecute()函式中,這裡會通過一系列的攔截器進行處理,重試請求、快取處理和網路請求都是在裡面完成的,最終得到返回的Response,並根據情況回撥最開始傳入的CallbackonResponse/onFailure方法。
  • 對於非同步請求而言,CallbackonResponse/onFailed是在子執行緒當中執行的,因此如果要在其中執行更新UI的操作,那麼需要通知主執行緒來更新。

更多文章,歡迎訪問我的 Android 知識梳理系列:

相關文章