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

