前言
大家好驮履,我是小益惭蟋!眾所周知,在2017年Google I/O大會(huì)上竖慧,Google宣布將Kotlin作為Android的第一開發(fā)語言嫌套,而且近年來很多中大型公司招聘Andorid開發(fā)者都要求會(huì)使用Kotlin開發(fā),所以如果你還沒又開始使用Kotlin圾旨,就趕緊上船吧踱讨!
推薦
文章將率先在公眾號(hào)「碼途有道」上發(fā)布,歡迎大家關(guān)注砍的!
一痹筛、認(rèn)識(shí)協(xié)程
使用Kotlin開發(fā)有很多好處,在這我就不一一贅述了廓鞠。而在Kotlin中有一樣?xùn)|西叫做“協(xié)程”帚稠,一些同學(xué)可能聽過甚至使用過,但也有很多同學(xué)可能對其不是很了解床佳,這里我簡單介紹一下協(xié)程是什么滋早!
協(xié)程簡介
協(xié)程其實(shí)是一種編程思想,在很多語言中都存在砌们,大致作用也都一樣杆麸,都是為了幫助我們在處理多線程編碼時(shí)提供更友好和簡便的編碼方式,只是每種語言的具體實(shí)現(xiàn)不一樣浪感,在Kotlin中的協(xié)程我們可以簡單的理解為是一個(gè)線程框架昔头。
普通線程的不便
我們通過一個(gè)小例子對比一下使用普通線程和使用協(xié)程實(shí)現(xiàn)起來有什么不同:
假設(shè)有四個(gè)接口請求A、B影兽、C揭斧、D,接口C需要獲取接口A與接口B的執(zhí)行結(jié)果后才能執(zhí)行峻堰,接口D需要獲取接口C的執(zhí)行結(jié)果才能執(zhí)行讹开。
上述場景是我們在平時(shí)Android開發(fā)中最常見的場景盅视,如果用普通線程的方式來實(shí)現(xiàn),接口請求的結(jié)果一般使用callback獲取萧吠,接口A與接口B應(yīng)該是并發(fā)執(zhí)行左冬,我們必須在接口A執(zhí)行完成的callback中檢查接口B是否執(zhí)行完成,同樣必須在接口B執(zhí)行完成的callback中檢查接口A是否執(zhí)行完成纸型,這樣才能在第一時(shí)間將兩個(gè)接口的結(jié)果交給接口C拇砰,而接口C則需要在執(zhí)行完成的callback中調(diào)用接口D,使其執(zhí)行狰腌。
fun apiA(object: callback(){
獲取 —> 結(jié)果A
調(diào)用 —> 接口AB執(zhí)行完畢檢測方法()
} )
fun apiB(object: callback(){
獲取 —> 結(jié)果A
調(diào)用 —> 接口AB執(zhí)行完畢檢測方法()
} )
fun 接口AB執(zhí)行完畢檢測方法(){
if( 結(jié)果A!=null && 結(jié)果B!=null ){
apiC(結(jié)果A , 結(jié)果B, object: callback(){
調(diào)用 —> apiD(結(jié)果C)
}
}
}
// 開始執(zhí)行
fun start(){
apiA();
apiB();
}
可以看出除破,在整個(gè)執(zhí)行流程中都充滿了callback,并且接口A與B之間的相互監(jiān)測是否執(zhí)行完畢也會(huì)多寫很多代碼琼腔。如果這種邏輯再稍微復(fù)雜點(diǎn)瑰枫,簡直就是回調(diào)地獄,代碼的閱讀體驗(yàn)也會(huì)變的越來越差丹莲。當(dāng)然我們可以使用Handler等消息通知方式來統(tǒng)一處理光坝,但是閱讀體驗(yàn)也是提升有限。
以上不便甥材,其歸根結(jié)底是因?yàn)榫€程是系統(tǒng)調(diào)度的盯另,系統(tǒng)控制線程的執(zhí)行結(jié)束,我們開發(fā)者在主線程中是無法得知線程何時(shí)執(zhí)行結(jié)束洲赵,只能等待線程自己通知我們鸳惯。
協(xié)程的優(yōu)勢
而上述場景使用協(xié)程來實(shí)現(xiàn),其實(shí)現(xiàn)邏輯大致如下:
val 結(jié)果A = 協(xié)程A執(zhí)行()
val 結(jié)果B = 協(xié)程B執(zhí)行()
val 結(jié)果C = 協(xié)程C執(zhí)行(結(jié)果A, 結(jié)果B)
協(xié)程D執(zhí)行(結(jié)果C)
PS:此處特別提示一點(diǎn)叠萍,上述協(xié)程A與B的邏輯看起來是協(xié)程A先執(zhí)行完后再協(xié)程B執(zhí)行芝发,但其實(shí)A、B也是并發(fā)執(zhí)行的
從上述流程中不難看出苛谷,其寫法直接從異步代碼寫法變成了同步代碼寫法辅鲸,邏輯瞬間清晰了很多,少了很多callback腹殿,代碼的閱讀體驗(yàn)也是直線上升独悴。而這都?xì)w功于協(xié)程可以靈活的在不同線程之間切換,開發(fā)者可以明確的控制協(xié)程的結(jié)束赫蛇,再也不用被動(dòng)的等待通知绵患。說到此處雾叭,有的同學(xué)可能就想到了RxJava悟耘,兩者都能實(shí)現(xiàn)我們想要的效果,不過兩者的設(shè)計(jì)理念并不相同织狐,語法也是天差地別暂幼,有興趣的同學(xué)兩者可以都了解一下筏勒。
綜上,我們可以得出一點(diǎn)旺嬉,協(xié)程可以將異步編碼簡化管行,用同步的方式寫異步,開發(fā)者可以靈活的控制協(xié)程的執(zhí)行與結(jié)束以及線程的切換邪媳。而這也是我們在Android開發(fā)中最在意的特性捐顷,協(xié)程的其他優(yōu)點(diǎn)我們暫時(shí)不必去了解。
二雨效、導(dǎo)入?yún)f(xié)程
目前高版本的Android Studio添加Kotlin支持時(shí)迅涮,已經(jīng)自動(dòng)添加Kotlin協(xié)程支持了,如果不能使用協(xié)程徽龟,可以添加如下依賴
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5"
三叮姑、協(xié)程的幾種使用方式
1. runBlocking
runBlocking {
getUserInfo(userId)
}
此方法一般不推薦使用,因?yàn)樗鼤?huì)阻塞線程
2. GlobalScope.launch
GlobalScope.launch {
getUserInfo(userId)
}
此種方法需要慎用据悔,雖然它不會(huì)阻塞線程传透,但是它的生命周期與app一致,并且不能取消
3. 自行創(chuàng)建CoroutineScope
// 此處的context是CoroutineContext极颓,和Activity繼承的Context不是同一個(gè)東西
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
getUserInfo(userId)
}
此方法比較推薦朱盐,使用此方法我們可以靈活的控制協(xié)程的生命周期。所以下面我們主要介紹此種方式的協(xié)程使用方式讼昆,其他兩種方式有興趣的同學(xué)可以自行去了解托享。
四、協(xié)程的簡單使用
協(xié)程目前常用的幾個(gè)方法有launch
浸赫、withContext
以及async/await
闰围,下面我們主要介紹一下launch
與withContext
,async/await
留到下章與suspend
一起講解既峡。
launch
首先我們先來看下launch
的源碼羡榴,簡單了解一下launch
是什么!
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
可以看出运敢,launch
是CoroutineScope
的擴(kuò)展函數(shù)(即launch是CoroutineScope的內(nèi)部函數(shù))校仑,并且最后返回了一個(gè)新的協(xié)程(即創(chuàng)建了新的Coroutine),具體實(shí)現(xiàn)我們暫且不管传惠。其使用方法如下:
// 切換到主線程
coroutineScope.launch(Dispatchers.Main) {
...
}
// 切換到IO線程執(zhí)行
coroutineScope.launch(Dispatchers.IO) {
...
}
// 小例子
coroutineScope.launch(Dispatchers.IO) {
val userInfo = getUserInfo()
}
tv_name = "xxx"
我們可以通過指定Dispatchers
來切換到不同的線程迄沫,如果不指定,則協(xié)程默認(rèn)在其所處方法的線程中執(zhí)行任務(wù)卦方。在{ }
中的代碼就是協(xié)程需要執(zhí)行的代碼塊羊瘩,在最后的小例子中,tv_name = "xxx"
并不會(huì)等待getUserInfo()
執(zhí)行完畢才執(zhí)行,因?yàn)?code>coroutineScope.launch(Dispatchers.IO) { }的執(zhí)行并不會(huì)阻塞UI線程的執(zhí)行尘吗,coroutineScope.launch(Dispatchers.IO) { }
就相當(dāng)于是另開了一個(gè)線程逝她,就如同new Thread().start()
一樣。所以協(xié)程與其外部線程的關(guān)系我們一定要理清睬捶,我們再通過一個(gè)具體的例子看下協(xié)程內(nèi)部:
coroutineScope.launch(Dispatchers.Main) {
tv_name.text = "xxx"
// 切換到IO線程
launch(Dispatchers.IO){
// 獲取用戶信息
val userInfo = getUserInfo()
// 切換到UI線程
launch(Dispatchers.Main){
// 修改用戶名顯示
tv_name.text = userInfo.username
// 切換到IO線程
launch(Dispatchers.IO) {
// 獲取消息列表
val msgList = getMessageList(userInfo.token)
// 切換到UI線程
launch(Dispatchers.Main){
// 顯示消息列表
}
}
}
}
}
在上述例子中黔宛,我們先在IO線程中獲取用戶信息,之后在主線程中更新用戶姓名擒贸,然后在IO線程中獲取消息列表臀晃,最后在主線程中顯示消息列表。整個(gè)流程通過launch
來創(chuàng)建在不同線程中工作的子協(xié)程完成介劫,并且完全是一種同步編碼的體驗(yàn)积仗。另外,如果兩個(gè)launch
處于同一層次蜕猫,如下:
coroutineScope.launch(Dispatchers.Main) {
// 切換到IO線程
launch(Dispatchers.IO){
// 獲取用戶信息
val userInfo = getUserInfo()
}
// 切換到IO線程
launch(Dispatchers.IO){
// 獲取首頁信息
val userInfo = getHomeInfo()
}
}
那么getUserInfo()
與getHomeInfo()
會(huì)并發(fā)執(zhí)行寂曹,也就是說這兩個(gè)launch
是并行的,這種方式適合于那種只管并發(fā)回右,不用返回結(jié)果的場景隆圆,例如多個(gè)網(wǎng)絡(luò)請求并發(fā),但是相互之間沒有關(guān)聯(lián)翔烁。
withContext
雖然使用launch
已經(jīng)使得整個(gè)編碼方式由異步變?yōu)橥矫煅酰嵌鄠€(gè)launch
嵌套并不美觀,我們可以優(yōu)化一下蹬屹,使用withContext
來改寫:
coroutineScope.launch(Dispatchers.Main) {
tv_name.text = "xxx"
// 獲取用戶信息
val userInfo = withContext(Dispatchers.Main) {
getUserInfo()
}
tv_name.text = userInfo.username
// 獲取消息列表
val msgList = withContext(Dispatchers.IO) {
getMessageList(userInfo!!.token)
}
// 顯示消息列表
}
在從上述代碼中侣背,withContext
都是順序執(zhí)行,不像多個(gè)同級(jí)的launch
是并發(fā)執(zhí)行慨默。使用withContext
可以消除launch
的嵌套贩耐,閱讀體驗(yàn)更佳。不過withContext
只能在CoroutineScope
內(nèi)部使用厦取,不能單獨(dú)在其他方法中使用潮太,比如:
class BaseActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
withContext(Dispatchers.IO){
...
}
}
}
這種寫法是不行的,因?yàn)?code>withContext只能在CoroutineScope
內(nèi)部使用虾攻。
五铡买、小結(jié)
本章內(nèi)容簡單介紹了協(xié)程是什么以及協(xié)程的基本使用,下章內(nèi)容將會(huì)介紹寫協(xié)程中的async/await
和suspend
關(guān)鍵字霎箍,這兩者在我們的日常開發(fā)中使用頻率非常高奇钞,尤其是suspend
。最后推薦一個(gè)Kotlin協(xié)程認(rèn)知系列文章漂坏,強(qiáng)烈推薦對協(xié)程不是很理解的同學(xué)去閱讀景埃!