什么是TM的kotlin协程?就是靠 恢复和挂起,像同步一样写异步代码

news2024/12/23 5:04:20

作者:J船长

一、协程协程,恢复挂起,让异步像同步

重要的说三遍

  • 协程协程,恢复挂起,让异步像同步
  • 协程协程,恢复挂起,让异步像同步
  • 协程协程,恢复挂起,让异步像同步

经常有这么一种感觉,读完一本小说,内容基本终将都是遗忘,能记住一句话很多时候都实属难得。

那么如果说起协程只能留个一个印象,那么可以是:

协程的核心是挂起(suspend)和恢复(resume),最经典的应用就是让几个异步方法能以类似同步的方式排队去执行。

爱与和平,比喻与爱

想象一下,你正在看一部非常引人入胜的电影,但是突然你想上厕所。你难道会错过电影中精彩的部分吗?当然不会。你会“暂停”电影,上完厕所再“恢复”播放。这就是协程的核心概念:挂起与恢复。

你可以把协程想象成一个大厨,他正在烹饪多道菜品。当一个菜品需要等待一段时间(比如煮沸或烘烤)时,大厨不会傻傻地等待,而是转向另一个任务。这就是协程的核心思想:挂起与恢复。

  • 协程有两个基本操作:挂起(suspend)和恢复(resume)。

  • 挂起不是停止执行,而是把控制权交还给调用者,直到我们准备好恢复执行。

  • 恢复就是我们准备好继续执行时,从上次暂停的地方恢复执行。

什么时候,需要用到协程?

协程,轻量级的线程管理工具。 什么时候用到? —— 执行异步任务管理并发操作

  • 异步、并发,用协程
  • 异步、并发,用协程
  • 异步、并发,用协程
  • 处理阻塞操作:如果你需要在主线程上执行可能会阻塞的操作,例如网络请求或数据库查询,那么你应该创建一个协程。在协程中执行这些操作可以避免阻塞主线程,保持应用的响应性。
  • 并发执行任务:如果你需要同时执行多个任务,并在它们全部完成后处理结果,你可以创建多个协程,并使用 async 函数或 withContext 函数来并发执行这些任务。
  • 实现复杂的并发逻辑:协程提供了一些高级的并发原语,如通道(Channel)和流(Flow),你可以使用它们来实现更复杂的并发逻辑。
  • 优化资源使用:协程是一种轻量级的线程,你可以创建大量的协程而不需要担心线程资源的开销。如果你需要在应用中执行大量的异步任务,那么使用协程可能会比使用线程更高效。
  • 在 Android 开发中,你还可以使用协程来配合 LiveData 或 Flow 实现数据的异步加载和更新,以及配合 Room 数据库库进行异步查询操作等。

以上这些场景都是创建协程的常见情况。使用协程可以让异步代码的写法更接近同步代码,更加直观易读,同时也让并发操作的控制更为简单。

最经典的场景

最经典的,无非就是,多个网络请求,一起执行,让这些本来都是异步的代码,像同步一样地执行。


二、 协程的依赖

安卓项目中使用协程,先添加依赖

build.gradle 引入一下

    // 协程依赖包
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'

    // 协程Android支持库
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'

三、先从最经典的场景说起

假设,有一个页面,需要用Retrofit,请求2个接口,当且仅当两个接口都请求成功时,汇总数据返回,更新UI。

其中,一个接口请求用户详情,一个接口请求文章数据,一个类用于汇总数据。

定义我们的数据类和API接口:

  • 接口方法被标记为 suspend,这意味着它们是挂起函数,可以在协程中异步执行
// 用户详细信息数据类
data class UserDetails(/* ... */)

// 用户文章数据类
data class Post(/* ... */)

// 用户详细信息和文章的数据类
data class UserDetailsAndPosts(val userDetails: UserDetails, val userPosts: List<Post>)

// API接口定义
interface ApiService {
    @GET("user/{userId}")
    suspend fun fetchUserDetails(@Path("userId") userId: String): UserDetails

    @GET("posts/{userId}")
    suspend fun fetchUserPosts(@Path("userId") userId: String): List<Post>
}

创建Retrofit实例

  • 我们使用了 CoroutineCallAdapterFactory,它允许我们在 Retrofit 中使用协程。
val retrofit = Retrofit.Builder()
    .baseUrl("https://your-api-url.com/") // API的基础URL
    .addConverterFactory(GsonConverterFactory.create()) // 使用Gson转换器
    .addCallAdapterFactory(CoroutineCallAdapterFactory()) // 使用协程适配器
    .build()

val apiService = retrofit.create(ApiService::class.java) // 创建API服务实例

ViewModel中处理异步请求

  • 使用了 viewModelScope 来启动协程,这意味着如果 ViewModel 被清除,所有的协程也会被取消,从而避免了内存泄露。
  • async 函数。async 函数会启动一个新的协程并返回一个 Deferred 对象,这个对象代表了一个未完成的协程。我们可以调用 Deferred 对象的 await 方法来等待协程完成并获取结果。
class UserViewModel(private val apiService: ApiService) : ViewModel() {
    val userDetailsAndPostsLiveData = MutableLiveData<UserDetailsAndPosts>()

