@[toc]
什么是協(xié)程秕铛?
官方描述:協(xié)程通過將復(fù)雜性放入庫來簡化異步編程。程序的邏輯可以在協(xié)程中順序地表達煞肾,而底層庫會為我們解決其異步性梢莽。該庫可以將用戶代碼的相關(guān)部分包裝為回調(diào)、訂閱相關(guān)事件臼朗、在不同線程(甚至不同機器)上調(diào)度執(zhí)行邻寿,而代碼則保持如同順序執(zhí)行一樣簡單蝎土。
協(xié)程就像非常輕量級的線程。線程是由系統(tǒng)調(diào)度的绣否,線程切換或線程阻塞的開銷都比較大誊涯。而協(xié)程依賴于線程,但是協(xié)程掛起時不需要阻塞線程蒜撮,幾乎是無代價的暴构,協(xié)程是由開發(fā)者控制的。所以協(xié)程也像用戶態(tài)的線程段磨,非常輕量級取逾,一個線程中可以創(chuàng)建任意個協(xié)程。
協(xié)程很重要的一點就是當(dāng)它掛起的時候苹支,它不會阻塞其他線程砾隅。協(xié)程底層庫也是異步處理阻塞任務(wù),但是這些復(fù)雜的操作被底層庫封裝起來债蜜,協(xié)程代碼的程序流是順序的晴埂,不再需要一堆的回調(diào)函數(shù),就像同步代碼一樣策幼,也便于理解邑时、調(diào)試和開發(fā)。它是可控的特姐,線程的執(zhí)行和結(jié)束是由操作系統(tǒng)調(diào)度的,而協(xié)程可以手動控制它的執(zhí)行和結(jié)束黍氮。
使用
首先需要添加依賴:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3"
1.runBlocking:T
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.e(TAG, "主線程id:${mainLooper.thread.id}")
test()
Log.e(TAG, "協(xié)程執(zhí)行結(jié)束")
}
private fun test() = runBlocking {
repeat(8) {
Log.e(TAG, "協(xié)程執(zhí)行$it 線程id:${Thread.currentThread().id}")
delay(1000)
}
}
[圖片上傳失敗...(image-26061b-1582802832277)]
runBlocking啟動的協(xié)程任務(wù)會阻斷當(dāng)前線程唐含,直到該協(xié)程執(zhí)行結(jié)束。當(dāng)協(xié)程執(zhí)行結(jié)束之后沫浆,頁面才會被顯示出來捷枯。
2.launch:Job
這是最常用的用于啟動協(xié)程的方式,它最終返回一個Job類型的對象专执,這個Job類型的對象實際上是一個接口淮捆,它包涵了許多我們常用的方法。下面先看一下簡單的使用:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.e(TAG, "主線程id:${mainLooper.thread.id}")
val job = GlobalScope.launch {
delay(6000)
Log.e(TAG, "協(xié)程執(zhí)行結(jié)束 -- 線程id:${Thread.currentThread().id}")
}
Log.e(TAG, "主線程執(zhí)行結(jié)束")
}
//Job中的方法
job.isActive
job.isCancelled
job.isCompleted
job.cancel()
jon.join()
[圖片上傳失敗...(image-acaffd-1582802832277)]
從執(zhí)行結(jié)果看出本股,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的一個擴展函數(shù)拄显,CoroutineScope簡單來說就是協(xié)程的作用范圍苟径。launch方法有三個參數(shù):1.協(xié)程下上文;2.協(xié)程啟動模式躬审;3.協(xié)程體:block是一個帶接收者的函數(shù)字面量棘街,接收者是CoroutineScope
1.協(xié)程下上文
上下文可以有很多作用蟆盐,包括攜帶參數(shù),攔截協(xié)程執(zhí)行等等遭殉,多數(shù)情況下我們不需要自己去實現(xiàn)上下文石挂,只需要使用現(xiàn)成的就好。上下文有一個重要的作用就是線程切換险污,Kotlin協(xié)程使用調(diào)度器來確定哪些線程用于協(xié)程執(zhí)行痹愚,Kotlin提供了調(diào)度器給我們使用:
- Dispatchers.Main:使用這個調(diào)度器在 Android 主線程上運行一個協(xié)程÷扌模可以用來更新UI 里伯。在UI線程中執(zhí)行
- Dispatchers.IO:這個調(diào)度器被優(yōu)化在主線程之外執(zhí)行磁盤或網(wǎng)絡(luò) I/O。在線程池中執(zhí)行
- Dispatchers.Default:這個調(diào)度器經(jīng)過優(yōu)化渤闷,可以在主線程之外執(zhí)行 cpu 密集型的工作疾瓮。例如對列表進行排序和解析 JSON。在線程池中執(zhí)行飒箭。
- Dispatchers.Unconfined:在調(diào)用的線程直接執(zhí)行狼电。
調(diào)度器實現(xiàn)了CoroutineContext接口。
2.啟動模式
在Kotlin協(xié)程當(dāng)中弦蹂,啟動模式定義在一個枚舉類中:
public enum class CoroutineStart {
DEFAULT,
LAZY,
@ExperimentalCoroutinesApi
ATOMIC,
@ExperimentalCoroutinesApi
UNDISPATCHED;
}
一共定義了4種啟動模式肩碟,下表是含義介紹:
啟動模式 | 作用 |
---|---|
DEFAULT | 默認的模式,立即執(zhí)行協(xié)程體 |
LAZY | 只有在需要的情況下運行 |
ATOMIC | 立即執(zhí)行協(xié)程體凸椿,但在開始運行之前無法取消 |
UNDISPATCHED | 立即在當(dāng)前線程執(zhí)行協(xié)程體削祈,直到第一個 suspend 調(diào)用 |
2.協(xié)程體
協(xié)程體是一個用suspend關(guān)鍵字修飾的一個無參,無返回值的函數(shù)類型脑漫。被suspend修飾的函數(shù)稱為掛起函數(shù),與之對應(yīng)的是關(guān)鍵字resume(恢復(fù))髓抑,注意:掛起函數(shù)只能在協(xié)程中和其他掛起函數(shù)中調(diào)用,不能在其他地方使用优幸。
suspend函數(shù)會將整個協(xié)程掛起吨拍,而不僅僅是這個suspend函數(shù),也就是說一個協(xié)程中有多個掛起函數(shù)時网杆,它們是順序執(zhí)行的羹饰。看下面的代碼示例:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch {
val token = getToken()
val userInfo = getUserInfo(token)
setUserInfo(userInfo)
}
repeat(8){
Log.e(TAG,"主線程執(zhí)行$it")
}
}
private fun setUserInfo(userInfo: String) {
Log.e(TAG, userInfo)
}
private suspend fun getToken(): String {
delay(2000)
return "token"
}
private suspend fun getUserInfo(token: String): String {
delay(2000)
return "$token - userInfo"
}
[圖片上傳失敗...(image-d05b20-1582802832277)]
getToken方法將協(xié)程掛起碳却,協(xié)程中其后面的代碼永遠不會執(zhí)行队秩,只有等到getToken掛起結(jié)束恢復(fù)后才會執(zhí)行。同時協(xié)程掛起后不會阻塞其他線程的執(zhí)行追城。
3.async
async跟launch的用法基本一樣刹碾,區(qū)別在于:async的返回值是Deferred,將最后一個封裝成了該對象座柱。async可以支持并發(fā)迷帜,此時一般都跟await一起使用物舒,看下面的例子。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch {
val result1 = GlobalScope.async {
getResult1()
}
val result2 = GlobalScope.async {
getResult2()
}
val result = result1.await() + result2.await()
Log.e(TAG,"result = $result")
}
}
private suspend fun getResult1(): Int {
delay(3000)
return 1
}
private suspend fun getResult2(): Int {
delay(4000)
return 2
}
async是不阻塞線程的,也就是說getResult1和getResult2是同時進行的戏锹,所以獲取到result的時間是4s冠胯,而不是7s。
應(yīng)用
項目中的網(wǎng)絡(luò)請求框架大部分都是基于RxJava + Retrofit + Okhttp封裝的锦针,RxJava可是很好的實現(xiàn)線程之間的切換荠察,如果只是網(wǎng)絡(luò)框架中用到了RxJava,那就是“大材小用”了奈搜,畢竟RxJava的功能還是很強大的悉盆。Retrofit從2.6.0開始已經(jīng)支持協(xié)程了:可以定義成一個掛起函數(shù)。
interface Api {
@POST("user/login")
suspend fun login(): Call<User>
}
下面的例子是使用協(xié)程來代替RxJava實現(xiàn)線程切換馋吗。
1.首先定義一個請求相關(guān)的支持DSL語法的接收者焕盟。
class RetrofitCoroutineDSL<T> {
var api: (Call<Result<T>>)? = null
internal var onSuccess: ((T) -> Unit)? = null
private set
internal var onFail: ((msg: String, errorCode: Int) -> Unit)? = null
private set
internal var onComplete: (() -> Unit)? = null
private set
/**
* 獲取數(shù)據(jù)成功
* @param block (T) -> Unit
*/
fun onSuccess(block: (T) -> Unit) {
this.onSuccess = block
}
/**
* 獲取數(shù)據(jù)失敗
* @param block (msg: String, errorCode: Int) -> Unit
*/
fun onFail(block: (msg: String, errorCode: Int) -> Unit) {
this.onFail = block
}
/**
* 訪問完成
* @param block () -> Unit
*/
fun onComplete(block: () -> Unit) {
this.onComplete = block
}
internal fun clean() {
onSuccess = null
onComplete = null
onFail = null
}
}
2.然后給協(xié)程定義一個擴展方法,用于Retrofit網(wǎng)絡(luò)請求宏粤。
fun <T> CoroutineScope.retrofit(dsl: RetrofitCoroutineDSL<T>.() -> Unit) {
//在主線程中開啟協(xié)程
this.launch(Dispatchers.Main) {
val coroutine = RetrofitCoroutineDSL<T>().apply(dsl)
coroutine.api?.let { call ->
//async 并發(fā)執(zhí)行 在IO線程中
val deferred = async(Dispatchers.IO) {
try {
call.execute() //已經(jīng)在io線程中了脚翘,所以調(diào)用Retrofit的同步方法
} catch (e: ConnectException) {
coroutine.onFail?.invoke("網(wǎng)絡(luò)連接出錯", -1)
null
} catch (e: IOException) {
coroutine.onFail?.invoke("未知網(wǎng)絡(luò)錯誤", -1)
null
}
}
//當(dāng)協(xié)程取消的時候,取消網(wǎng)絡(luò)請求
deferred.invokeOnCompletion {
if (deferred.isCancelled) {
call.cancel()
coroutine.clean()
}
}
//await 等待異步執(zhí)行的結(jié)果
val response = deferred.await()
if (response == null) {
coroutine.onFail?.invoke("返回為空", -1)
} else {
response.let {
if (response.isSuccessful) {
//訪問接口成功
if (response.body()?.status == 1) {
//判斷status 為1 表示獲取數(shù)據(jù)成功
coroutine.onSuccess?.invoke(response.body()!!.data)
} else {
coroutine.onFail?.invoke(response.body()?.msg ?: "返回數(shù)據(jù)為空", response.code())
}
} else {
coroutine.onFail?.invoke(response.errorBody().toString(), response.code())
}
}
}
coroutine.onComplete?.invoke()
}
}
}
在上面的代碼中绍哎,比較難理解的是下面的代碼:
val coroutine = RetrofitCoroutineDSL<T>().apply(dsl)
dsl是帶接收者的函數(shù)字面量来农,接收者是RetrofitCoroutineDSL,所有先創(chuàng)建一個接受者對象崇堰,然后將傳入的實參dsl賦值給該對象沃于。還可以寫成下面的樣子:
val coroutine = RetrofitCoroutineDsl<T>()
coroutine.dsl()
上面的寫法是直接調(diào)用函數(shù)字面量。為了方便里面海诲,把上述代碼翻譯成對應(yīng)的Java代碼:
RetrofitCoroutineDsl<T> coroutine = new RetrofitCoroutineDsl<T>();
dsl.invoke(coroutine);
調(diào)用函數(shù)dsl并傳入coroutine,其實就是把dsl賦值給coroutine揽涮。
3.最后一步,讓BaseActivity實現(xiàn)接口CoroutineScope饿肺,這樣在頁面中的上下文就是協(xié)程下上文
open class BaseActivity : AppCompatActivity(), CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}
override fun onDestroy() {
super.onDestroy()
// 關(guān)閉頁面后,結(jié)束所有協(xié)程任務(wù)
job.cancel()
}
}
是CoroutineContext中的運算符重載盾似,包含兩者的上下文:
//Returns a context containing elements from this context and elements from other [context].
//The elements from this context with the same key as in the other one are dropped.
public operator fun plus(context: CoroutineContext): CoroutineContext
在Activity中可以直接調(diào)用擴展函數(shù)retrofit來調(diào)用網(wǎng)絡(luò)請求:
retrofit<User> {
api = RetrofitCreater.create(Api::class.java).login()
onSuccess {
Log.e(TAG, "result = ${it?.avatar}")
}
onFailed { msg, _ ->
Log.e(TAG, "onFailed = $msg")
}
}
如果不需要處理訪問失敗的情況敬辣,可以寫成下面的樣子:
retrofit<User> {
api = RetrofitCreater.create(Api::class.java).login()
onSuccess {
Log.e(TAG, "result = ${it?.avatar}")
}
}
使用協(xié)程可以更好的控制任務(wù)的執(zhí)行,并且比線程更加的節(jié)省資源零院,更加的高效溉跃。結(jié)合DSL的代碼風(fēng)格,可以讓我們的程序更加直觀易懂告抄、簡潔優(yōu)雅撰茎。