IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Kotlin 协程总结,跳槽字节跳动 -> 正文阅读

[移动开发]Kotlin 协程总结,跳槽字节跳动


CoroutineScope(Dispatchers.IO).launch {

    ...

}



也可以使用 Dispatchers.Main 参数切换到主线程:


CoroutineScope(Dispatchers.Main).launch{

    ...

}



所以在「协程是什么」一节中讲到的异步请求的例子完整写出来是这样的:


CoroutineScope(Dispatchers.Main).launch{   // 在主线程开启协程

    val user = api.getUser() // IO 线程执行网络请求

    nameTv.text = user.name  // 主线程更新 UI

}



而通过 Java 实现以上逻辑,我们通常需要这样写:


api.getUser(new Callback<User>() {

    @Override

    public void success(User user) {

        runOnUiThread(new Runnable() {

            @Override

            public void run() {

                nameTv.setText(user.name);

            }

        })

    }

    

    @Override

    public void failure(Exception e) {

        ...

    }

});



这种回调式的写法,打破了代码的顺序结构和完整性,读起来相当难受。

[](

)3.协程具体怎么用

[](

)a.添加依赖


    //依赖协程核心库

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5'

    //依赖当前平台所对应的平台库

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5'



Kotlin 协程是以官方扩展库的形式进行支持的。我们所使用的「核心库」和 「平台库」的版本应该保持一致。

  • 核心库中包含的代码主要是协程的公共 API 部分。有了这一层公共代码,才使得协程在各个平台上的接口得到统一。

  • 平台库中包含的代码主要是协程框架在具体平台的具体实现方式。因为多线程在各个平台的实现方式是有所差异- 的。

[](

)b.开始使用

协程最简单的使用方法,其实在前面章节就已经看到了。我们可以通过一个 launch函数实现线程切换的功能:


CoroutineScope(Dispatchers.IO).launch {

    ...

}



这个 launch 函数,它具体的含义是:我要创建一个新的协程,并在指定的线程上运行它。这个被创建、被运行的所谓「协程」是谁?就是你传给 launch 的那些代码,这一段连续代码叫做一个「协程」。

所以,什么时候用协程?当你需要切线程或者指定线程的时候。你要在后台执行任务?切!


launch(Dispatchers.IO) {

    val image = getImage(imageId)

}



然后需要在前台更新界面?再切!


CoroutineScope(Dispatchers.IO).launch{

    val image = getImage(imageId)

    launch(Dispatch.Main) {

        avatarIv.setImageBitmap(image)

    }

}



好像有点不对劲?这不还是有嵌套嘛。

如果只是使用 launch 函数,协程并不能比线程做更多的事。不过协程中却有一个很实用的函数:withContext 。这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。那么可以将上面的代码写成这样:


CoroutineScope(Dispatchers.Main).launch {      // 👈 在 UI 线程开始

    val image = withContext(Dispatchers.IO) {  // 👈 切换到 IO 线程,并在执行完成后切回 UI 线程

        getImage(imageId)                      // 👈 将会运行在 IO 线程

    }

    avatarIv.setImageBitmap(image)             // 👈 回到 UI 线程更新 UI

} 



这种写法看上去好像和刚才那种区别不大,但如果你需要频繁地进行线程切换,这种写法的优势就会体现出来。可以参考下面的对比:


// 第一种写法

CoroutineScope(Dispatchers.IO).launch {

    ...

    launch(Dispachers.Main){

        ...

        launch(Dispachers.IO) {

            ...

            launch(Dispacher.Main) {

                ...

            }

        }

    }

}

?

// 通过第二种写法来实现相同的逻辑

CoroutineScope(Dispatchers.Main).launch{

    ...

    withContext(Dispachers.IO) {

        ...

    }

    ...

    withContext(Dispachers.IO) {

        ...

    }

    ...

}



由于可以"自动切回来",消除了并发代码在协作时的嵌套。由于消除了嵌套关系,我们甚至可以把 withContext 放进一个单独的函数里面:


launch(Dispachers.Main) {              // 👈 在 UI 线程开始

    val image = getImage(imageId)

    avatarIv.setImageBitmap(image)     // 👈 执行结束后,自动切换回 UI 线程

}

//                               👇

fun getImage(imageId: Int) = withContext(Dispatchers.IO) {

    ...

}



这就是之前说的「用同步的方式写异步的代码」了。

不过如果只是这样写,编译器是会报错的:


fun getImage(imageId: Int) = withContext(Dispatchers.IO) {

    // IDE 报错 Suspend function'withContext' should be called only from a coroutine or another suspend funcion

}