    fun fetchUserDetailsAndPosts(userId: String) {
        // 在ViewModel的协程范围内启动协程
        viewModelScope.launch {
            val userDetailsDeferred = async { 
                try {
                    apiService.fetchUserDetails(userId) // 获取用户详细信息
                } catch (e: Exception) {
                    // 处理 fetchUserDetails 的异常
                    null
                } 
            }
            val userPostsDeferred = async { 
                try {
                    apiService.fetchUserPosts(userId) // 获取用户文章列表
                } catch (e: Exception) {
                    // 处理 fetchUserPosts 的异常
                    null
                } 
            }

            // 等待两个请求完成并获取结果
            val userDetails = userDetailsDeferred.await()
            val userPosts = userPostsDeferred.await()

            // 如果两个请求都成功,创建 UserDetailsAndPosts 实例并更新 LiveData
            if(userDetails != null && userPosts != null){
                val userDetailsAndPosts = UserDetailsAndPosts(userDetails, userPosts)
                userDetailsAndPostsLiveData.value = userDetailsAndPosts
            }
        }
    }
}

在Activity中使用

class UserActivity : AppCompatActivity() {
    private lateinit var viewModel: UserViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)

        // 获取ViewModel实例
        viewModel = ViewModelProviders.of(this).get(UserViewModel::class.java)

        //

上面代码,大概看个印象,可以看下下面的分析,也可以直接跳到第四大点。

上面的代码,到底讲了什么

我们对这个使用 Kotlin 协程的网络请求例子做一个分析和总结。

  1. API接口定义:在这个例子中,我们首先定义了 API 接口。使用 Retrofit 库,我们可以通过创建接口并使用注解来定义网络请求。最重要的是,我们的接口方法被标记为 suspend,这意味着它们是挂起函数,可以在协程中异步执行。

  2. 创建Retrofit实例:我们创建了 Retrofit 客户端实例,并通过调用 create 方法创建 API 接口的实例。注意我们使用了 CoroutineCallAdapterFactory,它允许我们在 Retrofit 中使用协程。

  3. 在ViewModel中处理异步请求:我们在 ViewModel 中定义了一个 LiveData 对象来保存用户详细信息和文章的数据,然后创建了一个处理异步请求的方法。在这个方法中,我们使用了 viewModelScope 来启动协程,这意味着如果 ViewModel 被清除,所有的协程也会被取消,从而避免了内存泄露。

    为了并行执行两个网络请求,我们使用 async 函数。async 函数会启动一个新的协程并返回一个 Deferred 对象,这个对象代表了一个未完成的协程。我们可以调用 Deferred 对象的 await 方法来等待协程完成并获取结果。需要注意的是,这些协程都在 viewModelScope 的范围内,因此它们的生命周期受 ViewModel 的控制。

    对于每个网络请求,我们使用 try-catch 块来处理可能的异常。如果请求成功,我们将结果保存到 LiveData 对象中;如果请求失败,我们可以根据需要处理异常。

  4. 在Activity中使用ViewModel:在 Activity 中,我们获取 ViewModel 的实例,然后观察 LiveData 的变化来更新 UI。我们还调用了 fetchUserDetailsAndPosts 方法来启动网络请求。

这个例子展示了如何使用 Kotlin 协程来处理并行的异步任务,并通过 LiveData 对象将结果返回到 UI。Kotlin 协程提供了一种简洁的方式来管理异步操作,避免了回调地狱,使代码更易于阅读和维护。此外,通过正确使用 viewModelScope,我们可以确保在 ViewModel 被清除时取消所有的协程,从而避免了内存泄露。

上面这通解释分析,你也可以略过,下面开始正式比较系统地,说一说协程


四、协程,正式开打

四、 协程离不开的4个东西

  • suspend function : 即挂起函数
  • CoroutineScope : 即协程作用域
  • CoroutineContext: 即协程上下文
  • CoroutineBuilder: 即协程构建器

巴拉巴拉一堆名词。

写个最简单的协程代码,然后圈出来看一下。

了然了然一下

最简单的协程代码

fun log(msg: Any?) = println("ThreadName: [${Thread.currentThread().name}] threadId:[${Thread.currentThread().id}] $msg")

fun main() {
    GlobalScope.launch(context = Dispatchers.IO) {
        //延时一秒
        delay(1000)
        log("launch")
    }
    //主动休眠两秒,防止 JVM 过快退出
    Thread.sleep(2000)
    log("end")
}

输出:
ThreadName: [DefaultDispatcher-worker-1] threadId:[11] launch
ThreadName: [main] threadId:[1] end

那么,谁是 挂起函数function ?作用域Scope? 上下文Context? 构建器Build?

Emmm,F4的第一次见面,结束。


五、协程的作用域 CoroutineScope

CoroutineScope作用域这玩意,必须先讲,不然如果是刚入门的同学,可能会因为没先了解这个东西,整的云里雾里。GlobalScope这个作用域,它不会阻止 JVM 结束运行,这意味着!!!!!很可能JVM都结束了,你的协程还在运行,然后问题就来了,你在代码里面写了一些输出语句,然后你死等,就等不到你的输出语句被打印出来。然后一脸懵逼,二脸懵逼,懵山懵海。尽量别用它

