Kotlin協(xié)程

協(xié)程是什么

協(xié)程是一個子程序調(diào)度組件寞酿,并且運行其掛起恢復(fù)匠襟。進程包含線程钝侠,線程包含協(xié)程。一個線程中可以有任意多個協(xié)程宅此,但某一時刻只能有一個協(xié)程在運行机错,多個協(xié)程共享該線程的資源。
協(xié)程是一種并發(fā)設(shè)計的模式父腕,用來執(zhí)行異步執(zhí)行的代碼弱匪。

協(xié)程的特點

輕量:可以在單個線程上運行多個協(xié)程,協(xié)程支持掛起璧亮,不會是正在執(zhí)行協(xié)程的線程阻塞萧诫。掛起比阻塞節(jié)省內(nèi)存,且支持多個并行操作枝嘶。
內(nèi)存泄露更少:使用結(jié)構(gòu)化并發(fā)機制在一個作用域內(nèi)執(zhí)行多項操作帘饶。
內(nèi)置取消機制:取消操作會自動在整個協(xié)程層次結(jié)構(gòu)內(nèi)傳播。
jetpack集成:許多庫提供全面協(xié)程支持的擴展群扶。

添加依賴

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}
class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        // Create a new coroutine to move the execution off the UI thread
        viewModelScope.launch(Dispatchers.IO) {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}

viewModelScope屬于view model ktx擴展的內(nèi)容及刻,需要添加view model ktx依賴镀裤,它是一個預(yù)定義的CoroutineScope,一個CoroutineScope管理一個或多個相關(guān)的協(xié)程缴饭。所有協(xié)程都必須在一個作用域內(nèi)運行暑劝。
launch是一個函數(shù),用于創(chuàng)建協(xié)程并將其函數(shù)主體的執(zhí)行分派給相應(yīng)的調(diào)度程序颗搂。
Dispatchers.IO指示此協(xié)程應(yīng)在為I/O操作預(yù)留的線程上執(zhí)行担猛。
在該協(xié)程運行時,login函數(shù)會繼續(xù)執(zhí)行丢氢,并可能在網(wǎng)絡(luò)請求完成前返回傅联。
由于此協(xié)程通過viewModelScope啟動,因此在viewModel的作用域內(nèi)執(zhí)行疚察。如果view model銷毀蒸走,則viewModelScope會自動取消,所有運行的協(xié)程也會被取消稍浆。

class LoginRepository(...) {
    ...
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {

        // Move the execution of the coroutine to the I/O dispatcher
        return withContext(Dispatchers.IO) {
            // Blocking network request code
        }
    }
}

withContext(Dispatchers.IO)將協(xié)程的操作移至一個IO線程载碌,一種不錯的做法是使用withContext來確保每個函數(shù)都是主線程安全的。
makeLoginRequest還會用關(guān)鍵字suspend進行標(biāo)記衅枫。kotlin利用此關(guān)鍵字強制從協(xié)程內(nèi)調(diào)用函數(shù)嫁艇。

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {

        // Create a new coroutine on the UI thread
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"

            // Make the network call and suspend execution until it finishes
            val result = loginRepository.makeLoginRequest(jsonBody)

            // Display result of the network request to the user
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

因為makeLoginRequest是一個suspend函數(shù),而所有suspend函數(shù)都必須在協(xié)程中執(zhí)行弦撩,所以此處需要協(xié)程步咪。
launch未接受Dispatcher參數(shù),那么從viewModelScope啟動所有的協(xié)程都會在主線程中執(zhí)行益楼。
協(xié)程添加了suspend和resume來處理長時間執(zhí)行的任務(wù)猾漫。
suspend用來暫停執(zhí)行當(dāng)前協(xié)程,并保存所有局部變量感凤。
resume用于讓已掛起的協(xié)程從掛起處繼續(xù)執(zhí)行悯周。
如需調(diào)用suspend函數(shù),只能從其他suspend函數(shù)進行調(diào)用或通過使用協(xié)程構(gòu)建器來啟動新的協(xié)程進行調(diào)用陪竿。

suspend fun fetchDocs() {                             // Dispatchers.Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    show(result)                                      // Dispatchers.Main
}

suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }

