首頁 > 軟體

Android協程作用域與序列發生器限制介紹梳理

2022-08-25 22:00:36

一.受限協程作用域

協程的基礎與使用中提到,可以通過sequence方法構建一個序列發生器。但當在sequence方法中呼叫除了yield方法與yieldAll方法以外的其他掛起方法時,就會報錯。比如在sequence方法中呼叫delay方法,就會產生下面的報錯提示:

翻譯過來大致是“受限的掛起方法只能呼叫自身受限的協程作用域內的成員變數或掛起方法。這是什麼意思呢?

1.sequence方法

sequence方法就是構建序列發生器用到的方法,內部通過Sequence方法實現,程式碼如下:

@SinceKotlin("1.3")
public fun <T> sequence(@BuilderInference block: suspend SequenceScope<T>.() -> Unit): Sequence<T> = Sequence { iterator(block) }

其中引數block是一個在SequenceScope環境下的lambda表示式。

2.SequenceScope類

// 注意
@RestrictsSuspension
@SinceKotlin("1.3")
public abstract class SequenceScope<in T> internal constructor() {
    // 向迭代器中提供一個數值
    public abstract suspend fun yield(value: T)
    // 向迭代器中提供一組數值
    public abstract suspend fun yieldAll(iterator: Iterator<T>)
    // 向迭代器中提供Collection型別的一組數值
    public suspend fun yieldAll(elements: Iterable<T>) {
        if (elements is Collection && elements.isEmpty()) return
        return yieldAll(elements.iterator())
    }
    // 向迭代器中提供Sequence型別的一組數值
    public suspend fun yieldAll(sequence: Sequence<T>) = yieldAll(sequence.iterator())
}

SequenceScope類是一個獨立的抽象類,沒有繼承任何的類。它提供了四個方法,只要都是用來向外提供數值或物件。而該類成為受限協程作用域的關鍵在於該類被RestrictsSuspension註解修飾,程式碼如下:

@SinceKotlin("1.3")
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
public annotation class RestrictsSuspension

RestrictsSuspension註解用於修飾一個類或介面,表示該類是受限的。在被該註解修飾的類的擴充套件掛起方法中,只能呼叫該註解修飾的類中定義的掛起方法,不能呼叫其他類的掛起方法。

具體的,在sequence方法中,block就是SequenceScope類的擴充套件方法,因此在block中,只能使用SequenceScope類中提供的掛起方法——yield方法和yieldAll方法。同時,SequenceScope類的構造器被internal修飾,無法在外部被繼承,因此也就無法定義其他的掛起方法。

為什麼受限協程作用域不允許呼叫其他的掛起方法呢?

因為當一個方法掛起協程時,會獲取協程的續體,同時協程需要等待方法執行完畢後的回撥,這意味著會暴露協程的續體。可能會造成掛起協程執行的不確定性。

二.序列發生器

1.Sequence介面

首先來分析一下Sequence介面,程式碼如下:

public interface Sequence<out T> {
    public operator fun iterator(): Iterator<T>
}

2.Sequence方法

在協程中,有一個與Sequence介面同名的方法,該方法用於返回一個實現了Sequence介面的物件,程式碼如下:

@kotlin.internal.InlineOnly
public inline fun <T> Sequence(crossinline iterator: () -> Iterator<T>): Sequence<T> = object : Sequence<T> {
    override fun iterator(): Iterator<T> = iterator()
}

Sequence方法返回了一個匿名物件,並通過引數中的lambda表示式iterator實現了介面中的iterator方法。

從sequence方法的程式碼可以知道,用於構建序列發生器的sequence方法內部呼叫了Sequence方法,同時還呼叫了iterator方法,將返回的Iterator物件,作為Sequence方法的引數。

3.iterator方法