几个作用域

  • GlobalScope 尽量避免使用 GlobalScope
  • CoroutineScope 常用
  • MainScope
  • runBlocking
  • viewModelScope 安卓专用,配合viewModel 常用
  • lifecycleScope 安卓专用,配合lifecycle 常用

  1. GlobalScope:全局作用域,生命周期和应用一样长。虽然可以在任何地方启动协程,但在实际开发中尽量避免使用,以防止协程无法及时被取消造成的内存泄漏。
  2. CoroutineScope:一个通用的协程作用域接口,可以自定义作用域的上下文,比如你可以创建一个 CoroutineScope 的实例并指定其生命周期。
  3. MainScope:是 CoroutineScope 的一个实例,它在主线程上创建协程。通常用在 Android UI 线程的操作,需要手动进行取消操作(OnDestory)以防止内存泄漏。
  4. runBlocking:这是一个特殊的协程作用域,会阻塞当前线程并等待协程执行完毕。通常在测试和主函数中使用,开发中避免使用,因为它会阻塞线程,可能导致应用无响应。
  5. viewModelScope:这是 Android 架构组件提供的预定义作用域,专门为 ViewModel 设计,当 ViewModel 被清除时,viewModelScope 中的所有协程也会自动被取消。在 Android 开发中,这是最常使用的作用域之一。
  6. lifecycleScope:同样由 Android 架构组件提供,用于 LifecycleOwner(如 Activity 或 Fragment)。当 LifecycleOwner 的生命周期结束时,lifecycleScope 中的所有协程会自动取消。

作用域的开发场景代码

典型的应用场景

GlobalScope: 几乎不用

GlobalScope在 Android 开发中,GlobalScope 的使用场景较少,主要应用在应用程序级别的后台任务中。例如,可能需要在整个应用程序生命周期内持续监听网络连接状态的变化,这时候就可以使用 GlobalScope。但这需要谨慎处理,否则可能会导致内存泄漏。

GlobalScope.launch {
    // 这里我们在全局作用域启动一个新的协程,它会在整个应用程序生命周期内持续运行
    // 这个例子中,我们假设需要在应用程序生命周期内持续监听网络连接状态的变化
    while (isActive) {  // 在协程仍处于活动状态时持续执行
        val isConnected = checkNetworkConnection()  // 假设这是一个检查网络连接的挂起函数
        if (!isConnected) {
            notifyUser()  // 如果网络断开,通知用户
        }
        delay(5000)  // 每 5 秒检查一次网络连接
    }
}

CoroutineScope: 常用

CoroutineScope自定义作用域,用于创建一个具有特定生命周期和调度器的协程作用域,例如,可以用于处理一些长时间运行的后台任务。

val job = Job()
val dispatcher = Dispatchers.IO
val customScope = CoroutineScope(job + dispatcher)

customScope.launch {
    // 在自定义的协程作用域中启动一个新的协程。
    // 这个协程的生命周期与 `job` 相关联,当我们调用 `job.cancel()` 时,这个协程会被取消。
    // 这个协程会在 IO 调度器上运行,这适合执行 IO 密集型的任务,如网络请求或磁盘读写。
    val data = fetchDataFromNetwork()  // 假设这是一个挂起函数,用于从网络加载数据
    processData(data)  // 处理数据
}

MainScope

MainScope:MainScope 在 Android 开发中通常用于在主线程上执行一些短暂的任务,这些任务需要在用户界面上显示结果。例如,我们可能需要从网络上下载一张图片,然后在主线程上更新用户界面。不过,需要注意的是,与 viewModelScope 和 lifecycleScope 不同,MainScope 不会自动取消协程,我们需要在适当的时候手动取消协程,例如在 Activity 的 onDestroy 方法中,否则可能会导致内存泄漏

class MainActivity : AppCompatActivity() {
    private val mainScope = MainScope()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mainScope.launch {
            // 在主线程上获取数据
            val data = fetchDataFromNetwork()  // 假设这是一个挂起函数,用于从网络加载数据
            // 在主线程上更新 UI
            textView.text = data
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        // 当 Activity 销毁时,取消所有的协程以避免内存泄漏
        mainScope.cancel()
    }
}

viewModelScope 常用

viewModelScope:在 ViewModel 中运行协程,当 ViewModel 清理时,所有的协程都会被自动取消。适合用于触发数据加载等操作。

class MyViewModel : ViewModel() {
    fun fetchData() {
        viewModelScope.launch {
            // 在 ViewModel 的作用域中启动一个新的协程
            // 当 ViewModel 被清理时,这个协程会被自动取消
            val data = fetchDataFromNetwork()  // 假设这是一个挂起函数,用于从网络加载数据
            _data.value = data  // 更新 LiveData
        }
    }
}

lifecycleScope 常用

lifecycleScope:在生命周期拥有者(如 Activity 或 Fragment)中运行协程,当生命周期结束时,所有的协程都会被自动取消。适合在 UI 控制器中触发数据加载等操作。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        lifecycleScope.launch {
            // 在 Activity 的生命周期作用域中启动一个新的协程
            // 当 Activity 销毁时,这个协程会被自动取消
            val data = fetchDataFromNetwork()  // 假设这是一个挂起函数,用于从网络加载数据
            textView.text = data  // 更新 UI
        }
    }
}

runBlocking

runBlocking:在当前线程阻塞并启动新协程。在 Android 中,runBlocking 主要用于测试中,因为在真实的 Android 设备上,我们通常避免在主线程上阻塞。

@Test
fun testCoroutine() = runBlocking {
    // 在测试中,我们可以使用 `runBlocking` 在当前线程阻塞并启动新协程
    // 这样可以确保在测试方法返回之前,协程内的所有操作都已完成
    val data = fetchDataFromNetwork()  // 假设这是一个挂起函数,用于从网络加载数据
    assertEquals("expected data", data)
}

其实,对于开发初步使用

看到这里,基本也就够了。

当然,但是,接着说,也是可以的!

六、协程的构造器 CoroutineBuilder