意思是说,withContext 是一个 suspend函数,它需要在协程或者是另一个 suspend函数中调用。

[](

)c.suspend

suspend是 Kotlin 协程最核心的关键字,几乎所有介绍 Kotlin 协程的文章和演讲都会提到它。它的中文意思是「暂停」或者「可挂起」。如果你去看一些技术博客或官方文档的时候,大概可以了解到:「代码执行到 suspend 函数的时候会『挂起』,并且这个『挂起』是非阻塞式的,它不会阻塞你当前的线程。」

上面报错的代码,其实只需要在前面加一个suspend 就能够编译通过:


suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) {

    ...

}



suspend具体是什么,下面介绍。

[](

)4.小结

协程是一种编程思想,写法简洁,可以通过Dispatchers调度器切换到指定的线程。所有代码都是运行在线程中的,协程也是。

二、挂起是什么

==================================================================

[](

)1.「挂起」的本质

协程中「挂起」的对象到底是什么?挂起线程,还是挂起函数?都不对,我们挂起的对象是协程。

launch ,async 或者其他函数创建的协程,在执行到某一个 suspend 函数的时候,这个协程会被「suspend」,也就是被挂起。

那此时又是从哪里挂起?从当前线程挂起。换句话说,就是这个协程从正在执行它的线程上脱离。

注意,不是这个协程停下来了!是脱离,当前线程不再管这个协程要去做什么了。

suspend 是有暂停的意思,但我们在协程中应该理解为:当线程执行到协程的 suspend 函数的时候,暂时不继续执行协程代码了。

我们先让时间静止,然后兵分两路,分别看看这两个互相脱离的线程和协程接下来将会发生什么事情:

[](

)线程:

前面我们提到,挂起会让协程从正在执行它的线程上脱离,具体到代码其实是:

协程的代码块中,线程执行到了 suspend 函数这里的时候,就暂时不再执行剩余的协程代码,跳出协程的代码块。

那线程接下来会做什么呢?

如果它是一个后台线程:

  • 要么无事可做,被系统回收

  • 要么继续执行别的后台任务

跟 Java 线程池里的线程在工作结束之后是完全一样的:回收或者再利用。

如果这个线程它是 Android 的主线程,那它接下来就会继续回去工作:也就是一秒钟 60 次的界面刷新任务。

[](

)协程:

线程的代码在到达suspend函数的时候被掐断,接下来协程会从这个suspend 函数开始继续往下执行,不过是在指定的线程

谁指定的?是 suspend 函数指定的,比如函数内部的 withContext 传入的Dispatchers.IO所指定的 IO 线程。

Dispatchers 调度器,它可以将协程限制在一个特定的线程执行,或者将它分派到一个线程池,或者让它不受限制地运行。

常用的Dispatchers ,有以下三种:

  • Dispatchers.Main:Android 中的主线程

  • Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求

  • Dispatchers.Default:适合 CPU 密集型的任务,比如计算

回到我们的协程,它从suspend函数开始脱离启动它的线程,继续执行在 Dispatchers所指定的 IO 线程。

紧接着在 suspend 函数执行完成之后,协程为我们做的最爽的事就来了:会自动帮我们把线程再切回来

这个「切回来」是什么意思?

我们的协程原本是运行在主线程的,当代码遇到suspend函数的时候,发生线程切换,根据 Dispatchers 切换到了 IO 线程;

当这个函数执行完毕后,线程又切了回来,「切回来」也就是协程会帮我再 post一个 Runnable,让我剩下的代码继续回到主线程去执行。

ok,我们从线程和协程的两个角度都分析完成后,终于可以对协程的「挂起」suspend 做一个解释:

协程在执行到有 suspend 标记的函数的时候,会被 suspend 也就是被挂起,而所谓的被挂起,就是切个线程;

不过区别在于,挂起函数在执行完成之后,协程会重新切回它原先的线程。

再简单来讲,在 Kotlin 中所谓的挂起,就是一个稍后会被自动切回来的线程调度操作。

这个「切回来」的动作,在 Kotlin 里叫做 resume,恢复。

通过刚才的分析我们知道:挂起之后是需要恢复。

而恢复这个功能是协程的,如果你不在协程里面调用,恢复这个功能没法实现,所以也就回答了这个问题:为什么挂起函数必须在协程或者另一个挂起函数里被调用。

因为 一个挂起函数要么在协程里被调用,要么在另一个挂起函数里被调用,所以不管是直接还是间接地,总是会在一个协程里被调用的。

当然,要求 suspend 函数只能在协程里或者另一个 suspend 函数里被调用,还是为了要让协程能够在 suspend 函数切换线程之后再切回来。

[](

)2.怎么就「挂起」了?

