前言
最近新项目开端,老总发话说咱们要用新技能,不能再运用老的架构和技能了。迫于无法,开端Google引荐的新架构学习,基于单一数据源和单项数据流驱动的MVVM架构。在学习的过程中,又体系的了解了一遍Android协程的运用。有了一些新的感悟,就记录在此了。
本文根本转载自Android官方文档,加了少许个人见解。大佬们看到请轻喷。
一、协程的诞生
众所周知,Android为了主线程安全,是不能在主线程上去履行任何耗时操作的。开发者在进行耗时操作时,需求自己发动子线程后放在子线程中运转,过程中会产生很多的线程办理代码。协程的诞生就是为了优化这一操作,协程是一种并发的设计模式,能够在Android平台上运用它来简化需求异步履行的代码。
协程的特色
协程是Google引荐的在 Android 上进行异步编程的引荐处理方案。它具有一下特色:
- 轻量:您能够在单个线程上运转多个协程,因为协程支撑挂起,不会使正在运转协程的线程堵塞。挂起比堵塞节省内存,且支撑多个并行操作。
- 内存走漏更少:运用结构化并发机制在一个效果域内履行多项操作。
- 内置撤销支撑:撤销功能会主动经过正在运转的协程层次结构传达。
- Jetpack 集成:许多 Jetpack 库都包括提供全面协程支撑的扩展。某些库还提供自己的协程效果域,可供您用于结构化并发。
二、协程的运用
在后台线程中履行
如果在主线程上发出网络恳求,则主线程会处于等候或堵塞状态,直到收到呼应。因为线程处于堵塞状态,因而操作体系无法调用 onDraw(),这会导致运用冻住,并有或许导致弹出“运用无呼应”(ANR) 对话框。为了处理这个问题,一般开发中咱们会在后台线程上履行网络恳求等耗时操作。
下面咱们以一个简单的登录恳求为例,看一下协程操作的运用办法。
首要,咱们先看一下在Google引荐的架构中,Repository 类是怎么发出恳求的:
//网络恳求呼应成果实体封装类
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"))
}
}
上面的代码中为了对网络恳求的呼应数据做处理,咱们创立了自己的 Result 类。其间在 makeLoginRequest 是同步履行函数,会堵塞建议调用的线程。
ViewModel 会在用户与界面产生交互(例如,点击登录按钮)时触发网络恳求:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
如果咱们直接运用上述代码,LoginViewModel 就会在网络恳求发出时堵塞界面线程。如需将履行操作移出主线程,咱们以往的办法是发动一个新的线程去履行:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
//创立线程并发动,履行登录恳求。
Thread{
Runnable {
loginRepository.makeLoginRequest(jsonBody)
}
}.start()
}
}
上面这样的做法,会让咱们在每次履行登网络恳求时都创立一个线程,而且在恳求完结后需求运用回调和handler把恳求成果重新传递给主线程处理。而协程的出翔让咱们有个更简单的办法,就是创立一个新的协程,然后在 I/O 线程上履行网络恳求:
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
//创立一个新的协程,使其移出UI线程履行, Dispatchers.IO: I/O 操作预留的线程
viewModelScope.launch(Dispatchers.IO) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
//履行网络恳求操作,该恳求会在I/O 操作预留的线程上履行
loginRepository.makeLoginRequest(jsonBody)
}
}
}
下面咱们仔细分析一下 login 函数中的协程代码:
- viewModelScope 是预定义的 CoroutineScope,包括在 ViewModel KTX 扩展中(详见此处)。请注意,一切协程都必须在一个效果域内运转。一个 CoroutineScope 办理一个或多个相关的协程。
- launch 是一个函数,用于创立协程并将其函数主体的履行分派给相应的调度程序,(Dispatchers.IO) 为可选参数。
- Dispatchers.IO 指示此协程应在为 I/O 操作预留的线程上履行。
login 函数按以下办法履行:
- 运用从主线程上的 View 层调用 login 函数(点击登录按钮)。
- launch 会创立一个新的协程,而且网络恳求在为 I/O 操作预留的线程上独立发出。
- 在该协程运转时,login 函数会继续履行,并或许在网络恳求完结前回来(恳求不会堵塞主线程后续操作)。
因为此协程经过 viewModelScope 发动,因而在 ViewModel 的效果域内履行。如果 ViewModel 因用户离开屏幕而被毁掉,则 viewModelScope 会主动撤销,且一切运转的协程也会被撤销。
前面的示例存在的两个问题是,一是调用 makeLoginRequest 的任何项都需求记得将履行操作显式移出主线程,即在 launch 函数后传入 (Dispatchers.IO) 参数。二是没有对登录恳求的成果做处理。下面咱们来看看怎么修改 Repository 以处理这一问题。
运用协程确保主线程安全
如果函数不会在主线程上阻止界面更新,咱们即将其视为是主线程安全的。makeLoginRequest 函数不是主线程安全的,因为从主线程调用 makeLoginRequest 的确会堵塞界面。在上面的代码示例中,咱们能够在 ViewModel 中发动协程,并分配对应的调度程序,但是这种做法需求咱们每次在调用 makeLoginRequest 时都要去尾货调度程序。为了处理该问题咱们能够运用协程库中的 withContext() 函数将协程的履行操作移至其他线程:
class LoginRepository(...) {
private const val loginUrl = "https://example.com/login"
//suspend 关键字表示改办法会堵塞线程,Kotlin 利用此关键字强制从协程内调用函数。
suspend fun makeLoginRequest(
jsonBody: String
): Result<LoginResponse> {
// Move the execution of the coroutine to the I/O dispatcher
//表示协程的后续履行会被放在IO线程中
return withContext(Dispatchers.IO) {
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"))
}
}
}
withContext(Dispatchers.IO) 将协程的履行操作移至一个 I/O 线程,这样一来,咱们的调用函数就是主线程安全的,而且支撑根据需求更新界面。
makeLoginRequest 还会用 suspend 关键字进行符号。Kotlin 利用此关键字强制从协程内调用函数。
接下来咱们在 ViewModel 中,因为 makeLoginRequest 将履行操作移出主线程,login 函数中的协程现在能够在主线程中履行:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine on the UI thread
//直接在UI主线程中发动一个协程
viewModelScope.launch {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
// Make the network call and suspend execution until it finishes
//履行网络操作,而且等候被suspend符号的函数履行完结。
//该等候并不会堵塞主线程,因为被suspend符号的函数会被分配到IO线程履行
val result = loginRepository.makeLoginRequest(jsonBody)
// Display result of the network request to the user
//当收到恳求成果后,向用户实际恳求成果,并更行对应界面
when (result) {
is Result.Success<LoginResponse> -> // 登录成功,跳转主页。。。
else -> // 登录失利,提示用户过错信息
}
}
}
}
请注意,此处仍需求协程,因为 makeLoginRequest 是一个 suspend 函数,而一切 suspend 函数都必须在协程中履行。
此代码与前面的 login 示例的不同之处体现在以下几个方面:
- launch 不接受 (Dispatchers.IO) 参数。默许从 viewModelScope 发动的一切协程都会在主线程中运转。
- 体系现在会处理网络恳求的成果,以显示成功或失利界面。
login 函数现在按以下办法履行:
- 运用从主线程上的 View 层调用 login() 函数。
- launch 在主线程上创立新协程,然后协程开端履行。
- 在协程内,调用 loginRepository.makeLoginRequest() 现在会挂起协程的进一步履行操作,直至 makeLoginRequest() 中的 withContext 块完毕运转。
- withContext 块完毕运转后,login() 中的协程在主线程上恢复履行操作,并回来网络恳求的成果。
- 收到成果后,处理对应的成果并更行UI
处理反常
在进行网络恳求或许耗时操作时,经常会抛出反常。为了处理 Repository 或许出现的反常,咱们能够运用 try-catch 块捕捉并处理对应反常:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun makeLoginRequest(username: String, token: String) {
viewModelScope.launch {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
//运用try-catch捕捉反常
val result = try {
loginRepository.makeLoginRequest(jsonBody)
} catch(e: Exception) {
Result.Error(Exception("Network request failed"))
}
when (result) {
is Result.Success<LoginResponse> -> // 登录成功,跳转主页。。。
else -> // 登录失利,提示用户过错信息
}
}
}
}
在上面代码示例中,makeLoginRequest() 调用抛出的任何意外反常都会处理为界面过错。
总结
这样咱们就运用协程完好完成了一个登录恳求的操作,在此过程中,咱们只需求在 loginRepository 中运用 withContext 函数声明调度程序,就能够避免耗时操作堵塞主线程的问题,而且不需求开发者自己去办理对应的线程。而且因为有 viewModelScope 的存在,使得咱们也不需求去特意处理页面毁掉后的恳求撤销问题。优化性能的同时,又大大减少了咱们的代码量。是一种优秀的异步代码处理模式。
参阅
Android 上的 Kotlin 协程
Kotlin协程基础攻略
详解 ViewModel 的那些事儿