Kotlin协程基础入门
Kotlin 协程是什么
在 Kotlin 编程中,协程是一种轻量级的异步编程模型。它允许我们以一种更简洁、更直观的方式处理异步任务,就像编写同步代码一样,却能在后台线程执行任务,避免阻塞主线程,提高程序的响应性和性能。
从本质上来说,协程是一种基于线程的更细粒度的执行单元。传统的线程对于系统资源的消耗较大,而协程在相同的资源下可以创建更多的执行单元。它通过挂起和恢复机制来实现异步操作,当一个协程遇到挂起函数(比如等待网络请求返回、读取文件等需要耗时的操作)时,它会暂停自身的执行,释放线程资源给其他任务使用,当挂起的条件满足(比如网络请求有了响应、文件读取完成),协程再从暂停的地方恢复执行。
为什么要使用 Kotlin 协程
- 简化异步代码:在没有协程之前,处理异步任务通常需要使用回调函数。多层回调嵌套会导致代码可读性变差,形成所谓的 “回调地狱”。而协程允许我们以顺序执行的方式编写异步代码,让代码结构更清晰。 例如,假设我们有一个需要依次进行网络请求和数据库操作的任务,使用回调的方式可能如下:
networkRequest { response ->
databaseOperation(response) { result ->
// 处理最终结果
}
}
而使用协程可以写成:
val response = networkRequest()
val result = databaseOperation(response)
// 处理最终结果
- 提高代码的响应性:在 Android 开发等场景中,主线程负责处理用户界面的更新等操作。如果在主线程执行耗时任务,会导致界面卡顿。协程可以将耗时任务放在后台线程执行,主线程继续处理其他用户交互,提高应用的响应性。
- 更好的资源管理:协程是轻量级的,创建和销毁的开销较小。相比传统线程,在需要大量异步任务并发执行的场景下,协程能更有效地利用系统资源。
协程的基本构建块
- CoroutineScope(协程作用域)
- 定义:CoroutineScope 是协程的执行上下文,它决定了协程在何处执行以及何时结束。每个协程都必须在一个 CoroutineScope 中启动。
- 作用:它提供了对协程生命周期的控制。当 CoroutineScope 被取消时,所有在该作用域内启动的协程都会被取消。例如,在 Android 中,我们可以将协程绑定到 Activity 或 Fragment 的生命周期上,当 Activity 或 Fragment 销毁时,相关的协程也会被取消,避免内存泄漏。
- 示例:
import kotlinx.coroutines.*
// 创建一个 CoroutineScope
val scope = CoroutineScope(Job() + Dispatchers.Default)
fun main() {
scope.launch {
// 协程代码
println("Coroutine is running")
}
runBlocking {
delay(1000)
}
}
在这个例子中,scope
是一个自定义的 CoroutineScope,通过 Job()
和 Dispatchers.Default
创建。Job()
用于管理协程的生命周期,Dispatchers.Default
表示协程将在默认的后台线程池中执行。scope.launch
启动了一个新的协程。
- launch
- 定义:
launch
是 CoroutineScope 的一个扩展函数,用于启动一个新的协程。它返回一个 Job 对象,通过这个 Job 对象可以对协程进行控制,比如取消协程。 - 作用:启动一个异步任务,该任务在 CoroutineScope 定义的上下文中执行。
- 示例:
- 定义:
import kotlinx.coroutines.*
fun main() {
val scope = CoroutineScope(Job() + Dispatchers.Default)
val job = scope.launch {
for (i in 1..5) {
println("Count: $i")
delay(1000)
}
}
runBlocking {
delay(3000)
job.cancel()
}
}
这里 scope.launch
启动了一个协程,协程会每隔 1 秒打印一次 Count
,runBlocking
函数暂停主线程,3 秒后通过 job.cancel()
取消协程。
- async
- 定义:
async
也是 CoroutineScope 的扩展函数,它和launch
类似,用于启动一个异步任务,但async
返回一个 Deferred 对象。Deferred 是一个可延迟获取结果的对象,类似于 Java 中的 Future。 - 作用:当我们需要异步执行一个任务并获取其返回结果时,就可以使用
async
。 - 示例:
- 定义:
import kotlinx.coroutines.*
fun main() = runBlocking {
val scope = CoroutineScope(Job() + Dispatchers.Default)
val deferred = scope.async {
// 模拟耗时操作
delay(2000)
42
}
println("Waiting for result")
val result = deferred.await()
println("Result is: $result")
}
在这个例子中,scope.async
启动了一个异步任务,任务延迟 2 秒后返回 42。deferred.await()
会暂停当前协程,直到 async
启动的协程执行完毕并返回结果。
- runBlocking
- 定义:
runBlocking
是一个顶层函数,它创建一个新的 CoroutineScope,并在其中启动一个协程,同时阻塞当前线程,直到内部的协程执行完毕。 - 作用:通常用于测试或者在一些需要将异步操作转换为同步操作的场景。比如在
main
函数中,因为main
函数默认是同步执行的,我们可以使用runBlocking
来运行协程代码。 - 示例:
- 定义:
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
for (i in 1..3) {
println("Inner Coroutine: $i")
delay(1000)
}
}
for (i in 1..3) {
println("Outer: $i")
delay(1500)
}
}
在这个例子中,runBlocking
内部启动了一个协程,同时外部也有一个循环。runBlocking
会阻塞 main
函数所在的线程,直到内部协程和外部循环都执行完毕。
协程调度器(Dispatchers)
- Dispatchers.Default
- 定义:
Dispatchers.Default
表示使用默认的后台线程池来执行协程。这个线程池适用于 CPU 密集型任务,比如复杂的计算。 - 作用:将协程分配到一个后台线程池中执行,避免阻塞主线程。它的线程数量会根据系统的 CPU 核心数动态调整,以充分利用系统资源。
- 示例:
- 定义:
import kotlinx.coroutines.*
fun main() = runBlocking {
launch(Dispatchers.Default) {
// 模拟 CPU 密集型任务
var sum = 0
for (i in 1..1000000) {
sum += i
}
println("Sum is: $sum")
}
}
这里 launch(Dispatchers.Default)
将协程分配到默认的后台线程池执行复杂的计算任务。
- Dispatchers.IO
- 定义:
Dispatchers.IO
用于执行 I/O 相关的任务,比如文件读写、网络请求等。它使用一个专门的线程池来处理这些 I/O 操作。 - 作用:I/O 操作通常是耗时的且不占用大量 CPU 资源,
Dispatchers.IO
线程池可以有效地管理这些任务,提高 I/O 操作的效率。 - 示例:
- 定义:
import kotlinx.coroutines.*
import java.io.File
fun main() = runBlocking {
launch(Dispatchers.IO) {
val file = File("test.txt")
file.writeText("Hello, Kotlin Coroutines!")
val content = file.readText()
println("File content: $content")
}
}
在这个例子中,文件的读写操作在 Dispatchers.IO
线程池中执行,避免阻塞主线程。
- Dispatchers.Main
- 定义:在 Android 开发中,
Dispatchers.Main
用于在主线程(UI 线程)上执行协程。只有在主线程才能更新 UI 元素。 - 作用:当我们需要在异步任务完成后更新 UI 时,就可以使用
Dispatchers.Main
将协程切换到主线程执行。 - 示例:
- 定义:在 Android 开发中,
// 假设这是一个 Android 中的 Activity
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView = findViewById<TextView>(R.id.textView)
CoroutineScope(Job() + Dispatchers.IO).launch {
// 模拟网络请求
delay(2000)
withContext(Dispatchers.Main) {
textView.text = "Network request completed"
}
}
}
}
在这个 Android 示例中,先在 Dispatchers.IO
中模拟网络请求,完成后通过 withContext(Dispatchers.Main)
切换到主线程更新 TextView 的内容。
挂起函数
- 定义:挂起函数是 Kotlin 协程中一种特殊的函数,它可以暂停协程的执行,并且只有在满足一定条件时才会恢复执行。挂起函数必须在协程或者其他挂起函数内部调用。
- 作用:挂起函数通常用于处理异步操作,比如网络请求、文件读取等。通过挂起函数,我们可以以同步的方式编写异步代码,提高代码的可读性。
- 示例:
import kotlinx.coroutines.*
suspend fun fetchData(): String {
delay(2000)
return "Data fetched"
}
fun main() = runBlocking {
val data = fetchData()
println(data)
}
在这个例子中,fetchData
是一个挂起函数,它内部使用 delay
模拟了一个耗时操作。runBlocking
内部可以直接调用挂起函数 fetchData
,代码看起来就像同步执行一样。
协程的生命周期
- 创建:当使用
launch
或async
启动一个协程时,协程就进入了创建状态。此时协程还没有开始执行。 - 就绪:协程创建后,会等待调度器将其分配到一个合适的线程上执行,这个阶段就是就绪状态。
- 运行:当协程被调度到一个线程上并开始执行其代码时,就进入了运行状态。
- 挂起:如果协程执行到一个挂起函数,它会暂停执行,进入挂起状态。此时线程资源会被释放,用于执行其他任务。
- 恢复:当挂起函数的条件满足(比如网络请求返回、文件读取完成),协程会从挂起的地方恢复执行,重新进入运行状态。
- 完成:当协程的代码执行完毕或者因为异常终止时,协程就进入了完成状态。
例如:
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
println("Coroutine started")
try {
delay(2000)
println("Coroutine resumed after delay")
} catch (e: Exception) {
println("Coroutine cancelled: ${e.message}")
} finally {
println("Coroutine finished")
}
}
delay(1000)
job.cancel()
}
在这个例子中,协程启动后打印 Coroutine started
,然后进入挂起状态(delay
是挂起函数),1 秒后主线程取消协程,协程捕获到取消异常,打印 Coroutine cancelled
并执行 finally
块中的代码。
协程的并发和并行
- 并发:并发是指在同一时间段内,多个任务交替执行。在协程中,多个协程可以在同一个线程上交替执行,通过挂起和恢复机制实现。例如,我们有两个协程 A 和 B,A 执行到一个挂起函数时,线程资源被释放,B 可以在这个线程上开始执行,当 A 的挂起条件满足时,A 又可以恢复执行。
- 示例:
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
for (i in 1..3) {
println("Coroutine 1: $i")
delay(1000)
}
}
launch {
for (i in 1..3) {
println("Coroutine 2: $i")
delay(1500)
}
}
}
在这个例子中,两个协程在同一个线程上交替执行,根据 delay
的时间不同,输出会交替出现。
- 并行:并行是指在同一时刻,多个任务同时执行。在协程中,如果我们将不同的协程分配到不同的线程上执行,就可以实现并行。例如,使用
Dispatchers.Default
创建多个协程,这些协程可能会被分配到不同的线程池中线程上并行执行(具体取决于系统资源和调度器的策略)。- 示例:
import kotlinx.coroutines.*
fun main() = runBlocking {
val job1 = GlobalScope.launch(Dispatchers.Default) {
for (i in 1..3) {
println("Job 1 on thread: ${Thread.currentThread().name}")
delay(1000)
}
}
val job2 = GlobalScope.launch(Dispatchers.Default) {
for (i in 1..3) {
println("Job 2 on thread: ${Thread.currentThread().name}")
delay(1500)
}
}
job1.join()
job2.join()
}
在这个例子中,两个协程 job1
和 job2
都使用 Dispatchers.Default
,它们可能会在不同的线程上并行执行,通过打印线程名可以观察到。
协程的异常处理
- try - catch 块:和普通的 Kotlin 代码类似,我们可以在协程内部使用
try - catch
块来捕获异常。- 示例:
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
try {
throw RuntimeException("An error occurred")
} catch (e: RuntimeException) {
println("Caught exception: ${e.message}")
}
}
}
在这个例子中,协程内部抛出一个 RuntimeException
,通过 try - catch
块捕获并打印异常信息。
- CoroutineExceptionHandler:我们还可以通过
CoroutineExceptionHandler
来统一处理协程中的异常。它是一个实现了CoroutineExceptionHandler
接口的对象,可以传递给CoroutineScope
。- 示例:
import kotlinx.coroutines.*
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("Caught by CoroutineExceptionHandler: ${exception.message}")
}
fun main() = runBlocking {
val scope = CoroutineScope(Job() + Dispatchers.Default + exceptionHandler)
scope.launch {
throw RuntimeException("Another error")
}
}
在这个例子中,CoroutineExceptionHandler
捕获到协程中抛出的 RuntimeException
并打印异常信息。
实战案例:使用 Kotlin 协程进行网络请求和数据处理
假设我们有一个简单的应用场景,需要从网络获取用户数据,然后对数据进行一些处理并显示。我们可以使用 Kotlin 协程结合 Retrofit(一个流行的 Android 网络请求库)来实现。
- 添加依赖:在
build.gradle
文件中添加 Retrofit 和 Kotlin 协程相关依赖。
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
- 定义 Retrofit 接口:
import retrofit2.Response
import retrofit2.http.GET
interface UserApi {
@GET("users")
suspend fun getUsers(): Response<List<User>>
}
这里定义了一个 UserApi
接口,getUsers
方法是一个挂起函数,用于获取用户列表。
- 创建 Retrofit 实例:
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitInstance {
private const val BASE_URL = "https://example.com/api/"
private val okHttpClient = OkHttpClient.Builder()
.build()
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
val api: UserApi = retrofit.create(UserApi::class.java)
}
- 使用协程进行网络请求和数据处理:
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView = findViewById<TextView>(R.id.textView)
CoroutineScope(Job() + Dispatchers.IO).launch {
try {
val response = RetrofitInstance.api.getUsers()
if (response.isSuccessful) {
val users = response.body()
val userCount = users?.size?: 0
withContext(Dispatchers.Main) {
textView.text = "Number of users: $userCount"
}
} else {
withContext(Dispatchers.Main) {
textView.text = "Network request failed"
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
textView.text = "An error occurred: ${e.message}"
}
}
}
}
}
在这个 Android 示例中,首先在 Dispatchers.IO
中执行网络请求,成功后切换到 Dispatchers.Main
更新 UI。如果请求失败或发生异常,同样切换到主线程提示用户。
通过以上内容,我们对 Kotlin 协程的基础知识有了较为全面的了解,从基本概念到实际应用,Kotlin 协程为异步编程提供了强大而简洁的方式。无论是在 Android 开发还是其他 Kotlin 应用场景中,熟练掌握协程都能大大提高我们的编程效率和代码质量。