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

  • Post author:
  • Post category:其他


作者: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





launch





async


的对比



launch





async


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

返回值



目的


  • launch

    用来启动一个新协程,这个协程不会返回任何结果(而是返回一个

    Job

    对象)。它的主要用途是执行一些并发的操作,这些操作自身就是它们的目的,我们并不关心它们的返回值。我们可以使用这个返回的

    Job

    对象来控制这个协程,比如取消它。


  • async

    用来启动一个新协程,并且这个协程可以返回一个结果。

    async

    返回一个

    Deferred

    对象,这是一个特殊的

    Job

    ,我们可以使用

    await()

    方法来获取这个协程的结果。如果这个协程还没有完成,

    await()

    方法会挂起当前协程,直到这个协程完成。






Job





Deferred


的对比



Job





Deferred


都代表一个协程的生命周期和状态。它们的主要区别在于,

Deferred

可以有一个结果。


  • Job

    表示一个协程的生命周期。我们可以调用

    Job



    cancel()

    方法来取消这个协程。

    Job

    还有一些其他的方法和属性,用来查询协程的状态,比如

    isActive



    isCompleted



    isCancelled


  • Deferred



    Job

    的一个子接口,它增加了一个

    await()

    方法,用来获取协程的结果。当你调用

    await()

    方法时,如果这个协程还没有完成,当前协程会被挂起,直到这个协程完成。因此,

    Deferred

    是一个可以有结果的

    Job





launch





async


代码演示

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.Main



Dispatchers.IO



Dispatchers.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 协程中,挂起函数只能在以下环境中被调用:

  • 其他挂起函数:一个挂起函数可以调用其他的挂起函数。这意味着你可以在一个挂起函数中调用另一个挂起函数,而不需要任何特殊的语法或关键字。
  • 协程构建器:挂起函数可以在协程构建器(如

    launch



    async

    )中被调用。这是启动一个新的协程并在其中调用挂起函数的常见方式。
  • 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 修饰的函数就是挂起函数,协程也自带一些挂起函数

协程的挂起函数有许多,比如

delay



join



await



withContext



yield



withTimeout

等等。这些函数的作用和特点如下:


  • 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
  • 工具
  • 常见问题



版权声明:本文为weixin_61845324原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。