这篇是许久之前初学协程之时整理的笔记,今天偶然翻到便整理成md发出来。现在的我真的越来越难总结出这么多又臭又长的东西了。
协程
定义
官方描述:协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。
协程与线程的区别
协程是编译器级别的,线程是系统级别的
优势
协程就像轻量级的线程,线程是由系统调度的,线程的阻塞和切换开销都很大。而协程依赖于线程,协程挂起和切换的时候不需要阻塞线程,几乎是无代价的,
一个线程里面可以创建任意多个协程
使用
runBlocking
private fun test1() {
runBlocking {
Log.e(TAG, "test1: 进入协程,准备延迟")
delay(1000)
Log.e(TAG, "test1: 延迟结束")
}
Log.e(TAG, "test1: 主线程")
}
runBlocking启动的协程任务会阻断当前线程,直到该协程执行结束。当协程执行结束之后,页面才会被显示出来。
GlobalScope.launch
调用方法如下:
private fun test2() {
GlobalScope.launch{
log("进入协程")
delay(1000)
log("协程执行完成")
}
log("主线程执行代码")
}
以上代码执行结果如下:
E/MainLog: test1: 主线程执行代码
E/MainLog: test1: 进入协程
E/MainLog: test1: 协程执行完成
结论:launch方法不会阻塞主线程
Async
简单使用
private fun test3() {
GlobalScope.async {
log("进入协程async")
delay(1000)
log("协程执行完成async")
}
log("执行主线程")
}
上面代码执行结果如下:
E/MainLog: test1: 执行主线程
E/MainLog: test1: 进入协程async
E/MainLog: test1: 协程执行完成async
结论:async不会阻塞主线程
launch方法中执行多个async
代码如下:
private fun test4() {
GlobalScope.launch {
val result1 = GlobalScope.async {
delay(2000)
"result1"
}
val result2 = GlobalScope.async {
delay(1000)
"result2"
}
log("两个async执行结果:${result1.await()} :: ${result2.await()}")
}
}
上面代码中,在launch方法里面执行了两个async方法,得到两个结果,最终通过result.await()获取async执行的结果并打印。
注:await() 方法只能在一个协程内部调用,在主线程调用会报错
代码中必须两个aysnc函数都返回了结果才会调用打印日志方法
suspend兰布达lambda实现
suspend的返回值在resumeWith中可以使用result.getOrNull()
获得
withContext
withContext不会创建新的协程,可以用来切换调度器的时候使用
代码示例:
private fun test6() {
GlobalScope.launch(Dispatchers.Main) {//协程在主线程开始
val image = withContext(Dispatchers.IO) { // 切换到 IO 线程,并在执行完成后切回 UI 线程
log("withContext")
}
// doSomeThing
log("toMainThread")
}
}
上面的代码,协程会在Main主线程创建,但是中间有一部分逻辑需要在子线程中执行,使用withContext实现。withContext中的逻辑执行完成后会再次切换回主线程。
注 withContext会阻塞协程
协程的挂起suspend
使用suspend修饰的函数在协程内部被调用的时候会让协程进入挂起状态,直到函数执行完成才会结束挂起状态。
协程的取消
fun main() = runBlocking {
val job1 = launch { // ①
log(1)
delay(1000) // ②
log(2)
}
delay(100)
log(3)
job1.cancel() // ③
log(4)
}
上面代码的输出结果:1、3、4
因为delay是可以响应取消的,而job1被取消了所以不能输出2
如下代码,我们将job1中的delay加一个try/catch:那么输出会变为:
1、3、4、
cancelled. kotlinx.coroutines.JobCancellationException: Job was cancelled; job=StandaloneCoroutine{Cancelling}@e73f9ac、
2
也就是说我们调用job1.cancel的时候delay会抛出异常从而中断协程
fun main() = runBlocking {
val job1 = launch { // ①
log(1)
try {
delay(1000)
}catch (e:Exception){
log("cancelled. $e")// ②
}
log(2)
}
delay(100)
log(3)
job1.cancel() // ③
log(4)
}
kotlin中的Thread使用方法
有两种方式创建线程,方法如下:
val thread = thread {
}
val thread1 = thread(start = false) {
}
thread1.start()
默认无参的方式就会自动启动,也可以手动启动(必须设置start=false)
GlobalScope的使用注意点
Global scope 通常用于启动*协程,这些协程在整个应用程序生命周期内运行,不会被过早地被取消。程序代码通常应该使用自定义的协程作用域。直接使用 GlobalScope 的 async 或者 launch 方法是强烈不建议的
GlobalScope 创建的协程没有父协程,GlobalScope 通常也不与任何生命周期组件绑定。除非手动管理,否则很难满足我们实际开发中的需求。所以,GlobalScope 能不用就尽量不用。
比如如果在我们的activity中使用协程如果用GlobalScope实现,那么退出activity后是无法停止协程的运行的。这个情景下我们应用用MainScope实现是更好的
协程相关知识
协程的创建、start、join、取消、完成
当一个协程创建后它就处于新建(New)状态,当调用Job的start/join方法后协程就处于活跃(Active)状态,这是运行状态,协程运行出错或者调用Job的cancel方法都会将当前协程置为取消中(Cancelling)状态, 处于取消中状态的协程会等所有子协程都完成后才进入取消 (Cancelled)状态,当协程执行完成后或者调用CompletableJob(CompletableJob是Job的一个子接口)的complete方法都会让当前协程进入完成中(Completing)状态, 处于完成中状态的协程会等所有子协程都完成后才进入完成(Completed)状态。
协程调度器
协程上下文(coroutine context)包含一个协程调度器(参阅 CoroutineDispatcher),协程调度器 用于确定执行协程的目标载体,即运行于哪个线程,包含一个还是多个线程。协程调度器可以将协程的执行操作限制在特定线程上,也可以将其分派到线程池中,或者让它无限制地运行
如下代码中的参数就是一种指定调度器的方式:
GlobalScope.async(context = Dispatchers.IO){
}
如果启动协程的方法中没有指定参数,那么协程会从他外部的协程作用域继承上下文和作用域,如下代码:
GlobalScope.async{
}
四种调度器介绍Default、IO、Unconfined、Main
参考:https://blog.csdn.net/c10WTiybQ1Ye3/article/details/114956973
Default、io
Dispatchers.Default和Dispatchers.IO内部都是线程池实现,它们的含义是把协程运行在共享的线程池中
Unconfined
Dispatchers.Unconfined的含义是不给协程指定运行的线程,在第一次被挂起(suspend)之前,由启动协程的线程执行它,但被挂起后, 会由恢复协程的线程继续执行, 如果一个协程会被挂起多次, 那么每次被恢复后, 都有可能被不同线程继续执行
示例:
main
Dispatchers.Main的含义是把协程运行在平台相关的只能操作UI对象的Main线程,所以它根据不同的平台有不同的实现
job的理解
参考:http://blog.chengyunfeng.com/?p=1087
CoroutineScope.launch 函数返回的是一个 Job 对象,代表一个异步的任务。Job 具有生命周期并且可以取消。 Job 还可以有层级关系,一个Job可以包含多个子Job,当父Job被取消后,所有的子Job也会被自动取消;当子Job被取消或者出现异常后父Job也会被取消。
除了通过 CoroutineScope.launch 来创建Job对象之外,还可以通过 Job() 工厂方法来创建该对象。默认情况下,子Job的失败将会导致父Job被取消,这种默认的行为可以通过 SupervisorJob 来修改。
下面的代码演示了子协程中抛出异常父协程会被取消执行的代码
SupervisorJob
暂无
协程启动参数
启动协程需要三样东西,分别是 上下文、启动模式、协程体,协程体 就好比 Thread.run 当中的代码
协程的上下文
子协程的默认上下文:
当一个协程在另外一个协程的协程作用域中启动时,它将通过 CoroutineScope.coroutineContext 继承其上下文,新启动的协程的 Job 将成为父协程的 Job 的子 Job。当父协程被取消时,它的所有子协程也会递归地被取消
但是,当使用 GlobalScope 启动协程时,协程的 Job 没有父级。因此,它不受其启动的作用域和独立运作范围的限制
协程的启动模式
这篇文章把启动模式说的很明白配图说的很好
DEFAULT 立即执行协程体
ATOMIC 立即执行协程体,但在开始运行之前无法取消
UNDISPATCHED 立即在当前线程执行协程体,直到第一个
suspend 调用
LAZY 只有在需要的情况下运行
使用协程模式的代码:
GlobalScope.launch(start = CoroutineStart.LAZY){
println(“haha”)
}
协程体
暂无
协程作用域
作用域的一些概念说明
在协程的源代码中有一个接口 CoroutineScope用来指定协程的作用域
CoroutineContext:协程的上下文
MainScope:实现了 CoroutineScope接口 同时是通过调度器调度到了主线程的协程作用域
GlobalScope:实现了CoroutineScope接口 同时执行了一个空的上下文对象的协程作用域
coroutineContext:这通过个方法可以在一个协程中启动协程是承袭他的上下文,同时内部的job将成为外部job 的子job,当一个父协程被取消的时候,所有它的子协程也会被递归的取消。
CoroutineScope(coroutineContext:CoroutineContext):通过传递一个协程上下文实现作用域的创建
假设我们的应用程序有一个具有生命周期的对象,但该对象不是协程。例如,我们正在编写一个Android应用程序,并在Android Activity中启动各种协程,以执行异步操作来获取和更新数据、指定动画等。当 Actovoty 销毁时,必须取消所有协程以避免内存泄漏。当然,我们可以手动操作上下文和 Job 来绑定 Activity 和协程的生命周期。但是,kotlinx.coroutines 提供了一个抽象封装:CoroutineScope。你应该已经对协程作用域很熟悉了,因为所有的协程构造器都被声明为它的扩展函数
我们通过创建与 Activity 生命周期相关联的协程作用域的实例来管理协程的生命周期。CoroutineScope 的实例可以通过 CoroutineScope() 或 MainScope() 的工厂函数来构建。前者创建通用作用域,后者创建 UI 应用程序的作用域并使用 Dispatchers.Main 作为默认的调度器
例,activity作用域的MainScope:
class Activity {
private val mainScope = MainScope()
fun destroy() {
mainScope.cancel()
}
// to be continued ...}
作用域的代码示例CoroutineScope
1、activity中的协程指定协程作用域只作用于activity生命周期内
协程作用域MainScope
使用MainScope必须引入依赖:
implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8’
async和launch的异同
launch 与 async 这两个函数大同小异,都是用来在一个 CoroutineScope 内开启新的子协程的。不同点从函数名也能看出来,launch 更多是用来发起一个无需结果的耗时任务(如批量文件删除、创建),这个工作不需要返回结果。async 函数则是更进一步,用于异步执行耗时任务,并且需要返回值(如网络请求、数据库读写、文件读写),在执行完毕通过 await() 函数获取返回值
区别
1、async返回类型为Deferred, launch返回类型为job
2、async可以在协程体中自定义返回值,并且通过Deferred.await堵塞当前线程等待接收async协程返回的类型
其它协程相关的东西
withTimeout
下面这个代码,如果withTimeout内的代码执行时间超过了1300ms,那么会抛出异常
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
withTimeoutOrNull
和withTimeout一样的效果,不过超时的时候不会抛出异常
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
"Done" // 在它运行得到结果之前取消它
}
println("Result is $result")
suspendCoroutine
join方法妙用
需要调研的知识
suspend关键字(未开始)
Suspend 挂起函数
协程中更新ui方法
使用MainScope更新ui
使用withContext切换到主线程更新ui
协程用Dispatcher.Main创建协程可以更新ui