Kotlin Coroutine 一般翻譯成協(xié)程,顧名思義可以理解成協(xié)作程序吹害,它并不是 Kotlin 特有的,很多程序都有協(xié)程這個(gè)概念虚青。剛開(kāi)始接觸時(shí)它呀,對(duì)這些概念還是挺費(fèi)解的。我在這里試圖從0開(kāi)始棒厘,講講怎么理解協(xié)程這個(gè)概念钟些,并把它應(yīng)用到我們的 Android 應(yīng)用程序開(kāi)發(fā)中來(lái)。萬(wàn)事開(kāi)頭難绊谭,按照慣例政恍,我也用協(xié)程來(lái)寫(xiě)一個(gè) Hello World 出來(lái)。
1. 添加Android協(xié)程依賴(lài)庫(kù)
在 Android 中要使用協(xié)程达传,首先需要引入 Kotlin 的基礎(chǔ)庫(kù)和協(xié)程庫(kù)篙耗,kotlin 對(duì)協(xié)程的支持都在 kotlinx.coroutines包中,在 build.gradle 中添加依賴(lài):
//kotlin基礎(chǔ)庫(kù)和協(xié)程庫(kù)
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'
注意宪赶,在寫(xiě)這篇文章時(shí)宗弯,kotlinx-coroutines-android 的版本為1.3.2
,同時(shí)要求 kotlin 的版本為1.3.50
搂妻。
為了后面通過(guò)網(wǎng)絡(luò)請(qǐng)求舉例來(lái)說(shuō)明協(xié)程的作用蒙保,同時(shí)引入 retrofit
和 rxjava
庫(kù):
api 'com.squareup.retrofit2:retrofit:2.6.2'
api 'io.reactivex.rxjava2:rxjava:2.2.13'
api 'io.reactivex.rxjava2:rxandroid:2.1.1'
api 'com.squareup.retrofit2:adapter-rxjava2:2.6.2'
api 'com.squareup.retrofit2:converter-gson:2.6.2'
Android 開(kāi)發(fā)如果不知道 retrofit 和 rxjava 是什么,那么請(qǐng)自行搜索學(xué)習(xí)了之后再往下閱讀欲主。這里需要注意的是邓厕,retrofit 早一點(diǎn)的版本,對(duì)協(xié)程的新特性還不支持扁瓢。
2. 第一個(gè)協(xié)程程序
萬(wàn)事俱備只欠東風(fēng)详恼,隨便寫(xiě)一個(gè)點(diǎn)擊事件,執(zhí)行以下代碼:
println("1.測(cè)試開(kāi)始引几,創(chuàng)建協(xié)程")
GlobalScope.launch {
println("2.協(xié)程開(kāi)始運(yùn)行")
delay(3000)
println("3.協(xié)程內(nèi)部延遲3s后")
withContext(Dispatchers.Main) {
println("4.toast Hello World")
Toast.makeText(this@MainActivity, "Hello World", Toast.LENGTH_SHORT).show()
}
}
println("5.協(xié)程外部運(yùn)行")
執(zhí)行結(jié)果如下:
1.測(cè)試開(kāi)始昧互,創(chuàng)建協(xié)程
5.協(xié)程外部運(yùn)行
2.協(xié)程開(kāi)始運(yùn)行
3.協(xié)程內(nèi)部延遲3s后
4.toast Hello World
多次執(zhí)行,執(zhí)行結(jié)果也可能為:
1.測(cè)試開(kāi)始伟桅,創(chuàng)建協(xié)程
2.協(xié)程開(kāi)始運(yùn)行
5.協(xié)程外部運(yùn)行
3.協(xié)程內(nèi)部延遲3s后
4.toast Hello World
執(zhí)行結(jié)果里敞掘,"2" 和 "5" 的順序可能會(huì)變化,其他都不變楣铁。到這里玖雁,可能你會(huì)比較懵逼,協(xié)程到底是個(gè)什么鬼民褂,執(zhí)行順序怎么還會(huì)變化茄菊,先不用管這些疯潭,你只需要知道通過(guò) GlobalScope.launch()
方法啟動(dòng)了一個(gè)協(xié)程就行了赊堪。至此面殖,我們已經(jīng)很直觀(guān)的感受到了協(xié)程。
3. 采用Retrofit來(lái)進(jìn)行一次網(wǎng)絡(luò)接口請(qǐng)求
首先問(wèn)一個(gè)問(wèn)題哭廉,接口請(qǐng)求成功后脊僚,怎么將結(jié)果通知出去?目前常見(jiàn)的有2種方式:
- 通過(guò) callback 回調(diào)的方式遵绰;
- 使用 rxjava 類(lèi)似的框架辽幌;
那我們先來(lái)寫(xiě)一個(gè)例子,采用 retrofit 模擬做一個(gè)登錄請(qǐng)求:
//接口定義
interface TestService {
@GET("https://www.baidu.com")
fun testLogin(): Call<UserInfo>
@GET("https://www.baidu.com")
fun testLoginEx(): Flowable<UserInfo>
}
data class UserInfo(val name: String, val age: Int) {
override fun toString(): String {
return "UserInfo(name='$name', age=$age)"
}
}
val testApi: Retrofit by lazy {
val okHttpClient = OkHttpClient.Builder()
.addInterceptor { chain ->
val builder = Response.Builder()
//為了測(cè)試方便椿访,模擬返回?cái)?shù)據(jù)
val respBytes = "{\"name\":\"hjy\",\"age\":30}".toByteArray()
val source = Okio.source(ByteArrayInputStream(respBytes))
//模擬網(wǎng)絡(luò)請(qǐng)求乌企,線(xiàn)程睡眠3秒鐘
Thread.sleep(3000)
//隨機(jī)返回正確結(jié)果或者錯(cuò)誤結(jié)果
builder.code(if ((Math.random() * 10).toInt() % 2 == 0) 200 else 400)
.message("")
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.body(RealResponseBody("application/json", respBytes.size.toLong(), Okio.buffer(source)))
builder.build()
}
.build()
val retrofitBuilder = Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://wwww.baidu.com")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
retrofitBuilder.build()
}
熟悉 retrofit、okhttp 的成玫,這段代碼應(yīng)該都能看得懂加酵,為了方便測(cè)試,我們本地模擬隨機(jī)返回正確的結(jié)果或者返回異常哭当,后面很多實(shí)際案例如不做特殊說(shuō)明猪腕,會(huì)以此為基礎(chǔ)。
如果采用回調(diào)的方式來(lái)調(diào)用接口钦勘,常規(guī)的寫(xiě)法是:
val call = testApi.create(TestService::class.java).testLogin()
val handler = Handler(Looper.getMainLooper())
call.enqueue(object : retrofit2.Callback<UserInfo> {
override fun onFailure(call: Call<UserInfo>, t: Throwable) {
handler.post {
t.printStackTrace()
}
}
override fun onResponse(call: Call<UserInfo>, response: Response<UserInfo>) {
handler.post {
if (response.isSuccessful) {
println("callback 回調(diào)結(jié)果:${response.body()}")
} else {
println("請(qǐng)求失斅稀:errorCode = ${response.code()}")
}
}
}
})
如果采用 rxjava 的方式來(lái)調(diào)用接口,常規(guī)的寫(xiě)法是:
testApi.create(TestService::class.java)
.testLoginEx().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ userinfo ->
println("調(diào)用成功:${userinfo}")
}, { exception ->
println(exception.message)
})
上面這樣的代碼彻采,相信每個(gè)人都寫(xiě)過(guò)成百上千遍了腐缤,那么我根據(jù)經(jīng)驗(yàn)來(lái)評(píng)價(jià)下這2種方式:
- 回調(diào)的方式處理接口請(qǐng)求已是上古時(shí)代的產(chǎn)物了,在 Android 中更新 UI 必須在主線(xiàn)程中執(zhí)行肛响,所以最終相當(dāng)于嵌套了 2 個(gè) callback柴梆,一不小心就會(huì)產(chǎn)生回調(diào)地獄;
- rxjava 相比回調(diào)的方式已經(jīng)優(yōu)雅很多了终惑,在沒(méi)有使用協(xié)程之前斗幼,可以認(rèn)為是目前的最優(yōu)方案疆股。但是如果你用過(guò) map、flatMap 等操作符,一些眼花繚亂的操作雄妥,雖然功能很強(qiáng)大,但對(duì)開(kāi)發(fā)者來(lái)說(shuō)真的是轴咱,如果能不用就不用吧实苞;
- 如果有多個(gè)異步請(qǐng)求需要串行執(zhí)行或者并行請(qǐng)求,這2種方式寫(xiě)起來(lái)都很蛋疼质帅;
4. 使用協(xié)程來(lái)請(qǐng)求接口
先定義接口适揉,需要注意的是 retrofit 2.6.0
開(kāi)始才能直接支持協(xié)程留攒,老版本是不直接支持的,需要其他方式來(lái)實(shí)現(xiàn)嫉嘀,我這里略過(guò)不介紹了:
@GET("https://www.baidu.com")
suspend fun testLoginByCoroutine(): UserInfo
與 rxjava 的接口相比炼邀,增加了 suspend
關(guān)鍵字,返回結(jié)果直接是返回的對(duì)象剪侮,去掉了 Flowable
的包裝拭宁,再看如何調(diào)用:
MainScope().launch {
try {
val userInfo = testApi.create(TestService::class.java).testLoginByCoroutine()
//以下結(jié)果會(huì)在主線(xiàn)程執(zhí)行
println("調(diào)用成功:$userInfo")
} catch (e: Exception) {
//結(jié)果會(huì)在主線(xiàn)程執(zhí)行
println(e.message)
}
}
這里通過(guò) MainScope().launch(...)
啟動(dòng)了一個(gè)協(xié)程,MainScope() 是類(lèi)似 GlobalScope 的東西瓣俯,只不過(guò)它默認(rèn)是在主線(xiàn)程執(zhí)行杰标,現(xiàn)在可以先不用理會(huì)它。令人驚訝的是彩匕,協(xié)程體內(nèi)的代碼居然都是順序執(zhí)行腔剂,完全沒(méi)有了回調(diào),并且以上代碼并不會(huì)阻塞主線(xiàn)程驼仪,且返回的結(jié)果也是在主線(xiàn)程掸犬,現(xiàn)在居然線(xiàn)程切換的代碼都不用了。
以同步的方式來(lái)寫(xiě)異步的代碼谅畅,代碼簡(jiǎn)單登渣,邏輯清晰明了,有了它我再也不想用 rxjava 了毡泻。熟悉 javascript 的同學(xué)肯定了解胜茧,這不是與 js 里的 async、await
有異曲同工之妙么仇味。
5. 使用協(xié)程來(lái)請(qǐng)求多個(gè)接口
如果上面這個(gè)例子呻顽,你看不出協(xié)程有多大優(yōu)勢(shì)的話(huà),那么我們來(lái)看個(gè)更復(fù)雜的例子丹墨。假設(shè)我們有這樣一個(gè)業(yè)務(wù)場(chǎng)景:第一步需要先調(diào)用登錄接口廊遍,如果登錄成功則需要從服務(wù)端獲取某些信息,然后才能進(jìn)行后面的操作贩挣。也就是說(shuō)需要先后調(diào)用2個(gè)接口喉前,并且前一個(gè)接口的結(jié)果會(huì)影響后面的流程。同樣我們使用協(xié)程來(lái)模擬這種情況:
MainScope().launch {
try {
val testService = testApi.create(TestService::class.java)
//第一次調(diào)用
val userInfo1 = testService.testLoginByCoroutine()
println("第一個(gè)接口調(diào)用成功:$userInfo1")
//前面接口調(diào)用成功后王财,調(diào)用另一個(gè)接口
val userInfo2 = testService.testLoginByCoroutine()
println("第二個(gè)接口調(diào)用成功:$userInfo2")
} catch (e: Exception) {
println("出現(xiàn)異常:${e.message}")
}
}
出現(xiàn)的結(jié)果可能會(huì)有:
第一個(gè)接口調(diào)用成功:UserInfo(name='hjy', age=30)
第二個(gè)接口調(diào)用成功:UserInfo(name='hjy', age=30)
或者
出現(xiàn)異常:HTTP 400
或者
第一個(gè)接口調(diào)用成功:UserInfo(name='hjy', age=30)
出現(xiàn)異常:HTTP 400
至少到這里卵迂,我可以說(shuō)使用協(xié)程有這些好處:
- 幫助你告別回調(diào)地獄;
- 優(yōu)雅的線(xiàn)程切換绒净,甚至讓你無(wú)感知见咒;
- 統(tǒng)一異常處理入口,降低異常處理的復(fù)雜度挂疆;
6. 協(xié)程到底是什么改览?
前面這些例子下翎,如果你親手敲一遍代碼,并執(zhí)行看看結(jié)果宝当,那么你對(duì)協(xié)程就會(huì)有很直觀(guān)的感受了视事。那么協(xié)程到底是什么呢?
一個(gè)操作系統(tǒng)可管理多個(gè)進(jìn)程今妄,一個(gè)進(jìn)程可管理多個(gè)線(xiàn)程郑口。平時(shí)我們寫(xiě) Java 時(shí)鸳碧,所謂的異步任務(wù)都是運(yùn)行在線(xiàn)程當(dāng)中的盾鳞。而線(xiàn)程的切換,最典型的是 Android 中瞻离,我們?cè)?IO 線(xiàn)程請(qǐng)求接口腾仅,接口返回成功后需要切換到主線(xiàn)程更新 UI。這種一個(gè)進(jìn)程中線(xiàn)程的切換套利,都是由操作系統(tǒng)來(lái)調(diào)度的推励,程序并不好控制。
協(xié)程是一種比線(xiàn)程更輕量級(jí)的存在肉迫,網(wǎng)上很多人說(shuō)協(xié)程是輕量級(jí)的線(xiàn)程验辞,我覺(jué)得這種說(shuō)法不靠譜,很容易誤導(dǎo)人喊衫。協(xié)程并不是線(xiàn)程跌造,它就是一種特殊的函數(shù):它可以在某個(gè)地方掛起,并且可以在以后的某個(gè)時(shí)間點(diǎn)族购,重新在掛起處繼續(xù)運(yùn)行壳贪。協(xié)程的執(zhí)行最終靠的還是線(xiàn)程,應(yīng)用程序來(lái)調(diào)度協(xié)程選取合適的線(xiàn)程來(lái)獲取執(zhí)行權(quán)寝杖。把應(yīng)用程序比作操作系統(tǒng)违施,協(xié)程就像線(xiàn)程一樣,應(yīng)用程序可以自己來(lái)操作和調(diào)度協(xié)程的運(yùn)行瑟幕。