其实吧,这个东西,有点模糊。官方只说了launch和async是CoroutineBuilder。但是对于 runBlocking 和 coroutineScope,它们同时具有作用域和构造器的特性,但是我们这里,不把他们当做构造器,只认为 launch 和 async 是构造器。

协程的两个启动方法

  • launch 不关心返回结果 Job
  • async 可以返回结果 Deferred

launchasync 的对比

launchasync 都是用来创建新协程的构造器。它们的主要区别在于返回值目的

  • launch 用来启动一个新协程,这个协程不会返回任何结果(而是返回一个 Job 对象)。它的主要用途是执行一些并发的操作,这些操作自身就是它们的目的,我们并不关心它们的返回值。我们可以使用这个返回的 Job 对象来控制这个协程,比如取消它。

  • async 用来启动一个新协程,并且这个协程可以返回一个结果。async 返回一个 Deferred 对象,这是一个特殊的 Job,我们可以使用 await() 方法来获取这个协程的结果。如果这个协程还没有完成,await() 方法会挂起当前协程,直到这个协程完成。


JobDeferred 的对比

JobDeferred 都代表一个协程的生命周期和状态。它们的主要区别在于,Deferred 可以有一个结果。

  • Job 表示一个协程的生命周期。我们可以调用 Jobcancel() 方法来取消这个协程。Job 还有一些其他的方法和属性,用来查询协程的状态,比如 isActiveisCompletedisCancelled
  • DeferredJob 的一个子接口,它增加了一个 await() 方法,用来获取协程的结果。当你调用 await() 方法时,如果这个协程还没有完成,当前协程会被挂起,直到这个协程完成。因此,Deferred 是一个可以有结果的 Job

launchasync 代码演示

import kotlinx.coroutines.*

class MainActivity : AppCompatActivity() {
    
    private val job = Job()  // 用来管理协程的生命周期
    private val scope = CoroutineScope(Dispatchers.Main + job)  // 在主线程上创建协程作用域

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ===========  以下演示  launch

        // 使用 launch 启动一个新协程,这个协程不会返回任何结果。主要用途是执行一些并发的操作
        scope.launch {
            try {
                // 从网络获取用户数据
                // 这里的 fetchUserFromNetwork 是一个 suspend 函数,也就是一个挂起函数,它可以在协程中运行
                val user = fetchUserFromNetwork()
                // 在数据库中更新用户数据
                updateUserInDatabase(user)
            } catch (e: Exception) {
                // 处理可能发生的异常,例如网络错误或数据库错误
                handleException(e)
            }
        }
        
        // ===========  以下演示  async

        // 使用 async 启动一个新协程,并且这个协程可以返回一个结果。
        // async 返回一个 Deferred 对象,这是一个特殊的 Job,我们可以使用 await() 方法来获取这个协程的结果。
        val deferred: Deferred<User> = scope.async {
            fetchUserFromNetwork()
        }
        
        // 使用 await 获取协程的结果
        // 当你调用 await() 方法时,如果这个协程还没有完成,当前协程会被挂起,直到这个协程完成。
        scope.launch {
            try {
                val user = deferred.await()
                updateUserInDatabase(user)
            } catch (e: Exception) {
                handleException(e)
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        job.cancel()  // 取消所有的协程
    }
    
    // 下面是一些模拟的函数

    // suspend 关键字表明这是一个挂起函数。挂起函数可以在协程中运行,并且可以被挂起,而不会阻塞线程。
    private suspend fun fetchUserFromNetwork(): User {
        delay(1000)  // 模拟网络请求
        return User()  // 返回一个用户对象
    }

    private suspend fun updateUserInDatabase(user: User) {
        delay(500)  // 模拟数据库操作
    }

    private fun handleException(e: Exception) {
        // 在这里处理异常
    }
}

class User {
    // 用户类
}

这备注,可以说是相当详细了吧。

关于协程的构造器,可以了


七、协程的上下文

在 Android 开发中,以下几个协程上下文是比较常用的:

Dispatchers.Main

1.Dispatchers.Main:在主线程上执行协程代码,用于处理 UI 相关的操作,例如更新界面、响应用户交互等。在 Android 中,由于 UI 操作必须在主线程上执行,因此使用 Dispatchers.Main 是非常常见的。(由于协程在主线程中执行,不应该在该上下文中进行耗时操作,以免阻塞主线程导致应用无响应。)

Dispatchers.IO

2.Dispatchers.IO:用于执行 I/O 相关的操作,如网络请求、文件读写等。它使用了一个线程池来运行协程代码,适用于执行长时间的阻塞操作。在进行网络请求、数据库操作或文件读写等 I/O 操作时,使用 Dispatchers.IO 可以避免阻塞主线程,提高应用程序的响应性。

Dispatchers.Default

2.Dispatchers.Default:用于执行计算密集型操作,如数据处理或计算。它使用了一个线程池来运行协程代码,适用于执行消耗 CPU 资源的任务。当需要进行复杂的计算或处理大量数据时,使用 Dispatchers.Default 可以将这些操作分配到后台线程,避免阻塞主线程。

除了上述常用的协程上下文,还有一些其他的上下文也可能在特定的场景中使用:

Dispatchers.Unconfined

3.Dispatchers.Unconfined:这个上下文不受任何特定线程的限制,协程会在恢复后恢复到任意线程上。它适用于一些无需特定线程限制的操作,但需要注意的是,由于协程在不同的线程之间切换,可能会导致上下文切换的开销。

CoroutineName

4.CoroutineName:这个上下文用于为协程指定一个名称,以方便调试和追踪。在复杂的协程流程中,给协程命名可以帮助我们更好地理解和跟踪协程的执行路径。

在 Android 开发中,常用的协程上下文是根据具体的需求和场景选择的。主要关注需要在主线程上执行的 UI 操作、I/O 操作和计算密集型操作,并根据需要选择适当的协程上下文。

也就是说,日常使用基本都是Dispatchers.MainDispatchers.IODispatchers.Default


Dispatchers.Main示例

