前言
這篇文章并不打算去剖析協程的概念和原理等東西,類似的文章在網上已經有很多了,相信很多文章解釋得比我更準确和透徹,如果大家感興趣的話可以自行查閱學習。
對于協程還僅限于聽說的程度的同學,可以查閱一下這些資料:
網上有很多協程+Retrofit或是協程+XXX架構進行一系列封裝的文章,我也不過多贅述,但如果有同學說,我就想單獨用協程呢?僅協程這個單獨的個體來說,能給實際開發解決什麼痛點?
這一系列文章會假設大家對于協程有基本上的了解但不知道怎麼去用(實際上不了解也沒關系,看完文章你至少能夠知道協程帶來的好處),從最基礎的粒度上告訴大家,哪些場景下可以使用協程?可以帶來哪些好處?
scope
萬物始于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的運作狀态進行中斷操作。
launch
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方法以及其他協程函數打交道。
async
上面說了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 { 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協程幾個函數在預設情況下的使用場景,有興趣的同學可以自行看一下官方文檔,了解一下這幾個函數的一些可選參數:)
關于找一找教程網
本站文章僅代表作者觀點,不代表本站立場,所有文章非營利性免費分享。
本站提供了軟體程式設計、網站開發技術、伺服器運維、人工智能等等IT技術文章,希望廣大程式員努力學習,讓我們用科技改變世界。
[協程在Android實際開發中到底帶來哪些好處(一)]http://www.zyiz.net/tech/detail-121863.html