1、协程基本概念
协程就像非常轻量级的线程。
- 协程让异步逻辑同步化,杜绝回调地狱,代码逻辑非常简洁易懂
- 相对于线程切换是由操作系统进行调度的,程序员无法进行控制。
而协程的调度是由程序员在代码层面上进行控制的,程序员可以通过控制suspend函数的挂起和恢复,从而控制程序运行流程 ?
2、协程挂起与恢复
- 挂起(suspend),用于暂停执行当前协程,并保存所有局部变量
- 恢复(resume),用于让已暂停的协程从其暂停处继续执行
- 挂起函数,被suspend修饰的函数称为挂起函数;挂起函数只能在协程体内或其他挂起函数内调用
?
3、挂起与阻塞区别
- 阻塞,比如Thread.sleep(5000),当前线程执行到这行代码时,会停下来等待其执行完毕,这5秒期间它干不了任何的其他工作。
- 挂起,比如协程里的delay(5000),当前协程执行到这行代码时,会挂起,然后该干嘛就干嘛去,等到它delay完毕,就恢复,当前协程从挂起的位置继续往下执行。
findViewById<TextView>(R.id.btn_click).setOnClickListener {
Log.i("MainActivity", "run start")
GlobalScope.launch(Dispatchers.Main) {
Log.i("MainActivity", "delay start, current thread: ${Thread.currentThread().name}")
delay(5000)
Log.i("MainActivity", "delay over, current thread: ${Thread.currentThread().name}")
}
Thread.sleep(50)
Log.i("MainActivity", "run end")
}
findViewById<TextView>(R.id.btn_click).setOnClickListener {
Log.i("MainActivity", "run start")
GlobalScope.launch(Dispatchers.IO) {
Log.i("MainActivity", "delay start, current thread: ${Thread.currentThread().name}")
delay(5000)
Log.i("MainActivity", "delay over, current thread: ${Thread.currentThread().name}")
}
Thread.sleep(50)
Log.i("MainActivity", "run end")
}
从运行结果对比来看,得到的结论是,主线程里开启一个主协程,哪怕主线程sleep了,主协程里的内容也不会马上执行,这是因为都是属于主线程,就得看CPU调度了。而且使用Sleep阻塞了主线程,主线程也干不了别的事。 ?
4、调度器
- Dispatchers.Main
该调度器限制所有执行都在UI主线程,它是专门用于UI的,并且会随着平台的不同而不同。 对于JS或Native,其效果等同于Dispatchers.Default;对于JVM,它是Android的主线程、JavaFx或者Swing EDT的dispatcher之一。另外,使用该调度器,必须增加相应的组件依赖: kotlinx-coroutines-android 、kotlinx-coroutines-javafx、kotlinx-coroutines-swing - Dispatchers.Default
默认调度器,非主线程。它使用JVM的共享线程池,该调度器的最大并发度是CPU的核心数,默认为2。专为CPU密集型任务进行了优化,适用于数据排序、数据解析等 - Dispatchers.IO
IO调度器,他将阻塞的IO任务分流到一个共享的线程池中,使得不阻塞当前线程。该线程池大小为环境变量kotlinx.coroutines.io.parallelism的值,默认是64或核心数的较大者。该调度器和Dispatchers.Default共享线程,因此同样环境下创建的新协程不一定会导致线程的切换 专为磁盘和网络IO进行了优化,适用于文件读写、数据库读写、网络操作等 ?
5、启动
在非协程环境中凭空启动协程,有以下三种方式:
- runBlocking{}
启动一个新协程,并阻塞当前线程,直到其内部所有逻辑及子协程逻辑全部执行完成。不常用,仅在测试、调试中使用。
runBlocking (Dispatchers.Default) {
launch(Dispatchers.IO) {
delay(500)
Log.i("MainActivity", "Launch end")
}
Log.i("MainActivity", "runBlocking continue")
val result = async(Dispatchers.IO) {
delay(500)
Log.i("MainActivity", "Async end")
"ABC"
}
Log.i("MainActivity", result.await())
}
Log.i("MainActivity", "runBlocking end")
- GlobalScope.launch{}
在应用范围内启动一个全局的新协程,协程的生命周期与应用程序一致。这样启动的协程并不能使线程保活,就像守护线程。由于这样启动的协程存在启动协程的组件已被销毁但协程还存在的情况,极限情况下可能导致资源耗尽,因此并不推荐这样启动,尤其是在客户端这种需要频繁创建销毁组件的场景。
GlobalScope.launch(Dispatchers.Default) {
launch(Dispatchers.IO) {
delay(500)
Log.i("MainActivity", "Launch end")
}
Log.i("MainActivity", "runBlocking continue")
val result = async(Dispatchers.IO) {
delay(500)
Log.i("MainActivity", "Async end")
"ABC"
}
Log.i("MainActivity", result.await())
}
Log.i("MainActivity", "runBlocking end")
- 实现CoroutineScope接口,使用launch{}
这是在应用中最推荐使用的协程使用方式——为自己的组件实现CoroutieScope接口,在需要的地方使用launch{}方法启动协程。使得协程和该组件生命周期绑定,组件销毁时,协程一并销毁。从而实现安全可靠地协程调用。 通过该方式实现的,常见的API有 MainScope(应用在Activity中) ViewModelScope(应用在ViewModel中,绑定ViewModel生命周期) LifecycleScope(应用在带Lifecycle实现的Activity和Fragment中,绑定组件的生命周期)
class MainActivity : AppCompatActivity(), CoroutineScope {
companion object {
const val TAG = "MainActivity"
}
lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
job = Job()
findViewById<TextView>(R.id.btn_click).setOnClickListener {
launch {
Log.i(TAG, "Launch start, current thread: ${Thread.currentThread().name}")
delay(5000)
Log.i(TAG, "Launch end, current thread: ${Thread.currentThread().name}")
}
Log.i(TAG, "Button click event end, current thread: ${Thread.currentThread().name}")
}
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
在一个协程中启动另外一个协程,一般有这两种方式:
- launch{},CoroutineScope的扩展方法,创建并启动一个子协程,不阻塞当前协程,并返回新协程的Job
runBlocking {
launch(Dispatchers.IO) {
}
}
- async{},CoroutineScope的扩展方法,创建并启动一个子协程,不阻塞当前协程,返回一个Deffer,除包装了返回的结果外,其余特性与launch一致
GlobalScope.launch {
async(Dispatchers.Default) {
}
}
withContext() {} 是一个挂起函数,它不创建新的协程,这个函数作用是可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。
GlobalScope.launch(Dispatchers.Main) {
val image = withContext(Dispatchers.IO) {
getImage(url)
}
imageIv.setImageBitmap(image)
}
?
6、launch和async
- launch,返回一个Job,但不附带任何的结果
- async,返回一个Deferred,其实也是一个Job,可以使用await等待子协程执行完毕获取结果
GlobalScope.launch {
val job1 = launch(Dispatchers.IO) {
Log.i(TAG, "Job1 start")
delay(3000)
Log.i(TAG, "Job1 end")
}
val job2 = launch(Dispatchers.IO) {
Log.i(TAG, "Job2 start")
delay(2000)
Log.i(TAG, "Job2 end")
}
}
GlobalScope.launch {
val job1 = launch(Dispatchers.IO) {
Log.i(TAG, "Job1 start")
delay(3000)
Log.i(TAG, "Job1 end")
}
job1.join()
val job2 = launch(Dispatchers.IO) {
Log.i(TAG, "Job2 start")
delay(2000)
Log.i(TAG, "Job2 end")
}
job2.join()
}
GlobalScope.launch {
Log.i(TAG, "Launch start")
val job1 = async(Dispatchers.IO) {
Log.i(TAG, "Job1 start")
delay(6000)
Log.i(TAG, "Job1 end")
"Hello"
}
val job2 = async(Dispatchers.IO) {
Log.i(TAG, "Job2 start")
delay(5000)
Log.i(TAG, "Job2 end")
"World"
}
Log.i(TAG, "Launch continue")
val result1 = job1.await()
Log.i(TAG, "Launch job1 await")
val result2 = job2.await()
Log.i(TAG, "$result1 $result2")
}
?
7、启动模式
- CoroutineStart.DEFAULT
默认的启动模式,协程创建后,立即开始调度,调度器OK后就马上执行;随时可以取消,取消将其直接进入取消响应状态。
GlobalScope.launch {
Log.i(TAG, "Launch start")
val job1 = launch(Dispatchers.IO, CoroutineStart.DEFAULT) {
Log.i(TAG, "Job1 start")
}
job1.cancel()
Log.i(TAG, "Launch end")
}
- CoroutineStart.LAZY
协程创建后,不会有任何调度行为,协程体也自然不会进入执行状态,直到我们需要它执行的时候。需要它执行时,调用Job.start、Job.join、Deferred.await函数都能触发协程的调度器执行。
GlobalScope.launch(Dispatchers.Default) {
Log.i(TAG, "Launch start")
val job1 = launch(Dispatchers.IO, CoroutineStart.LAZY) {
Log.i(TAG, "Job1 start")
}
delay(500)
Log.i(TAG, "Launch continue")
job1.start()
Log.i(TAG, "Launch end")
}
- CoroutineStart.ATOMIC
协程创建后,立即开始调度,调度器OK后就马上执行,在协程开始运行前无法取消;和DEFAULT的区别在于取消的时机。
GlobalScope.launch {
Log.i(TAG, "Launch start")
val job1 = launch(Dispatchers.IO, CoroutineStart.ATOMIC) {
Log.i(TAG, "Job1 start")
}
job1.cancel()
Log.i(TAG, "Launch end")
}
- CoroutineStart.UNDISPATCHED
立即在当前线程执行协程体,直到第一个suspend挂起函数调用,才会创建协程并开启异步模式。
GlobalScope.launch(Dispatchers.Default) {
Log.i(TAG, "Launch start, current thread: ${Thread.currentThread().name}")
val job1 = launch(Dispatchers.IO, CoroutineStart.UNDISPATCHED) {
Log.i(TAG, "Job1 start, current thread: ${Thread.currentThread().name}")
delay(3000)
Log.i(TAG, "Job1 end, current thread: ${Thread.currentThread().name}")
}
Log.i(TAG, "Launch continue")
job1.join()
Log.i(TAG, "Launch end")
}
下图表示DEFAULT和ATOMIC启动模式在cancel时的差异性 ?
8、Job的生命周期
Job是协程上下文中的一部分,能够被组织成父子层次结构,并具有如下重要特性 1、父Job退出,所有子job会马上退出 2、子job抛出除CancellationException(意味着正常取消)意外的异常会导致父Job马上退出
Job的状态表如下:
状态 | isActive | isCompleted | isCancelled |
---|
新建状态 | false | false | false | 活动状态 | true | false | false | 正在完成 | true | false | false | 正在取消 | false | false | true | 已取消 | false | true | true | 已完成 | false | true | false |
Job的生命周期图如下(盗图):
|