  • 适用场景:用于在主线程上执行协程任务,例如更新用户界面。
  • 由于协程在主线程中执行,不应该在该上下文中进行耗时操作,以免阻塞主线程导致应用无响应。
import kotlinx.coroutines.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 启动一个协程,在主线程中更新UI
        CoroutineScope(Dispatchers.Main).launch {
            val result = fetchUserData()
            updateUI(result)
        }
    }

    private suspend fun fetchUserData(): String {
        // 模拟耗时操作
        delay(1000)
        return "User Data"
    }

    private fun updateUI(data: String) {
        textView.text = data
    }
}

Dispatchers.IO:

  • 适用场景:用于执行涉及 I/O 操作的协程任务,例如网络请求、文件读写等。
  • 这个上下文适用于执行会阻塞线程的 I/O 操作,如网络请求、文件读写等。它会自动根据需要创建足够的线程池来处理任务。(不要在主线程中执行)
import kotlinx.coroutines.*
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.URL
import kotlin.coroutines.CoroutineContext

class MainActivity : AppCompatActivity(), CoroutineScope {
    private lateinit var job: Job

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        job = Job()

        launch {
            val result = fetchDataFromNetwork()
            updateUI(result)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    }

    // 在 IO 线程中执行网络请求并读取数据
    private suspend fun fetchDataFromNetwork(): String = withContext(Dispatchers.IO) {
        val url = URL("https://api.example.com/data")
        val connection = url.openConnection()
        val inputStream = connection.getInputStream()
        val reader = BufferedReader(InputStreamReader(inputStream))
        val stringBuilder = StringBuilder()
        var line: String?
        while (reader.readLine().also { line = it } != null) {
            stringBuilder.append(line)
        }
        return@withContext stringBuilder.toString()
    }

    // 更新 UI 的函数
    private fun updateUI(data: String) {
        val textView = findViewById<TextView>(R.id.textView)
        textView.text = data
    }
}

Dispatchers.Default:

  • 适用场景:用于执行 CPU 密集型的计算任务,例如排序、解析数据等。
  • 这个上下文适用于执行消耗 CPU 资源较多的计算任务,如排序、解析数据等。它会根据需要创建足够的线程池来处理任务。
class MainActivity : AppCompatActivity() {
    private val scope = CoroutineScope(Dispatchers.Main)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        scope.launch(Dispatchers.Default) {
            // 在 Default 线程池上执行协程中的计算操作
            val result = performCalculation()
            withContext(Dispatchers.Main) {
                // 切换回主线程更新界面
                updateUI(result)
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        scope.cancel() // 取消协程
    }

    private suspend fun performCalculation(): Int {
        delay(1000) // 模拟计算操作
        return 42
    }

    private fun updateUI(result: Int) {
        // 在 UI 线程上更新界面
        textView.text = "Result: $result"
    }
}

八、协程 恢复和挂起

挂起(suspend)和恢复(resume)是协程的两个核心操作。

简单来说:

  • 挂起suspend 就是暂停当前协程的执行
  • 恢复resume 就是重新开始协程的执行。

我们可以把协程想象成一部正在播放的电影。当你需要离开一会(比如接个电话),你可能会按下"暂停"按钮,这就是挂起协程。当你准备好继续观看时,你会按下"播放"按钮,这就是恢复协程。

  • 挂起函数(Suspend function)是Kotlin协程中的一种特殊函数,它可以在不阻塞当前线程的情况下暂停协程的执行。Kotlin标准库中的许多函数都是挂起函数,例如 delay()yield()

  • suspend修饰之后函数就可以被 称之为 挂起函数

谁可以调用挂起函数

在 Kotlin 协程中,挂起函数只能在以下环境中被调用:

  • 其他挂起函数:一个挂起函数可以调用其他的挂起函数。这意味着你可以在一个挂起函数中调用另一个挂起函数,而不需要任何特殊的语法或关键字。
  • 协程构建器:挂起函数可以在协程构建器(如 launchasync)中被调用。这是启动一个新的协程并在其中调用挂起函数的常见方式。
  • CoroutineScope:在一个 CoroutineScope 的扩展函数中,你可以直接调用挂起函数。

挂起suspend 函数的小例子

看代码吧

我们前面说过,delay()本身也是一个挂起函数

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        println("协程开始执行 - ${Thread.currentThread().name}")
        delay(1000L)  // 这是一个挂起函数
        println("协程恢复执行 - ${Thread.currentThread().name}")
    }
    println("主线程继续执行 - ${Thread.currentThread().name}")
}

在上面的代码中,launch函数启动了一个新的协程,并在内部调用了delay(1000L)。这个delay函数就是一个挂起函数,它会暂停协程的执行1秒钟(但不会阻塞线程),然后恢复协程的执行。

这段代码的输出将是:

协程开始执行 - main 
主线程继续执行 - main 
协程恢复执行 - main

我们看到,输出顺序是 开始 —— 继续 ——恢复。

注意观察,虽然我们的协程在等待时,主线程并没有被阻塞,而是继续执行了下一行代码。协程的暂停并不会阻塞线程。

再来一个例子

