在kotlin中,协程是可挂起计算的实例。它在概念上类似于线程,因为它需要一个代码块来运行,该代码块与其余代码同时工作。但是,协程不绑定到任何特定线程。它可能会在一个线程中暂停执行,并在另一个线程中恢复执行。
1,协程作用域函数分析
1.1,runBlocking:
在Kotlin中,runBlocking是一个非常关键的函数,尤其是在与协程(Coroutines)一起使用时。它主要用于在阻塞的上下文中启动一个新的协程作用域,并且等待该作用域中的所有协程完成。
runBlocking 是一个用于阻塞当前线程直到其内部所有协程完成的协程构建器。
runBlocking会启动一个新的协程作用域,并挂起当前线程,直到作用域中的所有协程完成。它主要依赖于Kotlin协程的挂起函数和调度器来管理协程的执行。
import kotlinx.coroutines.*
runBlocking {
// 启动一个新的协程
val result = async {
delay(1000L) // 模拟耗时操作
"World!"
}
println("Hello, ${result.await()}") // 等待结果并打印
}
上面的代码中,runBlocking启动了一个协程作用域,在该作用域内,我们使用async函数启动了一个新的协程。
这个协程会延迟1秒(通过delay函数),然后返回字符串"World!"。主线程在result.await()处被挂起,直到async协程完成并返回结果。
使用runBlocking的注意事项:
避免在UI线程中使用:尽管runBlocking在某些情况下很有用,但应该避免在UI线程(如Android的Main线程)中使用它,因为它会阻塞UI线程,导致应用界面无响应。
性能考虑:runBlocking会挂起当前线程,因此如果可能的话,应该尽量使用非阻塞的方式来处理协程。
替代方案:在UI线程中,通常可以使用lifecycleScope(在Android中)或其他生命周期感知的协程作用域来管理协程的生命周期,而不是使用runBlocking。
1.2,CoroutineScope
CoroutineScope 是 Kotlin 协程中的一个重要接口,它代表了一个协程的作用域。
在 CoroutineScope 中,可以启动新的协程,这些协程将继承作用域中的上下文(包括调度器、协程名称、异常处理等)。
CoroutineScope 通常与 launch 和 async 等协程构建器一起使用,以启动和管理协程。
接口定义:
CoroutineScope 是一个接口,它定义了一个扩展函数 launch 和一个 coroutineContext 属性。
launch 函数用于启动一个新的协程,而 coroutineContext 属性则提供了当前作用域的协程上下文。
val coroutineScope = CoroutineScope(Dispatchers.Default)
coroutineScope.launch(Dispatchers.Default) { // 在Default线程中开启协程
val text = withContext(Dispatchers.IO) { // 开启一个子协程,并指定在I/O线程,去做一个耗时操作
delay(1000L) // 1 做一个1000MS的耗时操作
"world"
}
print(text)
}
print("Hello, ")
Thread.sleep(1500L) // 让主线程睡1500MS,防止主线程关闭
上下文继承:
在 CoroutineScope 中启动的协程将继承作用域的协程上下文。
这意味着,如果你在 CoroutineScope 中设置了特定的调度器或异常处理器,那么在该作用域中启动的所有协程都将使用这些设置。
生命周期管理:
CoroutineScope 本身并不管理协程的生命周期。
相反,它提供了一个作用域,在这个作用域中你可以启动和管理协程。
生命周期管理通常是通过其他机制来实现的,比如在 Android 中使用 ViewModel 或 LifecycleOwner 来管理协程的生命周期。
CoroutineScope的常见扩展函数:
launch:在CoroutineScope中启动一个新的协程,并返回一个Job对象,用于管理协程的状态和取消操作。可以使用launch函数来执行异步任务,例如网络请求或耗时的计算。
async:在CoroutineScope中启动一个新的协程,并返回一个Deferred对象,用于获取协程的执行结果。可以使用async函数来执行需要返回结果的异步任务,例如获取远程数据或执行复杂的计算。
withContext:在CoroutineScope中切换协程的上下文,以便在不同的线程或调度器中执行协程。可以使用withContext函数来实现协程的线程切换,例如在后台线程执行耗时操作后返回主线程更新UI。
supervisorScope:在CoroutineScope中创建一个独立的子作用域,该作用域下的协程异常不会传播给父作用域。可以使用supervisorScope函数来创建一个独立的协程作用域,以便处理子协程的异常。
coroutineScope:在CoroutineScope中创建一个新的协程作用域,该作用域下的所有协程都会等待其它协程完成后才会结束。可以使用coroutineScope函数来创建一个协程作用域,以便在其中启动多个协程并等待它们的完成。
自定义作用域:
可以通过实现 CoroutineScope 接口来创建自定义作用域。这允许你根据特定需求来管理协程的启动和取消。
以下是一个简单的 CoroutineScope 使用示例,展示了如何在自定义作用域中启动协程:
class MyCoroutineScope : CoroutineScope {
// 使用 Default 调度器作为默认上下文
override val coroutineContext: CoroutineContext = Dispatchers.Default + SupervisorJob()
// 启动协程的扩展函数
fun doWork() {
launch {
// 在 Default 调度器上执行一些工作
delay(1000L) // 模拟耗时操作
println("Work done in MyCoroutineScope")
}
}
}
在main函数中,
runBlocking {
// 创建自定义作用域实例
val myScope = MyCoroutineScope()
// 在自定义作用域中启动协程
myScope.doWork()
// 等待一段时间以确保协程完成(在实际应用中,你可能会有更好的方式来管理协程的完成)
delay(2000L)
}
上面代码输出:Work done in MyCoroutineScope
取消协程的代码示例:
在main函数中,
runBlocking {
// 启动一个长时间运行的协程
val job = launch {
try {
for (i in 1..Int.MAX_VALUE) {
println("Processing $i")
delay(100) // 模拟长时间运行的任务
// 检查协程是否被取消
if (!isActive) {
println("Coroutine was cancelled")
return@launch
}
}
} catch (e: CancellationException) {
println("Caught CancellationException: ${e.message}")
}
}
// 允许协程运行一段时间,然后取消它
delay(2000) // 等待2秒
job.cancel() // 取消协程
println("Requested coroutine cancellation")
// 等待一段时间以确保取消操作生效(在实际应用中,你可能不需要这样做)
delay(1000)
println("Main coroutine scope finished")
}
输出:
Processing 1
......
Processing 19
Requested coroutine cancellation
Caught CancellationException: StandaloneCoroutine was cancelled
Main coroutine scope finished
上面的代码中,使用了 runBlocking 来创建一个新的协程作用域,并在其中启动了一个长时间运行的协程 job。我们使用了 try-catch 来捕获可能抛出的 CancellationException。
也可以采用下面的代码自定义协程域:
class YourClass {
// 声明 CoroutineScope 并使用 SupervisorJob 初始化
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var volJob: Job? = null
// 启动协程的函数
fun startFunc() {
volJob = coroutineScope.launch {
delay(5000)
withContext(Dispatchers.Main) {
......
}
}
}
// 取消协程的函数
fun cancelVolJob() {
volJob?.cancel()
}
// 取消整个协程作用域的函数
fun cancelCoroutineScope() {
coroutineScope.cancel()
}
}
对于上面的代码,Dispatchers.IO + SupervisorJob()组合成一个 CoroutineContext 对象。
CoroutineContext 的本质:
CoroutineContext 是一个接口,它代表了协程的上下文。
这个上下文可以包含多种元素,例如调度器(CoroutineDispatcher)、作业(Job)、异常处理器(CoroutineExceptionHandler)等。
这些元素都实现了 CoroutineContext.Element 接口,并且可以组合在一起形成一个完整的协程上下文。
Dispatchers.IO 和 Job 与 CoroutineContext 的关系:
Dispatchers.IO:Dispatchers.IO 是 CoroutineDispatcher 类型的实例,而 CoroutineDispatcher 是 CoroutineContext 的子类型。
Dispatchers.IO 主要用于处理 I/O 密集型任务,它决定了协程将在哪个线程或线程池上运行。
Job:Job 也是 CoroutineContext 的一个元素,它代表协程的生命周期,可以用来启动、取消和监控协程的执行状态。
在 Kotlin 中,CoroutineContext 接口对 + 运算符进行了重载,具体实现如下:
public operator fun CoroutineContext.plus(other: CoroutineContext): CoroutineContext =
if (other === EmptyCoroutineContext) this else CombinedContext(this, other)
这个重载方法允许将两个 CoroutineContext 元素组合在一起,返回一个新的 CoroutineContext 对象。
当你使用 + 运算符将 Dispatchers.IO 和 Job 组合时,实际上是调用了这个重载方法。
由于 Dispatchers.IO 和 Job 都是 CoroutineContext 的元素,所以它们可以通过 + 运算符组合成一个新的 CoroutineContext 对象。
1.3,lifecycleScope
lifecycleScope 并不是 Kotlin 标准库的一部分,而是与 Android 的 Jetpack 组件(如 androidx.lifecycle)以及协程库(如 kotlinx-coroutines-android)结合使用的自定义作用域。
在 Android 开发中,lifecycleScope 通常用于在 Android 组件(如 Activity 或 Fragment)的生命周期内管理协程。这确保了协程能够正确地启动、取消和清理,从而避免内存泄漏和不必要的资源消耗。
lifecycleScope 是一个生命周期感知的协程作用域,它允许你在 Android 组件的生命周期内启动协程。它通常与 lifecycleOwner(如 Activity 或 Fragment)相关联,以确保协程的生命周期与组件的生命周期保持一致。
使用 lifecycleScope.launch 或类似的函数启动协程时,协程会自动与组件的生命周期绑定。
如果组件(如 Activity)被销毁或进入不可见状态,lifecycleScope 会确保相关的协程被取消,从而释放资源。
在Activity的onCreate函数中,
// 在生命周期作用域内启动协程
lifecycleScope.launch {
// 协程代码,这里可以执行网络请求、数据库操作等
delay(1000L) // 模拟耗时操作
// 当组件被销毁或进入不可见状态时,这里的代码会被取消
}
由于 lifecycleScope 是与 Android 生命周期绑定的,因此它不适用于非 Android 组件或纯 Kotlin/JVM 项目。
1.4,viewModelScope
viewmodelScope 是一个协程作用域,它确保在 ViewModel 被销毁时,所有在该作用域内启动的协程都会被取消。
它提供了一个简单的方法来启动协程,而无需手动管理它们的生命周期。
在 ViewModel 中使用 viewmodelScope.launch 或类似的函数启动协程时,该协程会被绑定到 ViewModel 的生命周期。viewmodelScope.launch时默认是android主线程即Dispatcher.Main。
如果 ViewModel 被清除(例如在配置更改后),viewmodelScope 会确保所有相关的协程都被取消。
viewmodelScope 特别适用于需要在 ViewModel 中执行异步操作的情况,如从网络获取数据、从数据库加载数据等。
允许在 ViewModel 中以声明性的方式管理协程,而无需担心它们的生命周期。
使用viewModelScope需要先添加上依赖:
在libs.versions.toml文件中,
[versions]
lvmViewModelKtx = "2.6.2"
[libraries]
lvmViewModelKtx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lvmViewModelKtx" }
在build.gradle.kts(:app)文件中,
dependencies {
implementation(libs.lvmViewModelKtx)
}
在kotlin文件中,
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class HelloViewModel : ViewModel() {
private fun fetchData() {
viewModelScope.launch {
delay(1000L)
}
}
}
2,Kotlin启动新协程的函数分析
2.1,async
async 函数用于启动一个新的协程,该协程会异步地执行一个代码块,并返回一个 Deferred 对象。
Deferred 是一个表示将来某个时刻会提供结果的接口,类似于Java中的 Future。
返回类型:async 返回一个 Deferred,其中 T 是协程执行完毕后的返回类型。
等待结果:要获取 async 协程的结果,你需要在某个点上调用 await() 方法。这会挂起调用它的协程,直到 async 协程完成并返回结果。
异常处理:如果 async 协程在执行过程中抛出异常,该异常会被捕获并存储在 Deferred 对象中。当你调用 await() 时,这个异常会被重新抛出。
await方法是 Deferred 接口的一个方法。它用于等待 async 协程完成,并获取其结果。 调用 await() 会挂起当前协程,直到 Deferred 对象所代表的异步操作完成。 一旦 async 协程完成,await() 会返回其结果。
2.2,launch
1)launch 函数用于启动一个新的协程,但它不会返回表示将来结果的 Deferred 对象。它只是简单地启动协程并继续执行。
2)launch 不返回任何结果,通常用于启动那些你不需要等待其完成结果的协程。
2.3,withContext
1)withContext允许你将协程的执行切换到不同的上下文(如不同的线程或线程池)。这对于需要在特定环境中执行某些操作(例如,在IO线程中读取文件或在计算线程中执行密集计算)的协程来说非常有用。
2)上下文切换:withContext 函数允许你指定一个新的 CoroutineContext,该上下文决定了协程将在哪个线程或线程池中执行。 这是通过传递一个实现了 CoroutineDispatcher 接口的对象来完成的。
3)挂起函数:withContext 是一个挂起函数,这意味着它会挂起当前协程的执行,直到在新的上下文中执行的代码块完成。
4)返回值:withContext 返回一个与 launch 类似的 Job 对象,但它实际上表示的是在新上下文中执行的新协程的结果。
然而,与 async 不同,withContext 不会返回一个 Deferred 对象,因此你不能直接通过 await 获取结果;相反,你需要通过其他方式(如回调、共享变量或通道)来处理结果。
5)异常处理:在 withContext 中抛出的异常会传播回调用它的协程,就像在任何其他协程中一样。你可以使用 try-catch 来捕获和处理这些异常。
在main函数中,
runBlocking {
// 在主线程中打印开始消息
println("Start on ${Thread.currentThread().name}")
// 使用 withContext 切换到 IO 线程
val result = withContext(Dispatchers.IO) {
// 在IO线程中执行一些操作(例如,模拟IO操作)
delay(1000L) // 模拟耗时IO操作
"Result from IO thread"
}
// 返回到主线程并打印结果
println("Result: $result on ${Thread.currentThread().name}")
}
上面的代码中,withContext(Dispatchers.IO)将协程的执行切换到IO线程。
虽然 withContext 允许在不同的上下文中执行协程,但它并不直接返回结果给调用者;相反,它返回的结果是在新的上下文中执行的代码块的结果。
下面是一个使用 withContext 和通道(Channels)来处理结果的复杂场景示例:
runBlocking {
// 创建一个通道来接收结果
val resultsChannel = Channel<String>()
// 启动一个协程,在IO线程中执行一些操作,并将结果发送到通道
launch(Dispatchers.IO) {
// 模拟IO操作
delay(1000L)
// 将结果发送到通道
resultsChannel.send("Result from IO thread 1")
}
// 启动另一个协程,也在IO线程中执行操作,并将结果发送到通道
launch(Dispatchers.IO) {
// 模拟另一个IO操作
delay(500L)
// 将结果发送到通道
resultsChannel.send("Result from IO thread 2")
}
// 在主线程中等待并收集结果
for (i in 1..2) { // 我们知道会有两个结果,所以循环两次
val result = resultsChannel.receive() // 接收结果,这会挂起直到有结果可用
println("Received: $result on ${Thread.currentThread().name}")
}
// 关闭通道,表示不再有新结果
resultsChannel.close()
// 打印结束消息
println("All results received on ${Thread.currentThread().name}")
}
上面代码的输出结果是:
Received: Result from IO thread 2 on main
Received: Result from IO thread 1 on main
All results received on main
在上面的代码中,创建了一个 Channel 来接收从IO线程发送的结果。
通道是Kotlin协程库中一种强大的工具,它们允许你在协程之间发送和接收数据,同时处理并发性和数据同步的问题。
使用 launch(Dispatchers.IO) 启动了两个协程,每个协程都在IO线程中执行模拟的IO操作,并将结果发送到通道。
在主线程中,我们使用一个循环来接收通道中的结果。receive() 函数是挂起的,它会等待直到有结果可用。
上面的代码中,launch不可以直接修改为withContext,否则会遇到阻塞主线程。
launch(Dispatchers.IO) {
// 模拟IO操作
delay(1000L)
// 将结果发送到通道
resultsChannel.send("Result from IO thread 1")
}
上面的代码也可以修改为如下代码:
launch {
val result1 = withContext(Dispatchers.IO) {
delay(1000L)
// 将结果发送到通道
"Result from IO thread 1"
}
resultsChannel.send(result1)
}
使用共享变量传递和处理结果的示例:
runBlocking {
// 创建一个线程安全的共享变量来存储结果
val result = AtomicReference<String?>(null)
// 启动一个协程来模拟异步操作
launch {
// 模拟一个耗时的异步操作
delay(1000L)
// 操作完成后,将结果设置到共享变量中
result.set("Operation completed!")
}
// 主线程等待结果(这不是最佳实践,仅用于演示)
while (result.get() == null) {
// 这里应该有一个更优雅的等待机制,比如使用挂起函数和延迟,
// 但为了简单起见,我们使用一个忙等待循环。
delay(100L) // 添加一点延迟以避免过度占用CPU
}
// 获取并处理结果
val finalResult = result.get()!!
println("Received result: $finalResult on ${Thread.currentThread().name}")
// 注意:在实际应用中,你应该避免忙等待,而是使用更合适的同步机制,
// 比如使用协程的挂起函数、通道或Flow来等待结果。
}
上面的代码中使用while循环并不是一个最佳方式,可以采用如下方式:
使用async和await:启动一个异步操作并返回一个Deferred对象,然后在需要的时候调用await来等待结果。
使用通道(Channel):创建一个通道,将结果发送到通道中,并在接收端等待结果。
使用Flow:如果你需要处理流式的多个结果,Flow是一个更强大的选择。
3,Kotlin协程的调度器分析:
Kotlin协程还提供了丰富的调度器(Dispatcher)来控制协程在不同线程中执行,比如Dispatchers.IO,Dispatchers.Main,Dispatchers.Default,Dispatchers.Unconfined。
在Kotlin协程中,Dispatchers扮演着至关重要的角色,它们决定了协程应该运行在哪个线程或线程池上。
3.1 Dispatchers.IO
功能和作用:
Dispatchers.IO是一个专门优化用于执行IO相关操作的调度器,适用于非主线程的磁盘和网络IO操作。
它使用按需创建的线程共享池,能够卸载IO密集型阻塞操作,如文件I/O和阻塞套接字I/O,从而避免阻塞主线程。
使用场景:
适用于读写文件、使用Android的shared preferences(首选项)、调用阻塞函数等场景。
3.2 Dispatchers.Main
功能和作用:
Dispatchers.Main是一个专门用于Android主线程的调度器,它将协程调度到主线程中执行。它适用于与界面交互和执行快速工作的场景。
使用场景:
通常用于调用suspend函数、运行Android界面框架操作以及更新LiveData对象等。
3.3 Dispatchers.Default
功能和作用:
Dispatchers.Default是一个默认调度器,如果没有指定协程调度器和其他任何拦截器,则默认使用它来构建协程。
它适用于执行占用大量CPU资源的工作,是一个CPU密集型任务调度器。
它使用共享后台线程的公共池,对于消耗CPU资源的计算密集型协程来说是一个合适的选择。
使用场景:
适用于对列表排序、解析JSON、图像处理计算等CPU密集型任务。
3.4 Dispatchers.Unconfined
功能和作用:
Dispatchers.Unconfined是一个非受限调度器,它对执行协程的线程不做限制。
协程在挂起点恢复执行时会在恢复所在的线程上直接执行。然而,它通常不建议在代码中使用,因为它可能导致协程在不同线程之间频繁切换,增加线程管理的复杂性。
使用场景:
通常不建议单独使用,但在某些特定情况下,当不关心协程运行在哪个线程上时可以考虑使用。
这四个调度器都是Kotlin协程库中提供的,它们共同构成了协程调度系统的核心。通过选择合适的调度器,开发者可以控制协程的运行环境和行为。
它们之间的区别是:
调度目标:
Dispatchers.IO专注于IO密集型任务,Dispatchers.Main专注于Android主线程任务,Dispatchers.Default专注于CPU密集型任务,而Dispatchers.Unconfined则不限制协程运行的线程。
线程管理:
Dispatchers.IO和Dispatchers.Default都使用线程池来管理线程,但它们的线程池配置和用途不同。Dispatchers.Main则直接使用Android的主线程。Dispatchers.Unconfined则没有固定的线程管理策略。
使用场景:
每个调度器都有其特定的使用场景和限制条件。开发者需要根据实际需求选择合适的调度器来构建协程。