get會在啟動網(wǎng)絡(luò)請求之前掛起協(xié)程禽翼,當(dāng)網(wǎng)絡(luò)請求完成時,get會恢復(fù)已掛起的協(xié)程族跛,而不是使用回調(diào)通知主線程闰挡。
協(xié)程使用調(diào)度程序確定哪些線程用于執(zhí)行協(xié)程。在kotlin中礁哄,所有協(xié)程都必須在調(diào)度程序中執(zhí)行长酗,協(xié)程可以自行掛起,而調(diào)度程序負責(zé)將其恢復(fù)桐绒。
kotlin提供了三個調(diào)度程序:
Dispatchers.Main:用于在主線程上運行協(xié)程夺脾,用于和界面交互和快速工作之拨,比如調(diào)用suspend函數(shù),更細UI等咧叭。
Dispatchers.IO:在主線程之外執(zhí)行磁盤或網(wǎng)絡(luò)I/O
*Dispatchers.Default:在主線程之外執(zhí)行大量占用CPU資源的操作

啟動協(xié)程的兩種方式

launch:可啟動新協(xié)程而不將結(jié)果返回給調(diào)用方敦锌,用于從常規(guī)函數(shù)啟動新協(xié)程。
async:會啟動一個新協(xié)程佳簸,并允許使用名為await的掛起函數(shù)返回結(jié)果,用于在另一個協(xié)程內(nèi)或在掛起函數(shù)內(nèi)且正在執(zhí)行并行分解時才使用此方式颖变。
coroutineScope用于啟動一個或多個協(xié)程生均,然后,可以使用await或awaitAll等待協(xié)程返回結(jié)果之后函數(shù)再返回腥刹。

suspend fun fetchTwoDocs() =        // called on any Dispatcher (any thread, possibly Main)
    coroutineScope {
        val deferreds = listOf(     // fetch two docs at the same time
            async { fetchDoc(1) },  // async returns a result for the first doc
            async { fetchDoc(2) }   // async returns a result for the second doc
        )
        deferreds.awaitAll()        // use awaitAll to wait for both network requests
    }

協(xié)程概念

CoroutineScope會跟蹤它使用launch或aysnc創(chuàng)建的所有協(xié)程马胧。可以隨時調(diào)用scope.cancel取消正在運行的工作衔峰。

class ExampleClass {

    // Job and Dispatcher are combined into a CoroutineContext which
    // will be discussed shortly
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine within the scope
        scope.launch {
            // New coroutine that can call suspend functions
            fetchDocs()
        }
    }

    fun cleanUp() {
        // Cancel the scope to cancel ongoing coroutines work
        scope.cancel()
    }
}

已取消的作用域無法再創(chuàng)建協(xié)程佩脊。僅當(dāng)控制其生命周期的類被銷毀時才應(yīng)調(diào)用cancel。使用viewModelScope時會自動在ViewModel#onCleared()時自動取消作用域垫卤。
Job是協(xié)程的句柄威彰,新創(chuàng)建的每個job都返回一個job實例,通過它可以管理協(xié)程的生命周期穴肘。

class ExampleClass {
    ...
    fun exampleMethod() {
        // Handle to the coroutine, you can control its lifecycle
        val job = scope.launch {
            // New coroutine
        }

        if (...) {
            // Cancel the coroutine started above, this doesn't affect the scope
            // this coroutine was launched in
            job.cancel()
        }
    }
}
CoroutinContext使用以下元素定義協(xié)程的行為:

Job:控制協(xié)程的生命周期歇盼;
CoroutineDispatcher: 將工作分配到適當(dāng)?shù)木€程;
CoroutineName: 協(xié)程的名稱评抚;
CoroutineExceptionHandler:處理未捕獲的異常豹缀;

Android協(xié)程最佳做法

注入調(diào)度程序
在創(chuàng)建新協(xié)程或調(diào)用withContext時,請勿對Dispatchers硬編碼

// DO inject Dispatchers
class NewsRepository(
    private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
    suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }
}

// DO NOT hardcode Dispatchers
class NewsRepository {
    // DO NOT use Dispatchers.Default directly, inject it instead
    suspend fun loadNews() = withContext(Dispatchers.Default) { /* ... */ }
}

掛起函數(shù)應(yīng)該能夠安全的從主線程調(diào)用
掛起函數(shù)應(yīng)該是主線程安全的慨代。

class NewsRepository(private val ioDispatcher: CoroutineDispatcher) {

    // As this operation is manually retrieving the news from the server
    // using a blocking HttpURLConnection, it needs to move the execution
    // to an IO dispatcher to make it main-safe
    suspend fun fetchLatestNews(): List<Article> {
        withContext(ioDispatcher) { /* ... implementation ... */ }
    }
}

