高效易用的okio(四)
超時機制在我們的日常生活中隨處可見,最為常見就是火車了,如果你不能按時到達火車站點,那麼你就錯失坐這一趟火車的機會
在前面的文章,就已經提到過 okio 中的有一個超時機制 Timeout
, 現在就來說說它的原理
okio 中的超時機制只要就兩種:
- 同步超時
Timeout
- 非同步超時
AsyncTimeout
還有一個超時物件 ForwardingTimeout
,不過這個屬於一個空盒子,需要裝入其他的 Timeout
物件才可以使用
同步超時 TimeOut
所謂超時,就是用來控制某個任務執行的最大時長,當執行超過指定的時間時,任務將被中斷
例如當從輸入流 Source
讀取資料超時後,輸入流將被關閉,任務到此結束
而在 Timeout
中,主要使用兩個判斷條件來判斷任務是否超時了:
- 任務設定了結束時間( hasDeadline = true )並且當前已經過了結束時間( deadlineNanoTime )
- 任務已經過了超時時間( timeoutNanos )
正是下面的變數:
public class Timeout {
.....
/**
* 是否設定了結束時間
*/
private boolean hasDeadline;
/**
* 結束時間
*/
private long deadlineNanoTime;
/**
* 超時時間
*/
private long timeoutNanos;
.....
}
在 Timeout
裡面,用於判斷超時的方法主要是有兩個,一個是 throwIfReached
:
/**
*
* 該方法並不是檢測超時方法
* 該方法用於檢測執行緒是否中斷了或者是否到了結束時間
* 如果是,那麼就丟擲異常來進行中斷
* 目前用於在執行讀寫操作時的檢查
*/
public void throwIfReached () throws IOException {
if (Thread.interrupted()) {
throw new InterruptedIOException("thread interrupted");
}
//判斷是否設定了結束flag以及是否到了結束時間
if (hasDeadline && deadlineNanoTime - System.nanoTime() <= 0) {
throw new InterruptedIOException("deadline reached");
}
}
正如上面寫的,這個方法是在執行讀寫操作時,判斷一下執行緒是否中斷或當前任務是否已經到了結束時間的
通過對該方法的全域性搜尋,可以大致明白這個用途:
這時候是不是覺得有點奇怪,怎麼還有一個 timeoutNanos
沒有用到的?
別急,接下來介紹的 waitUntilNotified
就需要用到它了
waitUntilNotified
用來等待某個指定的 monitor
物件,直到這個物件被 notify 或者超時時間到 為止:
public final void waitUntilNotified(Object monitor) throws InterruptedIOException {
try {
//獲取當前是否設定結束flag
boolean hasDeadline = hasDeadline();
//獲取超時時間
long timeoutNanos = timeoutNanos();
//當沒有設定超時時間,那麼就設定 wait ,它將無限等待直到物件被 notify 為止
if (!hasDeadline && timeoutNanos == 0L) {
monitor.wait();
return;
}
//根據timeoutNanos和deadlineNanoTime計算出較短的超時時間waitNanos
//也就是okio需要等待多久
long waitNanos;
//當前系統時間
long start = System.nanoTime();
if (hasDeadline && timeoutNanos != 0) {
//如果設定了結束flag並且超時時間不為0
//先計算下還有多久到結束時間
long deadlineNanos = deadlineNanoTime() - start;
//對比結束時間,超時時間,那個時間段更加短,取短的值
waitNanos = Math.min(timeoutNanos, deadlineNanos);
} else if (hasDeadline) {
waitNanos = deadlineNanoTime() - start;
} else {
waitNanos = timeoutNanos;
}
//呼叫wait方法並設定等待超時時間
//直到了超時了或者 monitor 給 notify 為止
long elapsedNanos = 0L;
if (waitNanos > 0L) {
long waitMillis = waitNanos / 1000000L;
monitor.wait(waitMillis, (int) (waitNanos - waitMillis * 1000000L));
//記錄跑到這裡過去了多少時間,用於計算超時
elapsedNanos = System.nanoTime() - start;
}
// 走到這裡,說明wait等待超時時間到,或者 monitor 給 notify了
if (elapsedNanos >= waitNanos) {
//如果時超時時間到就丟擲InterruptedIOException異常
throw new InterruptedIOException("timeout");
}
} catch (InterruptedException e) {
throw new InterruptedIOException("interrupted");
}
}
這裡使用了 Java 的 wait
和 notify
機制,這種常用在同步機制上面
非同步超時 AsyncTimeout
非同步超時 AsyncTimeout
繼承自 TimeOut
,相比起它的父類 TimeOut
,AsyncTimeout
多了一個守護執行緒 Watchdog
和需要自定義一個 timeOut
方法
例如在 okio 的原始碼中,就提供了一個自定義 timeOut
方法,用於任務超時後,關閉 Socket
private static AsyncTimeout timeout(final Socket socket) {
return new AsyncTimeout() {
...........
@Override
protected void timedOut() {
try {
socket.close();
} catch (Exception e) {
logger.log(Level.WARNING, "Failed to close timed out socket " + socket, e);
} catch (AssertionError e) {
if (isAndroidGetsocknameError(e)) {
// Catch this exception due to a Firmware issue up to android 4.2.2
// https://code.google.com/p/android/issues/detail?id=54072
logger.log(Level.WARNING, "Failed to close timed out socket " + socket, e);
} else {
throw e;
}
}
}
}
}
瞭解了 AsyncTimeout
自定義需要注意的事項後,我們來看下它裡面的具體原理,先來了解下 Watchdog
這個守護程序到底是幹什麼的:
可以看到,所有的 AsyncTimeout
在會組成一個連結串列,而 Watchdog
則是無限迴圈去取出裡面的元素,只要發現超時的元素就會執行 timedOut
方法
連結串列的定義是在類的開頭:
public class AsyncTimeout extends Timeout {
..........
/**
* 連結串列的頭節點,指向連結串列中第一個元素,也就是head.next為連結串列的第一個元素
* 如果head.next為null,那麼這是一個空的佇列
*/
static @Nullable AsyncTimeout head;
/**
* The next node in the linked list.
* 當前 AsyncTimeout 指向的下一個連結串列元素
*/
private @Nullable AsyncTimeout next;
..........
}
當連結串列不為空時, 就是 Watchdog
工作的時候了:
private static final class Watchdog extends Thread {
Watchdog() {
super("Okio Watchdog");
setDaemon(true);//設定為守護程序
}
@Override
public void run() {
while (true) {
try {
AsyncTimeout timedOut;
synchronized (AsyncTimeout.class) {
timedOut = awaitTimeout();
//沒有找到需要結束的節點,繼續迴圈查詢
if (timedOut == null) {
continue;
}
//如果只能找到頭指標,那麼說明這個佇列為null
if (timedOut == head) {
head = null;
return;
}
}
//找到發生超時的元素,執行它的自定義超時回撥方法timedOut,
//注意,這裡不能做耗時操作,否則會阻塞連結串列中其他已經發生超時的元素
timedOut.timedOut();
} catch (InterruptedException ignored) {
}
}
}
}
首先需要明確的是,守護執行緒耗費資源是非常低,當只剩下守護執行緒時,jvm 也就關閉了,因此這裡的死迴圈並沒有消耗太多的記憶體
然後就是 awaitTimeout
,這個方法用來查詢當前超時的元素,我們來看下它的怎麼去找的:
static AsyncTimeout awaitTimeout() throws InterruptedException {
//獲取連結串列的第一個元素
AsyncTimeout node = head.next;
if (node == null) {
// 連結串列為空,則最長等待 IDLE_TIMEOUT_NANOS 時間
long startNanos = System.nanoTime();
AsyncTimeout.class.wait(IDLE_TIMEOUT_MILLIS);
return head.next == null && (System.nanoTime() - startNanos) >= IDLE_TIMEOUT_NANOS
? head // The idle timeout elapsed.
: null; // The situation has changed.
}
long waitNanos = node.remainingNanos(System.nanoTime());
// 連結串列的第一個元素還沒有超時,繼續等待超時時間到
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
AsyncTimeout.class.wait(waitMillis, (int) waitNanos);
return null;
}
// 連結串列的第一個元素超時時間到,從連結串列中移除並將其返回
head.next = node.next;
node.next = null;
return node;
}
它是通過 head
去找的,這也說明了 連結串列的排序是按照超時時間的從低到高開始排序的
每次都是去找 head.next
節點,超時了就執行 timeOut
方法,沒有就繼續 wait
不過,當節點執行了 wait
之後,是什麼時候 notify
呢?通過程式碼追蹤,找到了 scheduleTimeout
方法以及 enter
方法 和 exit
方法
enter
方法是進入連結串列的方法, exit
是退出連結串列的方法,是執行讀寫操作都會呼叫它們:
enter
方法用於插入連結串列元素 :
public final void enter() {
if (inQueue) throw new IllegalStateException("Unbalanced enter/exit");
long timeoutNanos = timeoutNanos();
boolean hasDeadline = hasDeadline();
if (timeoutNanos == 0 && !hasDeadline) {
return; // No timeout and no deadline? Don't bother with the queue.
}
inQueue = true;
scheduleTimeout(this, timeoutNanos, hasDeadline);
}
它最終是呼叫了 scheduleTimeout
方法:
private static synchronized void scheduleTimeout(
AsyncTimeout node, long timeoutNanos, boolean hasDeadline) {
// 當連結串列為空時,初始化連結串列並啟動守護執行緒
if (head == null) {
head = new AsyncTimeout();
new Watchdog().start();
}
// 計算超時的時間點timeoutAt
long now = System.nanoTime();
if (timeoutNanos != 0 && hasDeadline) {
node.timeoutAt = now + Math.min(timeoutNanos, node.deadlineNanoTime() - now);
} else if (timeoutNanos != 0) {
node.timeoutAt = now + timeoutNanos;
} else if (hasDeadline) {
node.timeoutAt = node.deadlineNanoTime();
} else {
throw new AssertionError();
}
long remainingNanos = node.remainingNanos(now);
//從連結串列頭開始遍歷連結串列
for (AsyncTimeout prev = head; true; prev = prev.next) {
//當到達連結串列尾部,或者根據超時時間從短到長排序找到合適位置後插入連結串列
if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)) {
node.next = prev.next;
prev.next = node;
//如果當前插入的節點是連結串列的第一個元素,那麼需要喚醒在awaitTimeout方法中的wait操作
if (prev == head) {
//喚醒操作
AsyncTimeout.class.notify();
}
break;
}
}
}
exit
方法用於刪除連結串列中的元素:
final void exit(boolean throwOnTimeout) throws IOException {
boolean timedOut = exit();
if (timedOut && throwOnTimeout) throw newTimeoutException(null);
}
public final boolean exit() {
if (!inQueue) return false;
inQueue = false;
return cancelScheduledTimeout(this);
}
private static synchronized boolean cancelScheduledTimeout(AsyncTimeout node) {
// Remove the node from the linked list.
for (AsyncTimeout prev = head; prev != null; prev = prev.next) {
if (prev.next == node) {
prev.next = node.next;
node.next = null;
return false;
}
}
// The node wasn't found in the linked list: it must have timed out!
return true;
}
可以看到,到最後就是簡單的連結串列資料的刪除操作
總結
到此,這次 okio 框架的解析就結束了,雖然還有部分功能和模組沒有講到,不過大致的流程原理都已經過了一遍了
對比 Java 的原生 IO,可以看出 okio 使用更加方便,更加高效的記憶體利用,建議在實際開發中都用上它