@SinceKotlin("1.3")
public fun <T> iterator(@BuilderInference block: suspend SequenceScope<T>.() -> Unit): Iterator<T> {
    val iterator = SequenceBuilderIterator<T>()
    iterator.nextStep = block.createCoroutineUnintercepted(receiver = iterator, completion = iterator)
    return iterator
}

iterator方法內部建立了一個SequenceBuilderIterator物件,並且通過createCoroutineUnintercepted方法建立了一個協程,儲存到了SequenceBuilderIterator物件的nextStep變數中。可以發現,序列發生器的核心實現都在SequenceBuilderIterator類中。

4.SequenceBuilderIterator類

SequenceBuilderIterator類是用於對序列發生器進行迭代,在該類的內部對狀態進行了劃分,程式碼如下:

private typealias State = Int
// 沒有要發射的資料
private const val State_NotReady: State = 0
private const val State_ManyNotReady: State = 1
// 有要發射的資料
private const val State_ManyReady: State = 2
private const val State_Ready: State = 3
// 資料全部發射完畢
private const val State_Done: State = 4
// 發射過程中出錯
private const val State_Failed: State = 5

狀態轉移圖如下:

迭代器的初始狀態為State_NotReady,由於首次發射沒有資料,因此會進入State_Failed狀態。

State_Failed狀態會從序列發生器中獲取資料,如果是通過yield方法獲取的資料,則會進入State_Ready狀態,如果是通過yieldAll方法獲取的資料,則會進入State_ManyReady狀態。

當從序列發生器中獲取資料時,如果是在State_ManyReady和State_Ready狀態,則直接發射一個資料,對應的進入到State_ManyNotReady和State_NotReady狀態。如果是在State_ManyNotReady和State_NotReady狀態,則會判斷是否有資料,如果有資料則對應進入到State_ManyReady和State_Ready狀態。如果沒有則進入到State_Failed狀態,獲取資料。

當序列發生器發射完畢時,會進入State_Done狀態。

接下來對SequenceBuilderIterator類進行分析。

1.SequenceBuilderIterator類的全域性變數

SequenceBuilderIterator類繼承自SequenceScope類,實現了Iterator介面和Continuation介面。程式碼如下:

private class SequenceBuilderIterator<T> : SequenceScope<T>(), Iterator<T>, Continuation<Unit> {
    // 迭代器的狀態
    private var state = State_NotReady
    // 迭代器下一個要傳送的值
    private var nextValue: T? = null
    // 用於儲存yieldAll方法傳入的迭代器
    private var nextIterator: Iterator<T>? = null
    // 用於獲取下一個資料的續體
    var nextStep: Continuation<Unit>? = null
    ...
    // 空的上下文
    override val context: CoroutineContext
        get() = EmptyCoroutineContext
}

為什麼SequenceBuilderIterator類的上下文是空的呢?

因為SequenceBuilderIterator類繼承了SequenceScope類,因此該類也是受限的,因此不允許在類的擴充套件方法中呼叫類內以外的掛起方法。自然也就不能進行排程、攔截等操作,所以上下文為空。在協程中,受限協程的上下文一般都是空上下文。

2.yield方法與yieldAll方法

yield方法與yieldAll方法是SequenceScope類中定義的兩個方法,在SequenceBuilderIterator類中的實現如下:

// 發射一個資料
override suspend fun yield(value: T) {
    // 儲存資料到全域性變數中
    nextValue = value
    // 修改狀態
    state = State_Ready
    // 掛起協程,獲取續體
    return suspendCoroutineUninterceptedOrReturn { c ->
        // 儲存續體到全域性變數中
        nextStep = c
        // 掛起
        COROUTINE_SUSPENDED
    }
}
// 發射多個資料
override suspend fun yieldAll(iterator: Iterator<T>) {
    // 如果迭代器沒有資料,則直接返回
    if (!iterator.hasNext()) return
    // 如果有資料,則儲存到全域性變數
    nextIterator = iterator
    // 修改狀態
    state = State_ManyReady
    // 掛起協程,獲取續體
    return suspendCoroutineUninterceptedOrReturn { c ->
        // 儲存續體到全域性變數中
        nextStep = c
        // 掛起
        COROUTINE_SUSPENDED
    }
}

