Suspend函式與回撥的互相轉換

語言: CN / TW / HK

theme: cyanosis

前言

我們再來一期關於kotlin協程的故事,我們都知道在Coroutine沒有出來之前,我們對於非同步結果的處理都是採用回撥的方式進行,一方面回撥層次過多的話,容易導致“回撥地獄”,另一方法也比較難以維護。當然,我們並不是否定了回撥本身,回撥本身同時也是具備很多優點的,比如符合程式碼閱讀邏輯,同時回撥本身也是比較可控的。這一期呢,我們就是來聊一下,如何把回撥的寫法變成suspend函式,同時如何把suspend函式變成回撥,從而讓我們更加了解kotlin協程背後的故事

回撥變成suspend函式

來一個回撥

我們以一個回撥函式作為例子,當我們normalCallBack在一個子執行緒中做一些處理,比如耗時函式,做完就會通過MyCallBack回撥onCallBack,這裡返回了一個Int型別,如下: var myCallBack:MyCallBack?= null interface MyCallBack{ fun onCallBack(result: Int) } fun normalCallBack(){ thread { // 比如做一些事情 myCallBack?.onCallBack(1) } }

轉化為suspend函式

此時我們可以通過suspendCoroutine函式,內部其實是通過建立了一個SafeContinuation並放到了我們suspend函式本身(block本身)啟動了一個協程,我們之前在聊一聊Kotlin協程"低階"api 這篇文章介紹過 public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return suspendCoroutineUninterceptedOrReturn { c: Continuation<T> -> val safe = SafeContinuation(c.intercepted()) block(safe) safe.getOrThrow() } } 這時候我們就可以直接寫為,從而將回調消除,變成了一個suspend函式。 suspend fun mySuspend() = suspendCoroutine<Int> { thread { // 比如做一些事情 it.resume(1) } } 當然,如果我們想要支援一下外部取消,比如當前頁面銷燬時,發起的網路請求自然也就不需要再請求了,就可以通過suspendCancellableCoroutine建立,裡面的Continuation物件就從SafeContinuation(見上文)變成了CancellableContinuation,變成了CancellableContinuation有一個invokeOnCancellation方便,支援在協程體被銷燬時的邏輯。 public suspend inline fun <T> suspendCancellableCoroutine( crossinline block: (CancellableContinuation<T>) -> Unit ): T = suspendCoroutineUninterceptedOrReturn { uCont -> val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE) /* * For non-atomic cancellation we setup parent-child relationship immediately * in case when `block` blocks the current thread (e.g. Rx2 with trampoline scheduler), but * properly supports cancellation. */ cancellable.initCancellability() block(cancellable) cancellable.getResult() } 此時我們就可以寫出以下程式碼

suspend fun mySuspend2() = suspendCancellableCoroutine<Int> { thread { // 比如做一些事情 it.resume(1) } it.invokeOnCancellation { // 取消邏輯 } }

suspend函式變成回撥

見到了回撥如何變成suspend函式,那麼我們反過來呢?有沒有辦法?當然有啦!當時suspend函式中有很多種區分,我們一一區分一下 直接返回的suspend函式 suspend fun myNoSuspendFunc():Int{ return 1 } ``` 呼叫suspendCoroutine後直接resume的suspend函式 suspend fun myNoSuspendFunc() = suspendCoroutine {

    continuation ->
continuation.resume(1)

} 呼叫suspendCoroutine後非同步執行的suspend函式(這裡非同步可以是單執行緒也可以是多執行緒,跟執行緒本身無關,只要是非同步就會觸發掛起) suspend fun myRealSuspendFunc() = suspendCoroutine { thread { Thread.sleep(300) it.resume(2) } ``` 那麼我們來想一下,這裡真正發起掛起的函式是哪個?通過程式碼其實我們可以猜到,真正掛起的函式只有最後一個myRealSuspendFunc,其他都不是真正的掛起,這裡的掛起是什麼意思呢?我們從協程的狀態就可以知道,當前處於CoroutineSingletons.COROUTINE_SUSPENDED時,就是掛起狀態。我們迴歸一下,一個suspend函式有哪幾種情況

image.png

這裡的1,2,3就分別對應著上文demo中的例子 1. 直接返回結果,不需要進入狀態機判斷,因為本身就沒有啟動協程 2. 進入了協程,但是不需要進行SUSPEND狀態就已經有了結果,所以直接返回了結果 3. 進入了SUSPEND狀態,之後才能獲取結果

這裡我們就不貼出來原始碼了,感興趣可自己看Coroutine的實現,這裡我們要明確一個概念,一個Suspend函式的執行機制,其實並不依靠了協程本身。

對應程式碼表現就是,這個函式的返回結果可能就是直接返回結果本身,另一種就是通過回撥本身通知外部(這裡我們還會以例子說明)

suspend函式轉換為回撥

這裡有兩種情況,我們分別以kotlin程式碼跟java程式碼表示:

kotlin程式碼

由於kotlin可以直接通過suspend的擴充套件函式startCoroutine啟動一個協程, ``` fun myRealSuspendCallBack(){ ::myRealSuspendFunc.startCoroutine(object :Continuation{ 當前環境 override val context: CoroutineContext

        get() = Dispatchers.IO
    結果
    override fun resumeWith(result: Result<Int>) {

        if(result.isSuccess){
            myCallBack?.onCallBack(result.getOrDefault(0))
        }

    }
})

} ``` 其中Result就是一個內聯類,屬於kotlin編譯器新增的裝飾類,在這裡我們無論是1,2,3的情況,都可以在resumeWith 中獲取到結果,在這裡通過callback回撥即可

``` @JvmInline public value class Result @PublishedApi internal constructor( @PublishedApi internal val value: Any? ) : Serializable {

```

Java程式碼

這裡我們更正一個誤區,就是suspend函式只能在kotlin中使用/Coroutine協程只能在kotlin中使用,這個其實是錯誤的,java程式碼也能夠調起協程,只不過麻煩了一點,至少官方是沒有禁止的。 比如我們需要呼叫startCoroutine,可直接呼叫 ContinuationKt.startCoroutine();

當然,我們也能夠直接呼叫suspend函式 ``` Object result = CallBack.INSTANCE.myRealSuspendFunc(new Continuation() { @NonNull @Override public CoroutineContext getContext() { 這裡啟動的環境其實協程沒有用到,讀者們可以思考一下為什麼!這裡就當一個謎題啦!可以在評論區說出你的想法(我會在評論區解答) return (CoroutineContext) Dispatchers.getIO(); //return EmptyCoroutineContext.INSTANCE;

}

@Override
public void resumeWith(@NonNull Object o) {
    情況3
    Log.e("hello","resumeWith result is "+ o +" is main "+ (Looper.myLooper() == Looper.getMainLooper()));

      // 回撥處理即可
      myCallBack?.onCallBack(result)
}

});

if(result != IntrinsicsKt.getCOROUTINE_SUSPENDED()){ 情況1,2 Log.e("hello","func result is "+ result); // 回撥處理即可 myCallBack?.onCallBack(result) } ```

這裡我們需要注意的是,這裡java程式碼比kotlin多了一個判斷,同時resumeWith的引數不再是Result,而是一個Object

if(result != IntrinsicsKt.getCOROUTINE_SUSPENDED()){ } 這裡脫去了kotlin給我們新增的各種外殼,其實這就是真正的對於suspend結果的處理(只不過kotlin幫我們包了一層)

我們上文說過,suspend函式對應的三種情況,這裡的1,2都是直接返回結果的,因為沒有走到SUSPEND狀態(IntrinsicsKt.getCOROUTINE_SUSPENDED())這裡需要讀者好好閱讀上文,因此 result != IntrinsicsKt.getCOROUTINE_SUSPENDED(),就會直接走到這裡,我們就直接拿到了結果 if(result != IntrinsicsKt.getCOROUTINE_SUSPENDED()){ }

如果屬於情況3,那麼這裡的result就不再是一個結果,而是當前協程的狀態標記罷了,此時當協程完成執行的時候(呼叫resume的時候),就會回撥到resumeWith,這裡的Object型別o才是經過SUSPEND狀態的結果!

總結

經過我們suspend跟回撥的互相狀態,能夠明白了suspend背後的邏輯與掛起的細節,希望能幫到你!最後本篇還留下了一個小謎題,可以發揮你的理解在評論區說出你的想法!筆者之後會在評論區解答!

本文正在參加「金石計劃 . 瓜分6萬現金大獎」