這篇文章並不打算去剖析協程的概念和原理等東西,相似的文章在網上已經有不少了,相信不少文章解釋得比我更準確和透徹,若是你們感興趣的話能夠自行查閱學習。html
對於協程還僅限於據說的程度的同窗,能夠查閱一下這些資料:git
協程指南github
官方文檔,中文,能夠直連,強烈推薦。如果有條件能夠嘗試看英文版,由於協程還有一部分API屬於實驗階段,有可能會產生變化,英文文檔能夠看第一手資料。bash
破解Kotlin協程服務器
寫得很好的一個系列文章,不管是從入門或是加深理解都推薦看一看。框架
Kotlin 協程的掛起好神奇好難懂async
扔老師的兩個視頻,說得很淺顯易懂,看完基本上至少能瞭解協程究竟是個什麼玩意ide
網上有不少協程+Retrofit或
是協程+XXX框架
進行一系列封裝的文章,我也不過多贅述,但若是有同窗說,我就想單獨用協程呢?僅協程這個單獨的個體來講,能給實際開發解決什麼痛點?函數
這一系列文章會假設你們對於協程有基本上的瞭解但不知道怎麼去用(實際上不了解也不要緊,看完文章你至少可以知道協程帶來的好處),從最基礎的粒度上告訴你們,哪些場景下可使用協程?能夠帶來哪些好處?
萬物始於CoroutinScope
,可能有一些老的文章還在使用GlobalScope.launch
來教你怎麼啓動一個協程,那麼這麼用有問題嗎?固然沒問題了,連官方文檔的第一篇入門文章也是這麼教的,只是這麼用的話,會須要你注意自行處理協程的開始和結束等問題以避免致使內存泄漏或是其它你不想看見的異常,若是有更好的選擇的話,何苦爲難本身?
實際上如今Android官方的Jetpack
組件在不少狀況下已經提供給開發者默認的Scope,好比ViewModelScope
、LifecycleScope
等,不過這不是這篇文章的重點,前言已經說了,從最基礎的粒度上對吧。那麼拋棄這些框架組件來講,我推薦你用什麼呢?
val mainScope = MainScope()
複製代碼
就這麼簡單,一行代碼。讓咱們來看看Kotlin協程官方提供的這個MainScope()
是什麼東西:
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
複製代碼
很簡潔明瞭,這是一個內部的取消操做只會向下傳播,運行在主線程
的CoroutineScope
;
若是你想要一個默認運行在子線程
的Scope:
val ioScope = MainScope() + Dispatchers.IO
複製代碼
或者你想要一個自動捕獲內部包括子協程可能拋出的異常的Scope:
val mainScope = MainScope() + CoroutineExceptionHandler { context, throwable ->
throwable.printStackTrace()
}
複製代碼
我推薦你這麼使用的目的是,你能夠在Activity的OnDestroy
生命週期或者其它任何你須要的時機裏調用mainScope.cancel()
,便可在大部分狀況下
取消全部可能還在運行的子協程;
爲何是大部分狀況下?由於在某些時候,好比子協程
裏進行循環讀取文件流等阻塞操做時,你可能須要自行加上判斷Scope的運行狀態進行中斷操做。
Scope說完了,如今說說最簡單的一個函數:launch
假設你如今有某一部分代碼,你只是簡單的想延遲幾秒後執行,你不想用postDelay不想寫Timer等等,更不想在延遲的那幾秒內阻塞主線程(這不是廢話麼;D),你能夠這麼寫:
private fun delayToExecute(duration: Long, execution: () -> Unit) = mainScope.launch {
delay(duration)
execution()
}
複製代碼
這裏咱們將launch
放到了方法上,實際上這個方法返回一個Job
,若是你不須要的話,徹底能夠不去管它;而且咱們直接使用的是mainScope.launch
,沒有指定其它Dispatchers
,所以execution
函數體會在主線程裏執行,能夠直接在裏面作修改UI
等操做,你如今能夠這麼調用:
delayToExecute(3000) {
textView.text = "xxxx"
}
複製代碼
你也能夠將launch
放到調用者上,將前面的方法改成:
private suspend fun delayToExecute(duration: Long, execution: () -> Unit) {
delay(duration)
execution()
}
複製代碼
調用的時候就得改成:
mainScope.launch {
delayToExecute(3000) {
textView.text = "xxxx"
}
}
複製代碼
兩種方法有什麼區別呢?去查閱一下suspend標識符的意義吧,前言裏扔老師的視頻對於suspend的講解很清楚,這裏就不過多贅述
若是你但願execution
函數體執行在子線程上的話也很簡單,launch
支持指定Dispatchers
,加一個參數便可:
mainScope.launch(Dispatchers.IO)
複製代碼
是否是以爲太簡單了?就這?別急,launch
的本質目的只是讓你啓動一個基礎協程,有了它,你纔可以跟各類以後要說的各類suspend
方法以及其餘協程函數打交道。
上面說了launch
本質上只是啓動一個基礎協程,它返回一個Job
。
假設如今你有一個或多個函數,你但願它運行在子線程上,裏面進行一些耗時處理,一段時間後返回處理結果,當且僅當全部這些函數全都返回告終果以後,才進行後續的處理。
想想這種狀況下,若是不使用RxJava
,是否是須要寫一堆的回調去接收和處理結果?甚至即便使用RxJava
,是否是也或多或少的存在一些嵌套代碼?
這時候就是協程最讓開發者舒服的時候了,即所謂的用同步風格,寫異步代碼。
咱們將這種狀況拆開了看,這些運行在子線程上的函數能夠想象爲處理者
,收到一些數據,將數據處理以後返回結果,而調用者
但願以最簡單的形式去使用這些處理者
,也就是調起處理者
,以返回值的形式接收結果,不要再讓調用者
去寫回調接收結果。
咱們來看看假設有兩個處理者
,怎麼去實現:
private fun processA(data: String): String {
info { "${Thread.currentThread().name}: process A start" }
Thread.sleep(4000)
info { "${Thread.currentThread().name}: process A done" }
return "$data --> process A done"
}
private fun processB(data: String): String {
info { "${Thread.currentThread().name}: process B start" }
Thread.sleep(2000)
info { "${Thread.currentThread().name}: process B done" }
return "$data --> process B done"
}
複製代碼
processA
函數在4秒後返回處理結果,processB
在2秒後返回處理結果。
再來看看調用者
怎麼實現:
private fun invokeProcess() = mainScope.launch {
val defA = async(Dispatchers.IO) {
processA("Data for A")
}
val defB = async(Dispatchers.IO) {
processB("Data for B")
}
val resultA = defA.await()
val resultB = defB.await()
info { "${Thread.currentThread().name}: $resultA \t $resultB" }
}
複製代碼
以後執行invokeProcess()
,打印出來的日誌爲:
DefaultDispatcher-worker-2: process A start
DefaultDispatcher-worker-1: process B start
DefaultDispatcher-worker-1: process B done
DefaultDispatcher-worker-2: process A done
main: Data for A --> process A done Data for B --> process B done
複製代碼
從日誌上能夠清晰的看出來,async
函數在調用時即馬上執行(也能夠根據須要指定不當即執行,具體能夠了解一下async
接收的CoroutineStart
參數)
從通俗的角度上解釋一下async
這個函數,它會啓動一個子協程,將lambda表達式
(即上例中的處理者
函數)運行在指定的線程中,而且返回一個Deferred
對象,該對象的await()
方法的返回值即async
中lambda表達式
內的返回值。
在調用對應的Deferred
的await()
方法時,若函數還在處理中,則掛起當前協程(即上例中的調用者所在的協程)等待返回值;若函數此時已經有返回值,則當即獲得結果。
這裏僅僅只介紹了async
函數最基礎的用法,感興趣的同窗能夠看看官方文檔組合掛起函數瞭解更多可配置的參數。
可能有的同窗又說了,不就是async
、Deferred
嘛,相似的東西Java甚至其它語言也有啊。
那麼下面就來講說重頭戲,Kotlin協程是怎麼真正解決回調地獄的場景的。
suspendCoroutine
在上面的例子中,細心的同窗可能發現了,處理者
中的結果都是以return
的方式返回的,然而實際開發中,頗有可能處理者
脫離了你的控制,沒辦法以return
的方式返回結果。
好比說你可能須要在處理者
中調用某個第三方框架,這個第三方框架限制了你必須以回調的形式來接收結果;在這種狀況下,處理者
沒法避免的涉及到一些回調嵌套,那麼咱們看看怎麼樣讓調用者
最大限度的避免回調地獄。
以我我的很喜歡的一個動態權限處理框架AndPermission來做爲例子,這個框架幫助開發者去處理動態權限的判斷和申請,以回調的形式接收結果,大概是這個樣子的:
AndPermission.with(this)
.runtime()
.permission(Manifest.permission.CAMERA)
.onGranted {
// 已有權限或是用戶點擊了授予權限
}
.onDenied {
// 沒法獲取權限或是用戶點擊了拒絕授予權限
}
.start()
複製代碼
能夠看到,這個例子裏,權限的是否獲取是經過onGranted
和onDenied
來回調的。
若是不使用協程來的話,是否是得在那兩個回調裏嵌套進拿到權限結果後的邏輯代碼?這仍是隻嵌套了一層的狀況,假設有更多的相似這樣的嵌套狀況呢?
讓咱們換回剛纔說aync
時候的思路,把這個問題當作調用者
和處理者
的關係:
權限的申請應該是一個獨立的處理者
,內部的邏輯不該該須要調用者
去關注;
對於調用者
來講,權限的申請只應該關心最終結果,也就是true
或者false
就夠了。
如今來看看權限申請的處理者
怎麼實現:
private suspend fun checkPermission(context: Context, vararg permissions: String) =
suspendCoroutine<Boolean> { continuation ->
AndPermission.with(context)
.runtime()
.permission(permissions)
.onGranted {
// 已有權限或是用戶點擊了授予權限
continuation.resume(true)
}
.onDenied {
// 沒法獲取權限或是用戶點擊了拒絕授予權限
continuation.resume(false)
}
.start()
}
複製代碼
這裏稍微將參數改造了一下,甚至能夠將這個方法抽爲一個工具類的靜態方法,在任何須要判斷權限的時候均可以使用。
suspendCoroutine
函數接收一個lambda表達式
,調起的時候掛起調用者,並continuation.resume()
將結果以返回值的形式返回給調用者。
有點拗口?沒事,再來看看調用者
的代碼:
private fun startCamera() = mainScope.launch {
val permission = checkPermission(context, Manifest.permission.CAMERA)
if (permission) {
// 得到了權限,開啓攝像頭或其餘操做
} else {
// 未得到權限,提醒用戶使用該權限的目的
}
}
複製代碼
調用者
的代碼是否是簡潔明瞭許多?調用處理者
,接收一個布爾值,而後就直接能夠進行後續的邏輯處理了。
以此延伸,無論你有多少個相似這樣的異步返回結果的處理者
的時候,對於調用者來講都不要緊,通通都是一行代碼,接收一個返回值,搞定,是否是很方便?
留一個很常見的場景你們能夠嘗試本身去實現一下試試:
讀取SD卡內的某張圖片,對其進行壓縮,再將其上傳到服務器上
這個系列的第一篇文章就到此結束了,僅僅從最基本的角度講了Kotlin協程幾個函數在默認狀況下的使用場景,有興趣的同窗能夠自行看一下官方文檔,瞭解一下這幾個函數的一些可選參數:)