Android上的協(xié)程:簡介

協(xié)程是一種并發(fā)設(shè)計模式,在 Android 平臺上可以使用它來簡化異步執(zhí)行的代碼。

特點

  • 輕量:因為協(xié)程支持掛起,不會使正在運行協(xié)程的線程發(fā)生阻塞。掛起比阻塞節(jié)省內(nèi)存,且支持多個并行操作
  • 內(nèi)存泄漏更少:使用結(jié)構(gòu)化并發(fā)(Structured concurrency)機制在一個作用域內(nèi)執(zhí)行多項操作
  • 內(nèi)置取消支持:取消操作會自動在運行中的整個協(xié)程層次結(jié)構(gòu)內(nèi)傳播
  • Jetpack集成:許多Jetpack庫都包含提供全面協(xié)程支持的擴展,某些庫還提供自己的協(xié)程操作域,可供開發(fā)者用于結(jié)構(gòu)化并發(fā)

依賴庫

如需在Android項目中使用協(xié)程,需將以下依賴項添加到對應(yīng)modulebuild.gradle文件中:

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:<version>'
}

執(zhí)行后臺線程

如下示例代碼我們在主線程上發(fā)起網(wǎng)絡(luò)請求,主線程會處于等待或阻塞狀態(tài),直到收到網(wǎng)絡(luò)響應(yīng)。

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

class LoginRepository(private val responseParser: LoginResponseParser) {
    private const val loginUrl = "https://example.com/login"

    // Function that makes the network request, blocking the current thread
    fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())
            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

makeLoginRequest 是同步的,并且會阻塞發(fā)起調(diào)用的線程。為了對網(wǎng)絡(luò)請求的響應(yīng)建模,我們創(chuàng)建了自己的 Result 類。ViewModel 會在用戶點擊(例如,點擊按鈕)時觸發(fā)網(wǎng)絡(luò)請求:

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

    fun login(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

由于此時主線程處于阻塞狀態(tài),但Android系統(tǒng)需要更新UI時將無法調(diào)用onDraw(),這時將會導(dǎo)致應(yīng)用卡頓,并有可能產(chǎn)生應(yīng)用無響應(yīng)(ANR)對話框。為了更好的用戶體驗,我們就需要將網(wǎng)絡(luò)請求的操作放在后臺線程上去執(zhí)行。最簡單的方法就是創(chuàng)建一個新的協(xié)程,然后在I/O線程上執(zhí)行網(wǎng)絡(luò)請求:

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)
        }
    }
}

下面我們仔細分析一下login函數(shù)中的協(xié)程代碼:

  • viewModelScope是預(yù)定義的CorutineScop,包含在ViewModel KTX擴展中。請注意,所有協(xié)程都必須在一個作用域內(nèi)運行。一個CoroutineScope管理一個或多個相關(guān)的協(xié)程。
  • launch是一個函數(shù),用于創(chuàng)建協(xié)程并將其函數(shù)主體的執(zhí)行分派給相應(yīng)的調(diào)試程序。
  • Dispatchers.IO指示協(xié)程應(yīng)在為I/O操作預(yù)留的線程上執(zhí)行。

login函數(shù)按以下方式執(zhí)行:

  • 應(yīng)用從主線程上的View層調(diào)用login函數(shù)。
  • launch會創(chuàng)建一個新的協(xié)程,并且網(wǎng)絡(luò)請求在為I/O操作預(yù)留的線程上獨立發(fā)出。
  • 在該協(xié)程運行時,login函數(shù)會繼續(xù)執(zhí)行,并可能在網(wǎng)絡(luò)請求完成前返回。為模型簡單起見,我們暫時忽略網(wǎng)絡(luò)響應(yīng)。

由于些協(xié)程是通過viewModelScope啟動的,因此些協(xié)程的所有操作都在ViewModel的作用域內(nèi)執(zhí)行。如果ViewModel被銷毀,則viewModelScop也會被自動取消,且所有的協(xié)程也會被取消。

以上示例還存在一個問題,就是怎樣保證makeLoginRequest的所有調(diào)用都是在子線程中執(zhí)行,從而確保主線程安全呢?

使用線程確保主線程安全

如果函數(shù)操作不會阻塞主線程更新UI,我們即將其視為主線程安全。這里makeLoginRequest函數(shù)就不是主線程安全,因為在主線程調(diào)用makeLoginRequest會阻塞UI??梢允褂脜f(xié)程庫中的witContext()函數(shù)將協(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é)程的執(zhí)行操作移至一個I/O線程,從而保證主線程安全。suspend關(guān)鍵字強制標記此函數(shù)在協(xié)程內(nèi)調(diào)用

由于makeLoginRequest已將執(zhí)行操作移出主線程,由login函數(shù)中的協(xié)程可以在主線程中執(zhí)行:

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
            }
        }
    }
}

?請注意,此處仍需要協(xié)程,因為 makeLoginRequest 是一個 suspend 函數(shù),而所有 suspend 函數(shù)都必須在協(xié)程中執(zhí)行。

最后修改的的區(qū)別之處:

  • launch沒有Dispatchers.IO參數(shù)。如果launch沒有Dispatcher參數(shù),則從viewModelScope啟動的所有協(xié)程都會在主線程中執(zhí)行。
  • 返回網(wǎng)絡(luò)請求的處理結(jié)果到當(dāng)前主線程,成功或者失敗

login函數(shù)的執(zhí)行流程:

  • 應(yīng)用從主線程的view層調(diào)用login()``函數(shù)。
  • launch創(chuàng)建一個新的協(xié)程,在
  • 主線程上發(fā)出網(wǎng)絡(luò)請求,然后該協(xié)程開始執(zhí)行。
  • 在協(xié)程內(nèi),調(diào)用 loginRepository.makeLoginRequest() 現(xiàn)在會掛起協(xié)程的進一步執(zhí)行操作,直至 makeLoginRequest() 中的 withContext 塊結(jié)束運行。
  • withContext 塊結(jié)束運行后,login() 中的協(xié)程在主線程上恢復(fù)執(zhí)行操作,并返回網(wǎng)絡(luò)請求的結(jié)果。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容