我们了解到了什么是「挂起」后,再接着看看这个「挂起」是怎么做到的。

先随便写一个自定义的 suspend函数:


suspend fun suspendingPrint() {

  println("Thread: ${Thread.currentThread().name}")

}



I/System.out: Thread: main



输出的结果还是在主线程。

为什么没切换线程?因为它不知道往哪切,需要我们告诉它。

比如例子中 suspendingGetImage 函数代码:


//                                               👇

suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {

  ...

}



我们可以发现不同之处其实在于 withContext函数。

其实通过 withContext源码可以知道,它本身就是一个挂起函数,它接收一个 Dispatcher参数,依赖这个 Dispatcher 参数的指示,你的协程被挂起,然后切到别的线程。

所以这个 suspend,其实并不是起到把任何把协程挂起,或者说切换线程的作用。

真正挂起协程这件事,是 Kotlin 的协程框架帮我们做的。

所以我们想要自己写一个挂起函数,仅仅只加上 suspend 关键字是不行的,还需要函数内部直接或间接地调用到 Kotlin 协程框架自带的 suspend 函数才行。

[](

)3.suspend 的意义?

这个 suspend 关键字,既然它并不是真正实现挂起,那它的作用是什么?

它其实是一个提醒。

函数的创建者对函数的使用者的提醒:我是一个耗时函数,我被我的创建者用挂起的方式放在后台运行,所以请在协程里调用我。

为什么 suspend关键字并没有实际去操作挂起,但 Kotlin 却把它提供出来?

因为它本来就不是用来操作挂起的。

挂起的操作 —— 也就是切线程,依赖的是挂起函数里面的实际代码,而不是这个关键字。

所以这个关键字,只是一个提醒。

还记得刚才我们尝试自定义挂起函数的方法吗?


// 👇 redundant suspend modifier

suspend fun suspendingPrint() {

  println("Thread: ${Thread.currentThread().name}")

}



如果你创建一个 suspend 函数但它内部不包含真正的挂起逻辑,编译器会给你一个提醒:redundant suspend modifier,告诉你这个suspend是多余的。

因为你这个函数实质上并没有发生挂起,那你这个 suspend 关键字只有一个效果:就是限制这个函数只能在协程里被调用,如果在非协程的代码中调用,就会编译不通过。

所以,创建一个 suspend 函数,为了让它包含真正挂起的逻辑,要在它内部直接或间接调用 Kotlin 自带的 suspend 函数,你的这个 suspend才是有意义的。

[](

)4.怎么自定义 suspend 函数?

在了解了 suspend 关键字的来龙去脉之后,我们就可以进入下一个话题了:怎么自定义 suspend 函数。

这个「怎么自定义」其实分为两个问题:

  • 什么时候需要自定义 suspend 函数?

  • 具体该怎么写呢?

[](

)a.什么时候需要自定义 suspend 函数

如果你的某个函数比较耗时,也就是要等的操作,那就把它写成 suspend 函数。这就是原则。

耗时操作一般分为两类:I/O 操作和 CPU 计算工作。比如文件的读写、网络交互、图片的模糊处理,都是耗时的,通通可以把它们写进 suspend 函数里。

另外这个「耗时」还有一种特殊情况,就是这件事本身做起来并不慢,但它需要等待,比如 5 秒钟之后再做这个操作。这种也是 suspend 函数的应用场景。

[](

)a.具体该怎么写

给函数加上 suspend 关键字,然后在 withContext 把函数的内容包住就可以了。

提到用 withContext是因为它在挂起函数里功能最简单直接:把线程自动切走和切回。

当然并不是只有 withContext 这一个函数来辅助我们实现自定义的 suspend 函数,比如还有一个挂起函数叫 delay,它的作用是等待一段时间后再继续往下执行代码。

使用它就可以实现刚才提到的等待类型的耗时操作:


suspend fun suspendUntilDone() {

  while (!done) {

    delay(5)

  }

}



这些东西,在我们初步使用协程的时候不用立马接触,可以先把协程最基本的方法和概念理清楚。

[](

)5.小结

这一部分主要讲的是挂起,这个挂起对象就是协程,是一段代码块。

挂起其实就是切换线程,而协程还可以再帮我们切回来。

三、挂起的非阻塞式是怎么回事

=========================================================================

[](

)1.什么是「非阻塞式挂起」

非阻塞式是相对阻塞式而言的。

线程阻塞很好理解,现实中的例子就是交通堵塞,它的核心有 3 点:

  • 前面有障碍物,你过不去(线程卡了)
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-09-04 17:39:38  更:2021-09-04 17:39:41 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 13:16:48-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码