本篇文章主要介紹以下幾個知識點:
- 協(xié)程的基本用法
- 作用域構(gòu)造器
- 使用協(xié)程簡化回調(diào)寫法
內(nèi)容參考自第一行代碼第3版
1. 協(xié)程的基本用法
協(xié)程 可以簡單看作是一種輕量級的線程翎碑。一般線程需要依賴操作系統(tǒng)的調(diào)度才能實現(xiàn)不同線程之間的切換百宇,而協(xié)程卻可以在編程語言層面實現(xiàn)不同協(xié)程之間的切換他嫡,大大提升了并發(fā)編程的運行效率哈踱。
使用協(xié)程需要添加如下依賴:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
1.1 GlobalScope.launch 函數(shù)與 runBlocking 函數(shù)
開啟一個協(xié)程最簡單的方式是用 GlobalScope.launch
函數(shù)传黄,如下:
fun main() {
// GlobalScope.launch 函數(shù)可創(chuàng)建一個協(xié)程作用域鬓长,里面的代碼塊就是在協(xié)程中運行了
GlobalScope.launch {
println("code run in coroutine scope")
}
}
當然,上面運行 main()
函數(shù)并不能打印出日志尝江,因為 GlobalScope.launch
函數(shù)每次創(chuàng)建都是一個頂層協(xié)程涉波,這種協(xié)程當程序運行結(jié)束時也會跟著一起結(jié)束,使得應(yīng)用程序結(jié)束了而上面代碼塊中的代碼還沒來得及運行炭序。
解決這個問題很簡單啤覆,讓程序延遲一段時間在結(jié)束運行即可:
fun main() {
// GlobalScope.launch 函數(shù)可創(chuàng)建一個協(xié)程作用域,里面的代碼塊就是在協(xié)程中運行了
GlobalScope.launch {
println("code run in coroutine scope")
}
Thread.sleep(1000)
}
上面代碼運行可以正常打印日志了惭聂,但若代碼塊中的代碼不能在延遲一段時間內(nèi)運行結(jié)束窗声,那么還是會存在問題:
fun main() {
GlobalScope.launch {
println("code run in coroutine scope")
// delay() 是一個非阻塞式掛起函數(shù),它只會掛起當前協(xié)程辜纲,不影響其他協(xié)程運行
// delay() 只能在協(xié)程的作用域或其他掛起函數(shù)中調(diào)用
// 這邊讓協(xié)程掛起1.5秒
delay(1500)
println("code run in coroutine scope finished")
}
// 和 delay() 函數(shù)不同笨觅,Thread.sleep() 會阻塞當前的線程,運行在該線程下的所有協(xié)程都會被阻塞
Thread.sleep(1000)
}
上面運行會發(fā)現(xiàn)第二條日志沒打印出來耕腾,原因還是因為它還沒來得及運行應(yīng)用程序就結(jié)束了见剩。
當然,此時可以借助 runBlocking
函數(shù)了扫俺,它同樣會創(chuàng)建一個協(xié)程作用域苍苞,但它可以保證在協(xié)程作用域內(nèi)的所有代碼和子協(xié)程全部執(zhí)行完之前一直阻塞當前線程:
fun main() {
runBlocking {
println("code run in coroutine scope")
delay(1500)
println("code run in coroutine scope finished")
}
}
這樣,兩條日志都可以正常打印出來了狼纬。
注:runBlocking
函數(shù)通常只在測試環(huán)境下使用羹呵,在正式環(huán)境中使用容易產(chǎn)生一些性能上的問題。
1.2 launch 函數(shù)
當涉及到高并發(fā)的應(yīng)用場景時疗琉,就能體現(xiàn)出協(xié)程相比線程的優(yōu)勢冈欢,創(chuàng)建多個協(xié)程可以使用 launch
函數(shù):
fun main() {
runBlocking {
// launch 函數(shù)必須在協(xié)程的作用域才能調(diào)用,它會在當前作用域下創(chuàng)建子協(xié)程
// 若外層作用域的協(xié)程結(jié)束了盈简,該作用域下的所有子協(xié)程也會一同結(jié)束
launch {
println("launch1")
delay(1000)
println("launch1 finished")
}
launch {
println("launch2")
delay(1000)
println("launch2 finished")
}
}
}
// 打印結(jié)果:launch1凑耻,launch2犯戏,launch1 finished,launch2 finished
上面兩個子協(xié)程的日志是交替打印的拳话,即并發(fā)運行的先匪,不過它們運行在同一個線程中,只是由編程語言來決定如何在多個協(xié)程之間進行調(diào)度弃衍,使得協(xié)程的并發(fā)效率很高呀非。
1.3 suspend 關(guān)鍵字與 coroutineScope 函數(shù)
在 launch
函數(shù)中編寫的代碼是擁有協(xié)程作用域的,若將部分代碼提取到一個單獨的函數(shù)中就沒有協(xié)程作用域了镜盯,從而無法調(diào)用像 delay()
這樣的掛起函數(shù)岸裙,此時就需要借助 suspend
關(guān)鍵字。
關(guān)鍵字 suspend
可以將任意函數(shù)聲明成掛起函數(shù)速缆,掛起函數(shù)之間可以相互調(diào)用降允,使用如下:
// suspend 關(guān)鍵字只能將一個函數(shù)聲明成掛起函數(shù),是無法給它提供協(xié)程作用域的
// 由于 launch 函數(shù)必須在協(xié)程作用域才能調(diào)用艺糜,因而在這里此時是無法調(diào)用 launch 函數(shù)的
suspend fun printDot() {
println(".")
delay(1000)
}
上面要在 printDot()
中調(diào)用 launch
函數(shù)剧董,還需要借助 coroutineScope
函數(shù)。
函數(shù) coroutineScope
是一個掛起函數(shù)破停,從而可以在其他掛起函數(shù)中調(diào)用翅楼,它會繼承外部的協(xié)程作用域并創(chuàng)建一個子作用域,寫法如下:
suspend fun printDot() = coroutineScope {
launch {
println(".")
delay(1000)
}
}
另外真慢,coroutineScope
函數(shù)和 runBlocking
函數(shù)有點類似毅臊,可以保證其作用域內(nèi)的所有代碼和子協(xié)程在全部執(zhí)行完之前,會一直阻塞當前協(xié)程黑界。
小結(jié)1:coroutineScope
函數(shù)只會阻塞當前協(xié)程管嬉,不影響其他協(xié)程和線程,從而不會有性能上的問題朗鸠;runBlocking
函數(shù)會阻塞當前線程蚯撩,若在主線程中調(diào)用它的話,可能會導致界面卡死的現(xiàn)象童社,不推薦在實際項目中使用求厕。
小結(jié)2: GlobalScope.launch
和 runBlocking
函數(shù)可以在任意地方調(diào)用著隆,coroutineScope
函數(shù)可以在協(xié)程作用域或掛起函數(shù)中調(diào)用扰楼,而 launch
函數(shù)只能在協(xié)程作用域中調(diào)用。
小結(jié)3: GlobalScope.launch
每次創(chuàng)建的都是頂層協(xié)程美浦,一般也不太建議使用弦赖,除非明確要創(chuàng)建頂層協(xié)程。
2. 作用域構(gòu)建器
前面學習的 GlobalScope.launch
浦辨、runBlocking
蹬竖、launch
、coroutineScope
幾種作用域構(gòu)建器,都可以用于創(chuàng)建一個新的協(xié)程作用域币厕。
2.1 取消協(xié)程
不管是 GlobalScope.launch
還是 launch
函數(shù)列另,都會返回一個 Job 對象,調(diào)用其 cancel()
方法可取消協(xié)程旦装,如下:
val job = GlobalScope.launch {
// to do something
}
job.cancel()
若在 Acitivity 使用 GlobalScope.launch
這種協(xié)程作用域構(gòu)建器页衙,每次創(chuàng)建的都是頂層協(xié)程,那么當 Activity 關(guān)閉時阴绢,此時需要取消協(xié)程店乐,就需要逐個調(diào)用所有已創(chuàng)建協(xié)程的 cancel()
方法,使得代碼不好維護呻袭,因而在實際項目中不太常用這種協(xié)程作用域構(gòu)建器眨八,比較常用的寫法如下:
// 創(chuàng)建 Job 對象
val job = Job()
// 傳入 CoroutineScope 函數(shù)
val scope = CoroutineScope(job)
scope.launch {
// to do something
}
job.cancel()
這樣,所有調(diào)用 CoroutineScope
的 launch
函數(shù)創(chuàng)建的協(xié)程都會被關(guān)聯(lián)到 Job 對象的作用域下左电,只需調(diào)用一次 cancel()
方法廉侧,就可把同一作用域內(nèi)的所有協(xié)程取消,大大降低了協(xié)程管理成本篓足。
2.2 async 函數(shù)
調(diào)用 launch
函數(shù)可以創(chuàng)建一個新的協(xié)程伏穆,但它只能用于執(zhí)行一段邏輯,返回一個 Job 對象纷纫,不能獲取執(zhí)行結(jié)果枕扫,要獲取執(zhí)行結(jié)果可以借助 async
函數(shù)。
async 函數(shù)必須在協(xié)程作用域中才能調(diào)用辱魁,它會創(chuàng)建一個新的子協(xié)程并返回一個 Deferred 對象烟瞧,調(diào)用 Deferred 對象的 await()
方法可獲取執(zhí)行結(jié)果,如下:
fun main() {
runBlocking {
val result = async {
10 + 10
}.await()
println(result)
}
}
// 打印結(jié)果:20
當調(diào)用 await()
方法時染簇,若代碼塊中的代碼還沒執(zhí)行完参滴,await()
方法就會將當前協(xié)程阻塞,直到可以獲得 async
函數(shù)的執(zhí)行結(jié)果锻弓。
舉個栗子砾赔,用兩個 async
函數(shù)來執(zhí)行延遲任務(wù),并記錄運行耗時青灼,如下:
fun main() {
runBlocking {
val start = System.currentTimeMillis()
val result1 = async {
delay(1000)
10 + 10
}.await()
val result2 = async {
delay(1000)
15 + 5
}.await()
println("result is ${result1 + result2}")
val end = System.currentTimeMillis()
println("cost time is ${end - start} ms")
}
}
// 打印結(jié)果:result is 40暴心,cost time is 2069 ms
由于上面兩個 async
函數(shù)是一種串行的關(guān)系,前一個執(zhí)行完后一個才執(zhí)行杂拨,因而這種寫法非常低效专普,修改代碼使得兩個 async
函數(shù)同時執(zhí)行如下:
fun main() {
runBlocking {
val start = System.currentTimeMillis()
val deferred1 = async {
delay(1000)
10 + 10
}
val deferred2 = async {
delay(1000)
15 + 5
}
println("result is ${deferred1.await() + deferred2.await()}")
val end = System.currentTimeMillis()
println("cost time is ${end - start} ms")
}
}
// 打印結(jié)果:result is 40,cost time is 1043 ms
上面在需要用到 async
函數(shù)的執(zhí)行結(jié)果時才調(diào)用 await()
方法進行獲取弹沽,此時就變成一種并行關(guān)系了檀夹,運行效率也就提升了筋粗。
2.3 withContext 函數(shù)
withContext() 函數(shù) 是一個比較特殊的作用域構(gòu)建器,它是一個掛起函數(shù)炸渡,可以理解成 async
函數(shù)的一種簡化版寫法娜亿,用法如下:
fun main() {
runBlocking {
// 調(diào)用 withContext() 函數(shù)后,會立即執(zhí)行代碼塊中的代碼蚌堵,同時把當前協(xié)程阻塞
// 當代碼塊中的代碼執(zhí)行完后會吧最后一行的執(zhí)行結(jié)果作為返回值返回
// 相當于 val result = async{10 + 10}.await() 的寫法
val result = withContext(Dispatchers.Default) {
10 + 10
}
println(result)
}
}
值得注意的是暇唾,withContext()
函數(shù)強制要求指定一個線程參數(shù),線程參數(shù)有以下3種:
Dispatchers.Default 使用一種默認低并發(fā)的線程策略
Dispatchers.IO 使用一種較高并發(fā)的線程策略辰斋,如網(wǎng)絡(luò)請求
Dispatchers.Main 不會開啟子線程策州,在主線程中執(zhí)行,只能在Android項目中用
注:在協(xié)程作用域構(gòu)建器中宫仗,所有的函數(shù)都可以指定一個線程參數(shù)够挂,只不過 withContext()
函數(shù)是強制要求指定的,其他函數(shù)則是可選的藕夫。
3. 使用協(xié)程簡化回調(diào)的寫法
平時網(wǎng)絡(luò)請求數(shù)據(jù)時會采用回調(diào)機制來處理孽糖,回調(diào)機制基本上是靠匿名類來實現(xiàn)的:
val address = "https://www.baidu.com/"
// 模擬一個網(wǎng)絡(luò)請求回調(diào)
HttpUtil.sendHttpRequest(address, object :HttpCallbackListener{
override fun onFinish(response: String) {
// 得到服務(wù)器返回內(nèi)容
}
override fun onError(e: Exception) {
// 對異常處理
}
})
在 Kotlin 中,可以通過 suspendCoroutine
函數(shù)把傳統(tǒng)的回調(diào)機制的寫法大幅簡化毅贮。
suspendCoroutine 函數(shù)必須在協(xié)程作用域或掛起函數(shù)中調(diào)用办悟,它接收一個 Lambda 表達式參數(shù),將當前協(xié)程立即掛起滩褥,然后在一個普通的線程中執(zhí)行 Lambda 表達式中的代碼病蛉。
上述 Lambda 參數(shù)列表上會傳入一個 Continuation
參數(shù),調(diào)用它的 resume()
方法或 resumeWithException()
可以讓協(xié)程恢復執(zhí)行瑰煎。
定義個 request()
函數(shù)優(yōu)化上面的的回調(diào)寫法如下:
suspend fun request(address: String): String {
return suspendCoroutine { continuation ->
HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
override fun onFinish(response: String) {
// 成功
continuation.resume(response)
}
override fun onError(e: Exception) {
// 失敗
continuation.resumeWithException(e)
}
})
}
}
調(diào)用時就可以這樣寫:
suspend fun getBaiduResponse() {
try {
val response = request("https://www.baidu.com/")
// 進行數(shù)據(jù)處理
} catch (e: Exception) {
// 異常處理
}
}
這樣铺然,代碼就清爽了許多。
再舉個栗子酒甸,平時使用 Retrofit 來發(fā)起網(wǎng)絡(luò)請求時先定義接口和創(chuàng)建構(gòu)建器:
// Retrofit 構(gòu)建器
object ServiceCreator {
private const val BASE_URL = "..."
private val retrofit =
Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create())
.build()
// 獲取 Service 接口的動態(tài)代理對象方法
// 使用:val appService = ServiceCreator.create(AppService::class.java)
fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)
// 通過泛型實化進一步優(yōu)化:獲取 Service 接口的動態(tài)代理對象方法
// 使用:val appService = ServiceCreator.create<AppService>()
inline fun <reified T> create(): T = create(T::class.java)
}
// 定義接口
interface ApiService {
@GET("xxx/xxx")
fun getAppData(): Call<List<String>>
}
接著發(fā)起網(wǎng)絡(luò)請求:
val appService = ServiceCreator.create<ApiService>()
appService.getAppData().enqueue(object : Callback<List<String>> {
override fun onFailure(call: Call<List<String>>, t: Throwable) {
// 失敗邏輯處理
}
override fun onResponse(call: Call<List<String>>, response: Response<List<String>>) {
// 成功邏輯處理
}
})
這時魄健,使用 suspendCoroutine
函數(shù)可以對上面寫法進行優(yōu)化。使用泛型的方式定義一個 await()
函數(shù)如下:
// 這里 await() 定義成一個掛起函數(shù)插勤,并定義成 Call<T> 的擴展函數(shù)沽瘦,
// 從而所有返回值是 Call 類型的 Retrofit 網(wǎng)絡(luò)請求接口都可以直接調(diào)用 await() 函數(shù)
suspend fun <T> Call<T>.await(): T {
return suspendCoroutine { continuation ->
// 由于擴展函數(shù)的原因,這里擁有了 Call 對象的上下文农尖,
// 從而可以直接調(diào)用 enqueue() 方法讓 Retrofit 發(fā)起網(wǎng)絡(luò)請求
enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
// 失敗
continuation.resumeWithException(t)
}
override fun onResponse(call: Call<T>, response: Response<T>) {
// 成功
val body = response.body()
if (body != null) continuation.resume(body)
else continuation.resumeWithException(RuntimeException("body is null"))
}
})
}
}
有了 await()
函數(shù)后析恋,調(diào)用 Retrofit 的接口就會簡單很多,比如上面的發(fā)起網(wǎng)絡(luò)請求就可以寫成:
suspend fun getAppData(){
try {
// 只需簡單調(diào)用 await() 函數(shù)即可獲取響應(yīng)數(shù)據(jù)
val result = ServiceCreator.create<ApiService>().getAppData().await()
// 成功邏輯處理
} catch (e: Exception){
// 異常邏輯處理
}
}
注:每次發(fā)起請求時都進行 try catch
處理比較麻煩卤橄,可以選擇不處理绿满。在不處理情況下,若發(fā)生異常就會一層層向上拋出窟扑,直到某一層的函數(shù)處理為止喇颁。也可以在某個統(tǒng)一的入口函數(shù)中只進行一次 try catch
,從而讓代碼更簡潔嚎货。
本篇文章就介紹到這橘霎。