class MainActivity : AppCompatActivity() {
    private val coroutineScope = CoroutineScope(Dispatchers.Main)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        coroutineScope.launch {
            Log.d("协程示例", "onCreate: 协程开始")

            try {
                val result = withContext(Dispatchers.IO) {
                    performNetworkRequest()
                }

                Log.d("协程示例", "onCreate: 收到网络响应: $result")
                // 在这里处理网络响应
            } catch (e: Exception) {
                Log.e("协程示例", "onCreate: 网络请求失败", e)
                // 处理网络请求失败的情况
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        coroutineScope.cancel()
    }

    suspend fun performNetworkRequest(): String {
        Log.d("协程示例", "performNetworkRequest: 开始网络请求...")
        delay(2000) // 模拟网络请求的延迟
        return "网络响应"
    }

}

打印输出:

onCreate: 协程开始
performNetworkRequest: 开始网络请求...
onCreate: 收到网络响应: 网络响应

常见的挂起函数

被 suspend 修饰的函数就是挂起函数,协程也自带一些挂起函数

协程的挂起函数有许多,比如 delayjoinawaitwithContextyieldwithTimeout 等等。这些函数的作用和特点如下:

  • delay:延迟一段时间再继续执行协程。
  • join:等待一个协程的完成。
  • await:等待一个协程的结果。
  • withContext:在特定的上下文(比如在不同的线程或调度器)中执行代码。
  • yield:让出CPU的控制权,让其他协程有机会执行。
  • withTimeout:设置协程的超时时间。

以下是一个例子,结合 Android 的 Activity,演示如何使用这些函数:

import kotlinx.coroutines.*

class MyActivity : AppCompatActivity() {
    
    // 创建一个协程的上下文
    private val scope = CoroutineScope(Dispatchers.Main)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 创建并启动一个协程
        scope.launch {
            val result = getDataFromNetwork() // 这个函数是一个挂起函数
            // 这里可以更新UI
            textView.text = result
        }
    }

    // 从网络获取数据的挂起函数
    private suspend fun getDataFromNetwork(): String {
        // withContext 可以改变协程的上下文
        return withContext(Dispatchers.IO) {
            // 假设我们在这里使用了一个网络请求库,比如Retrofit
            // 它的函数通常也是挂起函数,可以在协程中直接调用
            // 这里我们简单模拟一下网络请求
            delay(1000) // 模拟网络延迟
            "获取到的数据"
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        // 当Activity销毁时,取消所有的协程以防止内存泄漏
        scope.cancel()
    }
}

单独演示下常用的 withContext


class MainActivity : AppCompatActivity() {

    // 创建一个 CoroutineScope,使用 Dispatchers.Main 作为默认的调度器
    val scope = CoroutineScope(Dispatchers.Main)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 使用 launch 开启一个协程
        scope.launch {
            Log.d("MainActivity", "开始协程,当前线程: ${Thread.currentThread().name}") // 打印日志,显示当前线程名

            // 使用 withContext 切换到 IO 调度器
            withContext(Dispatchers.IO) {

                downloadDataFromNetwork() // 模拟耗时操作,比如网络请求

                // 使用 withContext 再次切换到 Main 调度器
                withContext(Dispatchers.Main) {
                    Log.d("MainActivity", "切换回 Main 调度器,当前线程: ${Thread.currentThread().name}") // 打印日志,显示当前线程名

                    // 在主线程更新 UI
                    // textView.text = "更新后的文本"
                }

                // 使用 withContext 切换到 Default 调度器
                withContext(Dispatchers.Default) {
                    // 耗时计算
                    Log.d("MainActivity", "切换到 Default 调度器,当前线程: ${Thread.currentThread().name}") // 打印日志,显示当前线程名
                }
            }
        }
    }

