深入了解Android Okio的超時機制
Okio是一個IO庫,底層基于Java原生的輸入輸出流實現(xiàn)。但原生的輸入輸出流并沒有提供超時的檢測機制。而Okio實現(xiàn)了這個功能。建議讀者先閱讀 Android | 徹底理解 Okio 之源碼篇 ,然后再閱讀本篇內(nèi)容會更好理解。
Timeout 類的設計
探討超時機制,首先要了解Timeout這個類。Timeout實現(xiàn)了Okio的同步超時檢測,這里的同步指的是“任務執(zhí)行”和“超時檢測”是同步的,有順序的。同步超時不會直接中斷任務執(zhí)行,它首先會檢查是否發(fā)生超時,然后決定是否中斷任務執(zhí)行。throwIfReached就是一個同步超時檢測的方法。
理解 timeout 與 deadline 的區(qū)別
timeout中文意為“超時”,deadline中文意為“最后期限”,它們是有明顯區(qū)別的。 Timeout類中有一系列的timeoutXxx方法,timeoutXxx是用來設置**一次操作完成的最大等待時間。若這個操作在等待時間內(nèi)沒有結(jié)束,則認為超時。 deadlineXxx系列方法則是用來設置一項任務完成的最大等待時間。**意味著在未來多長時間內(nèi),需要將這項任務完成,否則認為超時。它可能包含一次或多次的操作。
讀取文件的例子
回顧下之前Okio讀取文件例子。
public void readFile() {
try {
FileInputStream fis = new FileInputStream("test.txt");
okio.Source source = Okio.source(fis);
BufferedSource bs = Okio.buffer(source);
source.timeout().deadline(1, TimeUnit.MILLISECONDS);
String res = bs.readUtf8();
System.out.println(res);
} catch (Exception e){
e.printStackTrace();
}
}在這個例子中,我們使用deadline設置了超時時間為1ms,這意味著從現(xiàn)在開始,讀取文件的這項任務,必須在未來的1ms內(nèi)完成,否則認為超時。而讀取文件的這項任務,就包含了多次的文件讀取操作。
搖骰子的例子
我們再來看下面這個搖骰子的程序。Dice是一個骰子類,roll方法表示搖骰子,搖出來的點數(shù)latestTotal不會超過12。rollAtFixedRate會開啟一個線程,每隔一段時間調(diào)用roll方法搖一次骰子。awaitTotal方法會當骰子的點數(shù)與我們傳遞進去的total值一樣或者超時而結(jié)束。
private class Dice {
Random random = new Random();
int latestTotal;
// 搖骰子
public synchronized void roll() {
latestTotal = 2 + random.nextInt(6) + random.nextInt(6);
System.out.println("Rolled " + latestTotal);
notifyAll();
}
// 開啟一個線程,每隔一段時間執(zhí)行 roll 方法
public void rollAtFixedRate(int period, TimeUnit timeUnit) {
Executors.newScheduledThreadPool(0).scheduleAtFixedRate(new Runnable() {
public void run() {
roll();
}
}, 0, period, timeUnit);
}
// 超時檢測
public synchronized void awaitTotal(Timeout timeout, int total) throws InterruptedIOException {
while (latestTotal != total) {
timeout.waitUntilNotified(this);
}
}
}timeout()是一個測試骰子類的方法,在主線程中運行。該程序設置每隔3s搖一次骰子,主線程設置超時時間為6s,期望搖到的點數(shù)是20。因為設置的超時是timeoutXxx系列的方法,所以這里超時的意思是“只要我搖一次骰子的時間不超過6s,那么我就不會超時,可以一直搖骰子”。因為搖出骰子的最大點數(shù)是12,而期望值是20,永遠也搖不出來20這個點數(shù),且搖一次骰子的時間是3s多,也不滿足超時的時間。所以主線程就會一直處于等待狀態(tài)。
public void timeout(){
try {
Dice dice = new Dice();
dice.rollAtFixedRate(3, TimeUnit.SECONDS);
Timeout timeout = new Timeout();
timeout.timeout(6, TimeUnit.SECONDS);
dice.awaitTotal(timeout, 20);
} catch (Exception e) {
e.printStackTrace();
}
}現(xiàn)在將timeout()方法修改一下,將timeout.timeout(6, TimeUnit.SECONDS)改為timeout.deadline(6, TimeUnit.SECONDS),之前我們說過deadlineXxx設置的超時**意味著在未來多長時間內(nèi),需要將這項任務完成。**在搖骰子這里的意思就是“從現(xiàn)在開始,我只可以搖6s的骰子。超過這個時間你還在搖,則認為超時”。它關(guān)注的是可以搖多久的骰子,而不是搖一次骰子不能超過多久的時間。
public void timeout(){
try {
Dice dice = new Dice();
dice.rollAtFixedRate(3, TimeUnit.SECONDS);
Timeout timeout = new Timeout();
timeout.deadline(6, TimeUnit.SECONDS);
dice.awaitTotal(timeout, 20);
} catch (Exception e) {
e.printStackTrace();
}
}上述程序,主線程會在6s后因超時而停止等待,結(jié)束運行。
等待直到喚醒
前面舉了兩個例子讓大家理解Okio中timeout和deadline的區(qū)別。在搖骰子的例子中用到了waitUntilNotified這個方法來檢測超時,中文意思為“等待直到喚醒”。也就是Java多線程中經(jīng)典的“等待-喚醒”機制,該機制常常用于多線程之間的通信。調(diào)用waitUntilNotified方法的線程會一直處于等待狀態(tài),除非被喚醒或者因超時而拋出異常。下面是該方法的源碼。
public final void waitUntilNotified(Object monitor) throws InterruptedIOException {
try {
boolean hasDeadline = hasDeadline();
long timeoutNanos = timeoutNanos();
// 若沒有設置 deadline && timeout,則一直等待直到喚醒
if (!hasDeadline && timeoutNanos == 0L) {
monitor.wait(); // There is no timeout: wait forever.
return;
}
// Compute how long we'll wait.
// 計算等待的時長,若同時設置了deadline 和 timeout,則 deadline 優(yōu)先
long waitNanos;
long start = System.nanoTime();
if (hasDeadline && timeoutNanos != 0) {
long deadlineNanos = deadlineNanoTime() - start;
waitNanos = Math.min(timeoutNanos, deadlineNanos);
} else if (hasDeadline) {
waitNanos = deadlineNanoTime() - start;
} else {
waitNanos = timeoutNanos;
}
// Attempt to wait that long. This will break out early if the monitor is notified.
long elapsedNanos = 0L;
if (waitNanos > 0L) {
long waitMillis = waitNanos / 1000000L;
// 等待 waitNanos
monitor.wait(waitMillis, (int) (waitNanos - waitMillis * 1000000L));
// 計算從等待 waitNanos 到喚醒所用時間
elapsedNanos = System.nanoTime() - start;
}
// Throw if the timeout elapsed before the monitor was notified.
// 若等待了 waitNanos 還沒喚醒,認為超時
if (elapsedNanos >= waitNanos) {
throw new InterruptedIOException("timeout");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Retain interrupted status.
throw new InterruptedIOException("interrupted");
}
}查看waitUntilNotified的源碼,我們發(fā)現(xiàn)該方法基于“等待-通知”機制,添加了多線程之間的超時檢測功能,一個線程用來執(zhí)行具體的任務,一個線程調(diào)用該方法來檢測超時。在Okio中的管道就使用了waitUntilNotified這個方法。
AsyncTimeout 類的設計
AsyncTimeout內(nèi)部維護一個單鏈表,節(jié)點的類型是AsyncTimeout,以到超時之前的剩余時間升序排序,即超時的剩余時間越大,節(jié)點就在鏈表越后的位置。對鏈表的操作,使用了synchronized關(guān)鍵字加類鎖,保證在同一時間,只有一個線程可以對鏈表進行修改訪問操作。
AsyncTimeout實現(xiàn)了Okio的異步超時檢測。這里的異步指的是“任務執(zhí)行”和“超時檢測”是異步的,在執(zhí)行任務的同時,也在進行任務的“超時檢測”。你會覺得這和上面搖骰子的例子很像,一個線程執(zhí)行任務,一個線程檢測超時。事實上,AsyncTimeout也正是這樣實現(xiàn)的,它內(nèi)部的Watchdog線程就是用來檢測超時的。當我們要對一次操作或一項任務設置超時,使用成對的enter()和exit(),模板代碼如下。
enter(); // do something exit();
若上面do something的操作超時,timedOut()方法將會在Watchdog線程被回調(diào)。可以看見,這種包裹性的模板代碼,靈活性很大,我們幾乎可以在其中放置任何想要檢測超時的一個或多個操作。
AsyncTimeout 成員變量
下面是AsyncTimeout類主要的成員變量。
private static final long IDLE_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(60); static @Nullable AsyncTimeout head; private boolean inQueue; private @Nullable AsyncTimeout next; private long timeoutAt;
IDLE_TIMEOUT_MILLIS,在單鏈表中沒有節(jié)點時,Watchdog線程等待的時間head,單鏈表的頭結(jié)點,是一個虛假節(jié)點。當鏈表中只存在該節(jié)點,認為該鏈表為空。inQueue,當前節(jié)點是否在鏈表中。next,當前節(jié)點的下一個節(jié)點。timeoutAt,以當前時間為基準,當前節(jié)點在將來何時超時。
AsyncTimeout 成員方法
scheduleTimeout 有序的將超時節(jié)點加入到鏈表中
scheduleTimeout方法可以將一個超時節(jié)點按照超時的剩余時間有序的插入到鏈表當中。注意該方法使用synchronized修飾,是一個同步方法,可以保證對鏈表的操作是線程安全的。
private static synchronized void scheduleTimeout(AsyncTimeout node, long timeoutNanos, boolean hasDeadline) {
// Start the watchdog thread and create the head node when the first timeout is scheduled.
// 若 head 節(jié)點為 null, 初始化 head 并啟動 Watchdog 線程
if (head == null) {
head = new AsyncTimeout();
new Watchdog().start();
}
// 計算 node 節(jié)點的 timeoutAt 值
long now = System.nanoTime();
if (timeoutNanos != 0 && hasDeadline) {
// Compute the earliest event; either timeout or deadline. Because nanoTime can wrap around,
// Math.min() is undefined for absolute values, but meaningful for relative ones.
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();
}
// Insert the node in sorted order.
// 返回 node 節(jié)點的超時剩余時間
long remainingNanos = node.remainingNanos(now);
// 從 head 節(jié)點開始遍歷鏈表, 將 node 節(jié)點插入到合適的位置
for (AsyncTimeout prev = head; true; prev = prev.next) {
// 若當前遍歷的節(jié)點下一個節(jié)點為 null 或者 node 節(jié)點的超時剩余時間小于下一個節(jié)點
if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)) {
// 將 node 節(jié)點插入到鏈表
node.next = prev.next;
prev.next = node;
// 若當前遍歷的節(jié)點是 head, 喚醒 watchdog 線程
if (prev == head) {
AsyncTimeout.class.notify(); // Wake up the watchdog when inserting at the front.
}
break;
}
}
}Watchdog 線程
在scheduleTimeout方法中,若head為null,則會初始化head并啟動Watchdog線程。Watchdog是一個守護線程,因此它會隨著JVM進程的結(jié)束而結(jié)束。前面我們說過Watchdog線程是用來檢測超時的,它會逐個檢查鏈表中的超時節(jié)點是否超時,直到鏈表中所有節(jié)點檢查完畢后結(jié)束運行。
private static final class Watchdog extends Thread {
Watchdog() {
super("Okio Watchdog");
setDaemon(true);
}
public void run() {
while (true) {
try {
// 超時的節(jié)點
AsyncTimeout timedOut;
// 加鎖,同步代碼塊
synchronized (AsyncTimeout.class) {
// 等待節(jié)點超時
timedOut = awaitTimeout();
// Didn't find a node to interrupt. Try again.
// 當前該節(jié)點沒有超時,繼續(xù)檢查
if (timedOut == null) continue;
// The queue is completely empty. Let this thread exit and let another watchdog thread
// get created on the next call to scheduleTimeout().
// 鏈表中已經(jīng)沒有超時節(jié)點,結(jié)束運行
if (timedOut == head) {
head = null;
return;
}
}
// Close the timed out node.
// timedOut 節(jié)點超時,回調(diào) timedOut() 方法
timedOut.timedOut();
} catch (InterruptedException ignored) {
}
}
}
}awaitTimeout 等待節(jié)點超時
在Watchdog線程中會調(diào)用awaitTimeout方法來等待檢測的節(jié)點超時,若檢測的節(jié)點沒有超時,該方法返回null。否則返回超時的節(jié)點。
static @Nullable AsyncTimeout awaitTimeout() throws InterruptedException {
// Get the next eligible node.
// 檢測的節(jié)點
AsyncTimeout node = head.next;
// The queue is empty. Wait until either something is enqueued or the idle timeout elapses.
// 若鏈表為空
if (node == null) {
long startNanos = System.nanoTime();
// Watchdog 線程等待 60s,期間會釋放類鎖
AsyncTimeout.class.wait(IDLE_TIMEOUT_MILLIS);
// 等待 60s 后若鏈表還為空則返回 head,否則返回 null
return head.next == null && (System.nanoTime() - startNanos) >= IDLE_TIMEOUT_NANOS
? head // The idle timeout elapsed.
: null; // The situation has changed.
}
// node 節(jié)點超時剩余的時間
long waitNanos = node.remainingNanos(System.nanoTime());
// The head of the queue hasn't timed out yet. Await that.
// node 節(jié)點超時剩余的時間 > 0,說明 node 還未超時,繼續(xù)等待 waitNanos 后返回 null
if (waitNanos > 0) {
// Waiting is made complicated by the fact that we work in nanoseconds,
// but the API wants (millis, nanos) in two arguments.
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
AsyncTimeout.class.wait(waitMillis, (int) waitNanos);
return null;
}
// The head of the queue has timed out. Remove it.
// node 節(jié)點超時了,將 node 從鏈表中移除并返回
head.next = node.next;
node.next = null;
return node;
}enter 進入超時檢測
分析完上面三個方法后再來看enter就非常的簡單了,enter內(nèi)部調(diào)用了scheduleTimeout方法來添加一個超時節(jié)點到鏈表當中,而Watchdog線程隨即會開始檢測超時。
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
inQueue = true;
scheduleTimeout(this, timeoutNanos, hasDeadline);
}exit 退出超時檢測
前面說過,enter和exit在檢測超時是需要成對出現(xiàn)的。它們之間的代碼就是需要檢測超時的代碼。exit方法的返回值表示enter和exit中間檢測的代碼是否超時。
public final boolean exit() {
if (!inQueue) return false;
// 更新 inQueue 為 false
inQueue = false;
return cancelScheduledTimeout(this);
}cancelScheduledTimeout方法會將當前的超時節(jié)點從鏈表中移除。為了保證對鏈表的操作是線程安全的,該方法也是一個同步方法。我們知道在awaitTimeout方法中,若某個節(jié)點超時了會將它從鏈表中移除。那么當調(diào)用cancelScheduledTimeout發(fā)現(xiàn)node不在鏈表中,則一定表明node超時了。
private static synchronized boolean cancelScheduledTimeout(AsyncTimeout node) {
// Remove the node from the linked list.
// 若 node 在鏈表中,將其移除。
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!
// node 不在鏈表中,則 node 一定超時了,返回 true
return true;
}總結(jié)
本文詳細講解了Okio中超時機制的實現(xiàn)原理,主要是Timeout和AsyncTimeout類的源碼分析與解讀。相信大家已經(jīng)掌握了這部分知識,現(xiàn)總結(jié)一下文中要點。
- Okio 基于等待-喚醒機制,使用
Watchdog線程來檢測超時。 - 當要對某項操作或任務進行超時檢測時,將它們放到
enter和exit的中間。 - Okio 對鏈表的使用非常頻繁,在文件讀寫和超時檢測都使用到了鏈表這個結(jié)構(gòu)。
以上就是深入了解Android Okio的超時機制的詳細內(nèi)容,更多關(guān)于Android Okio超時機制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android中通過view方式獲取當前Activity的屏幕截圖實現(xiàn)方法
這篇文章主要介紹了Android中通過view方式獲取當前Activity的屏幕截圖實現(xiàn)方法,本文方法相對簡單,容易理解,需要的朋友可以參考下2014-09-09
Android開發(fā)實現(xiàn)AlertDialog中View的控件設置監(jiān)聽功能分析
這篇文章主要介紹了Android開發(fā)實現(xiàn)AlertDialog中View的控件設置監(jiān)聽功能,結(jié)合實例形式分析了Android針對AlertDialog中的控件使用View進行監(jiān)聽的相關(guān)操作技巧,需要的朋友可以參考下2017-11-11
android app判斷是否有系統(tǒng)簽名步驟詳解
這篇文章主要為大家介紹了android app判斷是否有系統(tǒng)簽名步驟詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-11-11
Android自定義view實現(xiàn)列表內(nèi)左滑刪除Item
這篇文章主要介紹了微信小程序列表中item左滑刪除功能,本文分步驟給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2023-02-02

