前面有兩篇文章介紹過協程,加上這篇,基本上介紹得差不多了~
深入理解 Kotlin Coroutine
深入理解 Kotlin Coroutine (2)
上周在北京的活動上給大家分享了一下協程,發現大家對於協程最大的困惑是:我為什麼要用它。。。
正好,前面一直想要再寫篇協程的文章,這次讓我們再來回顧一下其中的一些問題。
我們為什麼要用協程?這個問題對於新接觸協程的朋友來說確實很容易讓人困惑,那麼我們就來看看協程給我們帶來什麼吧。
協程是一種語法糖 協程的出現是來解決異步問題的,但它本身卻不提供異步的能力,額這就很搞笑了,你來自於猴子與逗比嗎?當然不是,協程某種意義上更像是一種語法糖,它為我們隱藏了異步調用和回調的細節,讓我們更關注於業務邏輯的實現。
協程讓代碼更簡潔 協程可以允許我們用同步代碼的方式寫出異步代碼的功能。
async{ val bitmap = await{ loadImage(url) }
}
這是一段 Android 的代碼示例,請注意這個賦值操作,它實際上是切換到 UI 線程之後運行的,而 await 當中的 loadImage(url) 卻是在 IO 線程中運行,所以我們一方面知道協程的異步功能是有線程在後面支持的,另一方面我們也知道異步線程回調可以用協程直接簡化為一個簡單的賦值。
協程讓異步異常處理更方便 如果你的異步代碼出現異常,通常你會在你的回調中加入一個 onError 來傳遞這個異常信息:
interface Callback{ fun onError(e: CertainException) fun onSuccess(data: Data)
}
而在協程的只支持下,我們只要按照同步代碼的異常捕獲方式進行捕獲就可以了:
async{ try{ val bitmap = await{ loadImage(url) } }catch(e: CertainException){ ... }
}
協程更輕量級 通常比較傳統的伺服器實現,一個用戶請求接入後會給他開一個線程來等待和處理它的請求。這樣是非常不經濟的,因為這個用戶有可能就是來逗你玩的,建立連接之後半天來一個字節,就這樣你還得付出一個線程的代價來服務他,尷尬。如果用協程,那麼就要輕量多了,因為協程只是一塊兒內存,不像線程那樣還要對應作業系統內核線程(印象中 Hotpot 的實現就是這樣)。
用協程的理由我個人感覺也就這些了。一句話,協程是一種輕量級的方便操作異步代碼的語法糖,而它本身不提供異步能力。
為什麼說它是語法糖?async{ val bitmap = await{ loadImage(url) } }
這樣一句代碼其實在編譯完之後會生成一些新的類,async 後面的 Lambda 就會被編譯成一個 CoroutineImpl 類的子類實例,大家只需要按照我經常提到的查看 Kotlin 字節碼的方法去看看就知道了。
那麼這個類究竟是個什麼呢?
abstract class CoroutineImpl( arity: Int, @JvmField protected var completion: Continuation<Any?>? ) : Lambda(arity), Continuation<Any?> { @JvmField protected var label: Int = if (completion != null) 0 else -1 private val _context: CoroutineContext? = completion?.context override val context: CoroutineContext get() = _context!! private var _facade: Continuation<Any?>? = null val facade: Continuation<Any?> get() { if (_facade == null) _facade = interceptContinuationIfNeeded(_context!!, this) return _facade!! } override fun resume(value: Any?) { processBareContinuationResume(completion!!) { doResume(value, null) } } override fun resumeWithException(exception: Throwable) { processBareContinuationResume(completion!!) { doResume(null, exception) } } protected abstract fun doResume(data: Any?, exception: Throwable?): Any? open fun create(completion: Continuation<*>): Continuation<Unit> { throw IllegalStateException("create(Continuation) has not been overridden") } open fun create(value: Any?, completion: Continuation<*>): Continuation<Unit> { throw IllegalStateException("create(Any?;Continuation) has not been overridden") } }
它首先是個 Lambda ,這沒毛病。其次,它是一個 Continuation,了解協程的朋友似乎要知道什麼了,沒錯,這貨與我們自己在啟動協程時傳入的 Continuation 實例是同樣的東西,而且我們可以注意到構造方法當中有一個叫做 completion 的欄位,不要驚訝,那就是我們傳入的 Continuation。
實際上,我們通過編譯器編譯出來的字節碼發現,create 方法當中會通過我們的這個 Lambda 表達式創建一個新的 CoroutineImpl 實例,而 doResume 這個抽象方法其實就是我們的 Lambda 表達式的內容了。
在這裡我們還看到了 facade:
val facade: Continuation<Any?> get() { if (_facade == null) _facade = interceptContinuationIfNeeded(_context!!, this) return _facade!! }
其中 interceptContinuationIfNeeded 當中就會處理各個攔截器,來完成線程調度或者其他操作,也就是說 facade 返回的 Continuation 實例就是經過類似下面這樣的攔截器返回的實例了:
class UIContext : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { override fun <T> interceptContinuation(continuation: Continuation<T>) = ... }
協程是如何啟動的?我們再來看看協程是如何啟動的。我們啟動協程的時候通常會調用 startCoroutine 或者 createCoroutine,它們都會調用到一個方法:
public fun <T> (suspend () -> T).createCoroutineUnchecked( completion: Continuation<T> ): Continuation<Unit> = if (this !is CoroutineImpl) ... else (this.create(completion) as CoroutineImpl).facade
我們已經知道我們的調用者實際上就是一個 CoroutineImpl 的實例,我們只需要關注 else 分支即可,這時候調用我們前面已經見到的 create 方法再創建一個 CoroutineImpl 實例,並把 completion 這個對象傳給它,而 facade 實際上就會觸發攔截器的操作,最終返回的就是經過攔截器處理之後的 Continuation 實例了。
創建完協程之後,就是啟動的邏輯:
public fun <T> (suspend () -> T).startCoroutine( completion: Continuation<T> ) { createCoroutineUnchecked(completion).resume(Unit) }
直接調用 resume 方法,結果怎樣呢?由於攔截器都是我們自己提供的,比較直觀,我們暫且不提,通常情況下,這個 resume 方法最終本質上調用的還是 CoroutineImpl 的 resume 方法:
override fun resume(value: Any?) { processBareContinuationResume(completion!!) { doResume(value, null) } }
internal inline fun processBareContinuationResume(completion: Continuation<*>, block: () -> Any?) { try { val result = block() if (result !== COROUTINE_SUSPENDED) { (completion as Continuation<Any?>).resume(result) } } catch (t: Throwable) { completion.resumeWithException(t) } }
processBareContinuationResume 會首先觸發一次 doResume 的調用,這個調用也就是我們自己的協程代碼了,直到遇到第一個 suspend 調用,那麼這時候協程就會被掛起,等待異步操作執行完成之後再來調用我們的 resume/resumeWithException 方法來通知我們數據回來了或者異常發生了。這個過程直到整個協程執行流程結束。
我們稍微關注一下 Continuation 接口:
public interface Continuation<in T> { public val context: CoroutineContext public fun resume(value: T) public fun resumeWithException(exception: Throwable) }
再來看看我們通常的回調版本:
interface Callback<T>{ fun onError(e: Exception) fun onSuccess(data: T) }
除了協程上下文之外,剩下的兩個方法與我們的回調又有什麼區別呢?
小結協程是什麼?它就是用來簡化你的異步回調代碼的語法糖!
如果你有興趣加入我們,請直接關注公眾號 Kotlin ,或者加 QQ 群:162452394 (必須正確回答加群暗號哈,防止"特務"混入)聯繫我們。