    // 模拟从网络下载数据的挂起函数
    private suspend fun downloadDataFromNetwork(): String {
        // withContext 可以改变协程的上下文
        return withContext(Dispatchers.IO) {

            Log.d("MainActivity", "切换到 IO 调度器,当前线程: ${Thread.currentThread().name}") // 打印日志,显示当前线程名

            Log.d("MainActivity", "开始下载数据,当前线程: ${Thread.currentThread().name}") // 打印日志,显示当前线程名

            delay(1000) // 模拟网络下载延迟

            Log.d("MainActivity", "数据下载完成,当前线程: ${Thread.currentThread().name}") // 打印日志,显示当前线程名

            "下载的数据" // 返回下载的数据
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        // 当 Activity 销毁时,取消所有协程
        scope.cancel()
    }
}

输出

开始协程,当前线程: main
切换到 IO 调度器,当前线程: DefaultDispatcher-worker-1
开始下载数据,当前线程: DefaultDispatcher-worker-1
数据下载完成,当前线程: DefaultDispatcher-worker-1
切换回 Main 调度器,当前线程: main
切换到 Default 调度器,当前线程: DefaultDispatcher-worker-1

emm,基本写到这里的话,日常开发也够用了。不写了太长了这个文章

番外篇 协程到底是不是在单线程里并发?还是不多线程?

协程可能在单线程,也可能是多线程。具体要取决于Dispatcher

协程本身并不直接与线程对应,它是一种轻量级的线程,可以在单个线程内并发执行,也可以在多个线程之间切换。一个协程可以在一个线程中启动,然后在另一个线程中暂停,然后再在另一个线程中恢复。

具体来说,协程的运行取决于它的调度器(Dispatcher)。调度器决定了协程在哪个线程或哪些线程上执行。比如 Dispatchers.Main 是主线程调度器,Dispatchers.IO 是专门用于磁盘和网络 IO 读写的调度器,Dispatchers.Default 是用于 CPU 密集型任务的调度器,Dispatchers.Unconfined 是一个特殊的调度器,它没有特定的线程,会在当前线程立即执行协程,如果协程中有挂起点,它会在恢复时继续在其他合适的线程执行。

总的来说,协程既可以在单线程中并发运行,也可以在多线程中运行,取决于你的需求和使用的调度器。但是,与线程相比,协程更轻量级,可以创建成千上万个而不会对性能造成大的影响。

来例子啊

在单线程内并发运行多个协程


import kotlinx.coroutines.*

fun main() = runBlocking {
    // 在主线程启动两个协程
    launch {
        println("协程1在 ${Thread.currentThread().name} 线程运行")
        delay(1000L)
        println("协程1在 ${Thread.currentThread().name} 线程结束")
    }
    launch {
        println("协程2在 ${Thread.currentThread().name} 线程运行")
        delay(1000L)
        println("协程2在 ${Thread.currentThread().name} 线程结束")
    }
}

这个例子中,我们在主线程中启动了两个协程,它们会并发执行,但都在同一个线程内。


在多线程之间切换协程:

import kotlinx.coroutines.*

fun main() = runBlocking {
    // 在主线程启动协程
    launch(Dispatchers.Main) {
        println("协程在 ${Thread.currentThread().name} 线程开始")

        // 使用 withContext 切换到 IO 线程
        withContext(Dispatchers.IO) {
            println("协程在 ${Thread.currentThread().name} 线程运行")
        }

        println("协程在 ${Thread.currentThread().name} 线程结束")
    }
}

这个例子中,我们首先在主线程中启动了一个协程,然后使用 withContext(Dispatchers.IO) 切换到了 IO 线程,然后又回到了主线程。我们可以在日志中看到协程在不同的线程之间切换。


Kotlin+协程已经让我们在网络编程中体会到了它的过人之处。为了能让大家能够顺畅的使用Kotlin语言,这为大家准备了《Kotlin开发学习指南》的核心笔记:https://qr18.cn/CdjtAF

Android Kotlin 学习指南

概述

  • Kotlin 用于服务器端
  • Kotlin 用于 Android
  • Kotlin 用于 JavaScript
  • 新特性

Kotlin 基础包括操作、编码习惯与习惯用法

  • 基本语法
  • 习惯用法
  • 编码规范

基础部分:https://qr18.cn/CdjtAF

  • 基本类型
  • 控制流
  • 返回与跳转

类与对象

  • 类与继承
  • 属性与字段
  • 接口
  • 可见性修饰符
  • 扩展
  • 数据类
  • 密封类
  • 泛型
  • 嵌套类
  • 枚举类
  • 对象
  • ……

函数与 Lambda 表达式:https://qr18.cn/CdjtAF

  • 函数
  • Lambda 表达式
  • 内联函数
  • 协程