通過上面的程式碼可以知道,yield方法和yieldAll方法主要做了三件事情,掛起協程、修改狀態、儲存要傳送的資料和續體。而yieldAll發射多個資料原理在於儲存了引數中Iterator介面指向的物件,通過迭代器獲取資料。

3.hasNext方法

hasNext方法是Iterator介面中定義的方法,用於迭代時判斷是否還有資料,程式碼如下:

override fun hasNext(): Boolean {
    // 迴圈
    while (true) {
        // 判斷狀態
        when (state) {
            // 剛通過yield方法發射資料
            State_NotReady -> {}
            // 剛通過yieldAll方法發射資料
            State_ManyNotReady ->
                // 如果迭代器中還有資料
                if (nextIterator!!.hasNext()) {
                    // 修改狀態,返回true
                    state = State_ManyReady
                    return true
                } else {
                    // 沒有資料,則置空,丟棄迭代器
                    nextIterator = null
                }
            // 如果序列發生器已經發射完資料,返回false
            State_Done -> return false
            // 如果有資料,則直接返回true
            State_Ready, State_ManyReady -> return true
            // 其他狀態,則丟擲異常
            else -> throw exceptionalState()
        }
        // 走到這裡,說明需要去獲取下一個資料
        // 修改狀態
        state = State_Failed
        // 獲取全域性儲存的續體
        val step = nextStep!!
        // 置空
        nextStep = null
        // 恢復序列發生器的執行,直到遇到yield方法或yieldAll方法掛起
        step.resume(Unit)
    }
}
// 異常狀態的處理
private fun exceptionalState(): Throwable = when (state) {
    State_Done -> NoSuchElementException()
    State_Failed -> IllegalStateException("Iterator has failed.")
    else -> IllegalStateException("Unexpected state of the iterator: $state")
}

4.next方法

next方法也是Iterator介面中定義的方法,用於在迭代器中存在資料時獲取資料,程式碼如下:

override fun next(): T {
    // 判斷狀態
    when (state) {
        // 如果當前處於已經發射完資料的狀態,則判斷是否有資料
        State_NotReady, State_ManyNotReady -> return nextNotReady()
        // 如果通過yieldAll方法獲取到了資料
        State_ManyReady -> {
            // 修改狀態
            state = State_ManyNotReady
            // 通過迭代器獲取資料
            return nextIterator!!.next()
        }
        // 如果通過yield方法獲取到了資料
        State_Ready -> {
            // 修改狀態
            state = State_NotReady
            // 獲取儲存的資料並進行型別轉換
            @Suppress("UNCHECKED_CAST")
            val result = nextValue as T
            // 全域性變數置空
            nextValue = null
            // 返回資料
            return result
        }
        // 其他情況,則丟擲異常
        else -> throw exceptionalState()
    }
}
// 如果沒有資料,則丟擲異常,有資料,則返回資料
private fun nextNotReady(): T {
    if (!hasNext()) throw NoSuchElementException() else return next()
}

5.總結

當使用序列發生器進行迭代時,首先會呼叫hasNext方法,hasNext方法會通過儲存的續體,恢復序列發生器所在的協程繼續執行,獲取下一次待發射的資料。如果獲取了到資料,則會返回true,這樣之後通過next方法就可以獲取到對應的資料。

當序列發生器所在的協程在執行中遇到yield方法時,會發生掛起,同時將下一次待發射的資料儲存起來。如果遇到的是yieldAll方法,則儲存的是迭代器,下一次發射資料時會從迭代器中獲取。

到此這篇關於Android協程作用域與序列發生器限制介紹梳理的文章就介紹到這了,更多相關Android協程作用域內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com