问:你对Kotlin的协程理解
答:协程可以看作是一种轻量级的线程,只不过线程有系统调度,而协程由代码来控制。协程允许我们在单线程模式下模拟多线程编程效果,代码的执行与挂起完全由编程语言来控制,与系统无关。
本篇文章文字内容可能较多,跟着官方文档的思路来解答Kotlin的协程:Kotlin协程
一、特点:
- 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。(单线程模拟多线程编程效果)
- 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
- 内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。
- Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。
二、在后台线程中执行
Android中,如果在主线程(UI线程)中执行耗时操作,则会导致线程阻塞直到收到响应,如果一直处于阻塞状态(Activity 五秒钟),即可能发生ANR,所以需要子线程来执行耗时操作,这里所说的在后台线程中执行就是子线程中执行。
//使用密封类,后续解析Result时不需要特别指定else
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) {
fun makeLoginRequest(jsonBody: String): Result<LoginResponse> {
//执行网络请求,如果成功
if(success){
return Result.Success(responseParser.parse(inputStream))
}
return Result.Error(Exception("Cannot open HttpURLConnection")
}
}
class LoginViewModel(private val loginRepository: LoginRepository):ViewModel() {
fun login(username: String, token: String) {
//当调在主线程中调用login方法时,耗时方法则阻塞主线程,可能产生ANR
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
如上代码,如果在Activity或其他主线程所在线程中调用login方法,则可能造成主线程阻塞,造成ANR。在协程以前,一般使用Thread、AsyncTask、RxJava 等来进行 异步 、开启线程、线程切换 等等一系列操作。但是在协程出现以后:
class LoginViewModel(private val loginRepository: LoginRepositor): ViewModel() {
fun login(username: String, token: String) {
//创建一个协程来执行线程操作
viewModelScope.launch(Dispatchers.IO) {
//viewModelScope其实是androidx.lifecycle.viewModelScope,用于开启协程,Dispatchers.IO 指定协程运行在IO线程
//其中Dispatchers还有 Dispatchers.MAIN(指定协程在主线程中执行) , Dispatchers.DEFAULT,用于在CPU密集型计算的协程中。
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
}
前面提到过,协程是用来执行异步、耗时任务的,标记makeLoginRequest()方法为“耗时任务”需要使用 suspend 关键字(这里叫 挂起函数 )其实: 凡是耗时函数/方法,就应该标上suspend关键字前缀,让别人知道它是个耗时函数/方法。并且它只能在协程里,或者另外一个同样标记了suspend关键字的函数/方法里调用
修改LoginRepository中的代码:
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
}
}
}
使用suspend关键字修饰makeLoginRequest函数,并且使用withContext(Dispatchers.IO)挂起函数,同时指定其运行的线程为IO线程。此时,当前协程会阻塞,不影响主线程执行。修改代码:
class LoginViewModel(private val loginRepository: LoginRepositor): ViewModel() {
fun login(username: String, token: String) {
//创建一个协程来执行线程操作
viewModelScope.launch() {
//makeLoginRequest方法为挂起函数,同时使用withContext(Dispatchers.IO)指定挂起函数运行于IO线程
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
//协程执行完成网络请求后返回结果,
val result = loginRepository.makeLoginRequest(jsonBody)
//doSomething
}
}
}
以上即为协程的简单使用与理解。
聊聊协程的一些关键字/函数的理解:
GlobalScope.launch{} 开启一个顶级协程,创建一个协程作用域,当应用程序运行结束也会跟着一起结束。
fun main(){
GlobalScope.launch{
System.out.println("GlobalScope.launch 顶级协程中运行的代码")
}
}//运行程序会发现没有任何打印,因为应用程序结束的时候还没来得及打印协程也跟着停止了。
fun main(){
GlobalScope.launch{
//delay(1500) //挂起函数,在协程或其他挂起函数中可以调用,非阻塞式挂起函数,只会挂起协程,不影响当前线程下的其他协程。
//如果开启,则协程还没打印时,应用程序已经结束,则不打印。
System.out.println("GlobalScope.launch 顶级协程中运行的代码")
}
Thread.sleep(1000)
} //线程睡眠1000毫秒,协程执行打印时,应用还没有停止,则打印。
runBlocking: 创建一个协程作用域,阻塞当前线程,直到协程作用域中的代码全部执行完毕。不常用。
launch: 创建一个协程,只能在协程作用域中调用。
suspend: 声明一个函数为挂起函数,协程中遇到挂起函数则会 挂起 ,直到函数执行完成后 恢复 到挂起位置
coroutineScope: 一个挂起函数,继承外部的协程作用域并创建一个子作用域,这样就可以给任意函数提供协程作用域了。注意:挂起函数只能在协程作用域或其他挂起函数中调用,所以,最终,coroutineScope函数一定是在协程作用域中的。coroutineScope函数会一直阻塞当前 协程 如:
fun main(){
GlobalScope.launch{
coroutineScope{ //继承外部协程作用域,一直阻塞当前协程。
launch{ //运行在coroutineScope的子作用域中
delay(1500)
System.out.println("我是协程1呀")
}
}
coroutineScope{ //继承外部协程作用域,一直阻塞当前协程。
launch{ //运行在coroutineScope的子作用域中
delay(500)
System.out.println("我是协程2呀")
}
}
}
System.out.println("我是线程打印的呀")
Thread.sleep(1600)
}
//执行结果:
我是线程打印的呀
我是协程1呀
//我是协程2不打印
Job: GlobalScope.launch{}、launch{} 函数都会返回一个Job对象,其可以调用cancel()方法取消协程。
CoroutineScope(job): CoroutineScope(job).launch中创建的所有协程,都会关联在Job对象的作用域下,只需要调用一次cancel 就可以将其创建的所有协程一次性取消。
async: 函数只能在协程作用域中调用,创建一个新的协程并返回 Deferred 对象,Deferred对象可以调用await()函数来获取协程执行结果。同时await函数是一个阻塞函数,会将当前协程作用域阻塞,影响其他协程执行。如果这个协程作用域中有两个及以上,想要同时并发执行的话,可以之后进行调用。
fun main(){
runBlocking{
val result = async{
delay(1000)
5+5
}
val result2 = async{
delay(1000)
4+6
}
System.out.println("执行结果:$result.await() + $result2.await()") //打印执行结果:20
//如果在async后面直接加await()函数的话耗时差不多2000毫秒多(串行),但是上面的例子只耗时1000毫秒多(并行)。
}
}
withContext(): withContext函数必须指定执行线程withContext(Dispatchers.IO)、 withContext(Dispatchers.Main)、withContext(Dispatchers.Default), IO毫无疑问执行在IO操作较多的协程(网络请求、数据库、文件等IO操作), Main指定协程在主线程中执行,Default低并发线程策略,所有协程默认Default,在CPU进行密集型计算时使用。
suspendCoroutine: 能将传统回调机制的写法大幅简化,必须在协程作用域或挂起函数中调用。接收一个Lambda表达式,将当前协程挂起,在Lambda表达式中午传入Coroutine对象,调用它的resume、resumeWithException既可以让当前协程恢复执行。
//传统写法:
fun test(object: CallListener{
override fun finish(){
//doSomething()
}
override fun error(){
//doSomething()
}
})
//使用suspendCoroutine写法
suspend fun httpRequest(){
suspendCoroutine{ continuation->
test(object: CallListener{
override fun finish(){
continuation.resume()
}
override fun error(){
continuation.resumeWithException()
}
})
}
}
//之后调用test方法则只需要调用 httpRequest()方法即可。
网友评论