協(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)module的build.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,包含在ViewModelKTX擴展中。請注意,所有協(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é)果。