  • 其他
  • 核心库
  • Java 互操作
  • JavaScript
  • 工具
  • 常见问题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/653135.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Apikit 自学日记:导入第三方产品 API 数据

除了手动创建API文档&#xff0c;系统也提供了一键导入 Swagger、Postman、RAP、YAPI 等产品数据的功能。方便从其他平台进行迁移。 产品支持度导入文件的后缀名Eolinker API 研发管理完全支持.jsonPostman V2.1支持导入API基础信息&#xff0c;超过10级分组的API数据将不会被导…

接口测试——接口测试文档

在执行接口测试前&#xff0c;测试人员肯定会先拿到开发给予的接口文档。测试人员可以根据这个文 档编写接口测试用例。所以&#xff0c;我们要先了解接口文档的主要构成及含义。 以购买开心产品项目接口文档为例&#xff0c;解析一下接口文档的组成。 完整的接口文档有公共信…

【VMD-DBO-LSTM】变分模态分解-蜣螂优化算法-长短时记忆神经网络研究(Python代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

乐鑫 Thread 边界路由器解决方案

乐鑫科技 Thread 边界路由器 (Thread Border Router) 解决方案正式获得了由 Thread Group 颁发的 Thread Certified Component 证书&#xff0c;符合最新的 Thread 1.3 标准&#xff0c;并支持 Matter 应用场景。 本文将深入探讨该解决方案的技术细节和优势&#xff0c;以及如…

springboot+vue在线考试系统(java项目源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的在线考试系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 &#x1f495;&#x1f495;作者&#xff1a;风歌&a…

Unity编辑器扩展-第五集-撤回操作/禁止操作/加快捷键

第四集链接&#xff1a;Unity编辑器扩展-第四集-获取物体的方法_菌菌巧乐兹的博客-CSDN博客 一、本节目标效果展示 1.加入撤回操作 众所周知“撤回是ctrlz”,但如果你前几集仔细练习了&#xff0c;你会发现一个让你头痛不已的事情&#xff0c;用代码改的东西没法撤回。现在就…

JMeter如何和Charles进行接口测试

目录 一、什么是接口测试&#xff1f; 二、接口测试的好处 三、接口测试如何开展 四、如何使用JMeter 总结&#xff1a; 什么是接口测试&#xff0c;接口测试到底如何开展&#xff0c;我相信任何一个软件测试人员都会有这样的疑问&#xff0c; 这里我以接口测试自动化平台…

7.Sentinel控制台 配置 nacos持久化

一、Sentinel 持久化机制 推送模式说明优点缺点原始模式API将规则推送至客户端并直接更新到内存中,扩展写数据源简单、无任何依赖不保证一致性;规则保存在内存中,重启不建议用于生产环境PULL模式客户端主动向某个规则管理中心定期轮询拉取规则,<br />这个规则中心可以…

Discovery Holdings 探索没有复杂性的多云机会

开发新的产品和服务&#xff0c;让客户 “惊叹”&#xff0c;是业界一个一直以来的的承诺&#xff0c;尤其是在一个快节奏的数字世界。提供金融服务的公司面临着敏捷的金融科技公司的威胁和不断变化的客户期望的持续压力。为了保持领先地位&#xff0c;他们需要提供价值、自助服…

【送书福利-第十二期】前沿技术、人工智能、集成电路科学与芯片技术、新一代信息与通信技术、网络空间安全技术,四大热点领域热书!

大家好&#xff0c;我是洲洲&#xff0c;欢迎关注&#xff0c;一个爱听周杰伦的程序员。关注公众号【程序员洲洲】即可获得10G学习资料、面试笔记、大厂独家学习体系路线等…还可以加入技术交流群欢迎大家在CSDN后台私信我&#xff01; 本文目录 一、前言二、书籍介绍1、《Web3…

【夜深人静学数据结构与算法 | 第三篇】 二叉树

目录 前言&#xff1a; 二叉树&#xff1a; 二叉树的种类&#xff1a; 二叉树的存储方式&#xff1a; 1. 链式存储 2. 数组存储 二叉树的遍历方式 深度优先遍历 广度优先遍历 总结&#xff1a; 前言&#xff1a; 本文将会详细的介绍各种常见的树以及相对应的概念&…

iTOP-RK3588 | Linux系统和应用开发手册

iTOP -RK3588开发板使用手册上新&#xff0c;后续资料会不断更新&#xff0c;不断完善&#xff0c;帮助用户快速入门&#xff0c;大大提升研发速度。 迅为RK3588开发板已经对Linux系统完成适配&#xff0c;同时新增了对应的文档教程辅助大家参考学习——《itop-3588开发板Linu…

戴着很舒服的蓝牙耳机推荐,佩戴舒适度高的蓝牙耳机分享

​无论是日常通勤还是运动健身&#xff0c;相信大多数朋友都喜欢一边在朝夕的奔波路上&#xff0c;或是休息锻炼的闲散时间中&#xff0c;一边听着好听的音乐营造一份轻松愉悦的氛围感来放松自己的心情。随着蓝牙耳机的增长和功能增加&#xff0c;很多用户找不到一款好用且适合…

从乐观到悲观:MySQL中不同类型的锁全面解析

大家好&#xff0c;我是你们的小米&#xff01;今天我要和大家分享关于MySQL的乐观锁和悲观锁&#xff0c;以及不同类型的锁。锁作为数据库中重要的概念之一&#xff0c;对于保证数据的一致性和并发性至关重要。现在就让我们一起来深入了解吧&#xff01; 为什么需要锁 在并发…

Android Compose UI实战练手----Google Bloom欢迎页

目录 1.概述2.效果图展示2.1 亮色主题效果&#xff1a;2.2 深色主题效果 3.项目结构解析3.1 颜色配置Color.kt3.2 形状配置Shape.kt3.3 主题配置Theme.kt3.4 字体配置 Type.kt 4.沉浸式状态栏适配5.UI界面分解及实现5.1 欢迎页背景内容5.2 欢迎页内容组件实现 5.3 欢迎页内容的…

自动化测试成熟度模型

目录 前言&#xff1a; 重新认识自动化测试 新手落地自动化测试 自动化测试成熟度模型 初级阶段-测试半自动化 中级阶段-回归测试自动化 高级阶段-大范围自动化测试 成熟阶段-自动化测试流水线 前言&#xff1a; 随着软件行业的快速发展&#xff0c;软件质量已经成为各…

chatgpt赋能python:Python如何绘制坐标轴

Python如何绘制坐标轴 在数据可视化中&#xff0c;坐标轴是一种非常常见的图形元素&#xff0c;其作用是让人们更好地理解和分析数据。在Python编程中&#xff0c;我们可以使用各种数据可视化库如Matplotlib和Seaborn来绘制坐标轴。本文将介绍如何使用Matplotlib库来绘制坐标轴…

17. 数据结构之图

前言 前面介绍了队列&#xff0c;栈等线性数据结构&#xff0c;二叉树&#xff0c;AVL树等非线性数据结构&#xff0c;本节&#xff0c;我们介绍一种新的非线性数据结构&#xff1a;图。图这种结构有很广泛的应用&#xff0c;比如社交网络&#xff0c;电子地图&#xff0c;多对…

【JVM篇】Java内存区域与OOM

目录 1、概述 2、运行时数据区域 3、程序计数器 4、Java虚拟机栈 5、本地方法栈 6、Java堆 7、方法区 8、运行时常量池 9、直接内存 1、概述 内存是非常重要的系统资源&#xff0c;是硬盘和 CPU 的中间仓库及桥梁&#xff0c;承载着操作系统和应用程序的实时运行。JVM…

Windows Server 2025预览版安装配置

一、安装篇 1.2.目前最新版是 25379版本&#xff0c;需要注册微软账户&#xff0c;加入先期预览计划才可以下载。 下载完镜像后&#xff0c;制作U盘启动盘&#xff0c;安装。 注意&#xff1a;无论是rufs制作启动盘&#xff0c;还是在安装的时候&#xff0c;都要使用UEFI模式…