// This use case fetches the latest news and the associated author.
class GetLatestNewsWithAuthorsUseCase(
    private val newsRepository: NewsRepository,
    private val authorsRepository: AuthorsRepository
) {
    // This method doesn't need to worry about moving the execution of the
    // coroutine to a different thread as newsRepository is main-safe.
    // The work done in the coroutine is lightweight as it only creates
    // a list and add elements to it
    suspend operator fun invoke(): List<ArticleWithAuthor> {
        val news = newsRepository.fetchLatestNews()

        val response: List<ArticleWithAuthor> = mutableEmptyList()
        for (article in news) {
            val author = authorsRepository.getAuthor(article.author)
            response.add(ArticleWithAuthor(article, author))
        }
        return Result.Success(response)
    }
}

ViewModel來創(chuàng)建協(xié)程
應(yīng)該由ViewModel來創(chuàng)建協(xié)程邢笙,而不是公開掛起函數(shù)來執(zhí)行業(yè)務(wù)邏輯。

// DO create coroutines in the ViewModel
class LatestNewsViewModel(
    private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {

    private val _uiState = MutableStateFlow<LatestNewsUiState>(LatestNewsUiState.Loading)
    val uiState: StateFlow<LatestNewsUiState> = _uiState

    fun loadNews() {
        viewModelScope.launch {
            val latestNewsWithAuthors = getLatestNewsWithAuthors()
            _uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors)
        }
    }
}

// Prefer observable state rather than suspend functions from the ViewModel
class LatestNewsViewModel(
    private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {
    // DO NOT do this. News would probably need to be refreshed as well.
    // Instead of exposing a single value with a suspend function, news should
    // be exposed using a stream of data as in the code snippet above.
    suspend fun loadNews() = getLatestNewsWithAuthors()
}

不要公開可變類型
最好向其他類公開不可變類型侍匙。

// DO expose immutable types
class LatestNewsViewModel : ViewModel() {

    private val _uiState = MutableStateFlow(LatestNewsUiState.Loading)
    val uiState: StateFlow<LatestNewsUiState> = _uiState

    /* ... */
}

class LatestNewsViewModel : ViewModel() {

    // DO NOT expose mutable types
    val uiState = MutableStateFlow(LatestNewsUiState.Loading)

    /* ... */
}

數(shù)據(jù)層或業(yè)務(wù)層應(yīng)公開掛起函數(shù)或數(shù)據(jù)流氮惯。

參考:
https://developer.android.com/kotlin/coroutines?hl=zh-cn#kts

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市丈积,隨后出現(xiàn)的幾起案子筐骇,更是在濱河造成了極大的恐慌,老刑警劉巖江滨,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铛纬,死亡現(xiàn)場離奇詭異,居然都是意外死亡唬滑,警方通過查閱死者的電腦和手機告唆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門棺弊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人擒悬,你說我怎么就攤上這事模她。” “怎么了懂牧?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵侈净,是天一觀的道長。 經(jīng)常有香客問我僧凤,道長畜侦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任躯保,我火速辦了婚禮旋膳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘途事。我一直安慰自己验懊,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布尸变。 她就那樣靜靜地躺著义图,像睡著了一般。 火紅的嫁衣襯著肌膚如雪振惰。 梳的紋絲不亂的頭發(fā)上歌溉,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天,我揣著相機與錄音骑晶,去河邊找鬼痛垛。 笑死,一個胖子當(dāng)著我的面吹牛桶蛔,可吹牛的內(nèi)容都是我干的匙头。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼仔雷,長吁一口氣:“原來是場噩夢啊……” “哼蹂析!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起碟婆,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤电抚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后竖共,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蝙叛,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年公给,在試婚紗的時候發(fā)現(xiàn)自己被綠了借帘。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜘渣。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖肺然,靈堂內(nèi)的尸體忽然破棺而出蔫缸,到底是詐尸還是另有隱情,我是刑警寧澤际起,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布拾碌,位于F島的核電站,受9級特大地震影響街望,放射性物質(zhì)發(fā)生泄漏倦沧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一它匕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧窖认,春花似錦豫柬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至喝噪,卻和暖如春础嫡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酝惧。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工榴鼎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人晚唇。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓巫财,卻偏偏與公主長得像,于是被迫代替她去往敵國和親哩陕。 傳聞我的和親對象是個殘疾皇子平项,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容