这篇博客不讲解协程原理,本着快速学习,快速理解,快速使用方式来讲解协程.

kotlin协程是什么? 它其实是类似android的Handler或者java的RxJava. 本质就是为了处理各个线程上的工作协调. 在实际的Android开发最经常的情况就是需要让子线程耗时处理的数据结果发布到主线程上的UI. 所以在需求的本质上就是一个线程+观察者模式的组合. 而观察者模式归根结底就是接口. 所以不管是Handler还是RxJava的实现都需要写大量的模版代码,而kotlin的协程更加简化.

参考 http://www.kotlincn.net/docs/reference/coroutines/coroutines-guide.html

依赖

    //kotlin协程
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")

简单demo快速了解GlobalScope.launch

这里使用GlobalScope.launch一个新协程,GlobalScope是提供快速构建协程的方式. 协程处理用GlobalScope构建外还可以选择其他方式构建,下面会一一讲解到.

请注意,此代码是在Android平台下运行的,所以我不需要让主线程等待了.

    fun demo() {
        GlobalScope.launch {
            delay(1000)//非堵塞线程的延迟一秒钟
            Log.e(TAG, "当前线程id = " + Thread.currentThread().id)
        }
        Log.e(TAG, "主线程id = " + mainLooper.thread.id)
    }

结果

2021-10-15 15:23:38.687 21612-21612/com.example.myapplication E/ytzn: 主线程id = 2
2021-10-15 15:23:39.690 21612-21758/com.example.myapplication E/ytzn: 当前线程id = 3011

指定协程的线程类型

    /**
     * 线程类型
     */
    fun threadType() {
        GlobalScope.launch(Dispatchers.Default) {//默认线程
            Log.e(TAG, "Default当前线程id = " + Thread.currentThread().id)
        }
        GlobalScope.launch(Dispatchers.IO) {//IO线程
            Log.e(TAG, "IO当前线程id = " + Thread.currentThread().id)
        }
        GlobalScope.launch(Dispatchers.Main) {//主线程
            Log.e(TAG, "Main当前线程id = " + Thread.currentThread().id)
        }
        GlobalScope.launch(Dispatchers.Unconfined) {//不限于任何特定线程,就是创建的地方是什么线程,那么内部就是就是什么线程
            Log.e(TAG, "Unconfined当前线程id = " + Thread.currentThread().id)
        }
    }

结果:

2021-10-23 17:59:51.986 29732-29768/com.example.myapplication E/ytzn: Default当前线程id = 951
2021-10-23 17:59:51.987 29732-29769/com.example.myapplication E/ytzn: IO当前线程id = 952
2021-10-23 17:59:51.992 29732-29732/com.example.myapplication E/ytzn: Unconfined当前线程id = 2
2021-10-23 17:59:51.997 29732-29732/com.example.myapplication E/ytzn: Main当前线程id = 2

协程的启动模式

一共有四种

        CoroutineStart.DEFAULT  //默认,会立即启动协程
        CoroutineStart.LAZY     //被动,需要调用start方法才能执行
        CoroutineStart.ATOMIC   //原子性,立即调度执行,并且开始执行前无法被取消,直到执行完毕或者遇到第一个挂起点suspend
        CoroutineStart.UNDISPATCHED //立即在当前线程执行协程体内容

DEFAULT

    fun default() {
        GlobalScope.launch(start = CoroutineStart.DEFAULT) {
            Log.e("ytzn", "执行协程1")
        }

        GlobalScope.launch {
            Log.e("ytzn", "执行协程2")
        }
        //上面2个协程是一样的,在launch不传入启动模式时,默认DEFAULT模式
    }

LAZY

    fun lazy() {
        val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
            Log.e("ytzn", "执行协程")
        }
        job.start() //需要调用start方法才能启动
        job.cancel() //也可以取消
    }

GlobalScope.async 带返回值协程

async方法其实与launch方法是一样的,唯一区别是async可以返回值.如果需要async返回值则必须在协程内部

代码:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        btn1.setOnClickListener {
            GlobalScope.launch {
                val random = async {
                    return@async (1..10).random()
                }.await() //请注意别忘记了await方法,否则下面的log打印不会等待此返回值
                Log.e("ytzn", "random = $random")
            }
        }
    }

结果:

2021-11-06 17:27:20.125 10056-10125/com.example.myapplication E/ytzn: random = 10

异常抛出

    /**
     * 异常处理
     */
    suspend fun demo() {
        GlobalScope.launch(Dispatchers.Default) {
            throw RuntimeException()
        }
        val job = GlobalScope.async(Dispatchers.Default) {
            throw RuntimeException()
        }
        try {
            job.await()
        } catch (e: RuntimeException) {
        }
    }

协程的取消

在等待状态下的取消

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        btn1.setOnClickListener {
            GlobalScope.launch {
                val job = launch {
                    Log.e("ytzn","A")
                    delay(1000)
                    Log.e("ytzn","B")
                    delay(1000)
                    Log.e("ytzn","C 因为被取消所以不会被打印")
                }
                delay(1500)
                Log.e("ytzn","取消协程")
                job.cancel()
            }
        }
    }

结果:

2021-11-09 21:21:56.459 8982-9270/com.example.myapplication E/ytzn: A
2021-11-09 21:21:57.469 8982-9269/com.example.myapplication E/ytzn: B
2021-11-09 21:21:57.968 8982-9269/com.example.myapplication E/ytzn: 取消协程

理解join()与cancelAndJoin()方法

join意思是阻塞等待协程结束。也就是说cancel执行后会马上返回,执行后续的代码,但是这个时候协程不一定结束。再调用join方法,这里表示阻塞等待协程结束。确保协程结束之后才执行后续的代码。我们也可以调用job.cancelAndJoin().

这里有两段代码可以验证join的功能

1.首先是不使用join方法的代码

        btn1.setOnClickListener {
            GlobalScope.launch {
                val job = launch {
                    Log.e("ytzn", "time 1 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 2 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 3 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 4 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 5 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 6 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 7 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 8 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 9 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 10 = ${System.currentTimeMillis()}")
                }
                Log.e("ytzn", "A")
                job.cancel()
                Log.e("ytzn", "B")
                Log.e("ytzn", "C")
            }
        }

在不调用join方法,可以看下面的结果,在打印日志 "C" 后依然执行了协程中的代码. 请注意,不调用join方法不是说必定会让 "C" 比 协程里的日志同步执行, 而是会有出现这种同步执行概率.

结果:

2021-11-24 17:34:56.862 30507-30557/com.example.myapplication E/ytzn: A
2021-11-24 17:34:56.862 30507-30558/com.example.myapplication E/ytzn: time 1 = 1637746496862
2021-11-24 17:34:56.862 30507-30558/com.example.myapplication E/ytzn: time 2 = 1637746496862
2021-11-24 17:34:56.862 30507-30558/com.example.myapplication E/ytzn: time 3 = 1637746496862
2021-11-24 17:34:56.862 30507-30558/com.example.myapplication E/ytzn: time 4 = 1637746496862
2021-11-24 17:34:56.862 30507-30558/com.example.myapplication E/ytzn: time 5 = 1637746496862
2021-11-24 17:34:56.862 30507-30558/com.example.myapplication E/ytzn: time 6 = 1637746496862
2021-11-24 17:34:56.862 30507-30557/com.example.myapplication E/ytzn: B
2021-11-24 17:34:56.862 30507-30558/com.example.myapplication E/ytzn: time 7 = 1637746496862
2021-11-24 17:34:56.862 30507-30557/com.example.myapplication E/ytzn: C
2021-11-24 17:34:56.862 30507-30558/com.example.myapplication E/ytzn: time 8 = 1637746496862
2021-11-24 17:34:56.863 30507-30558/com.example.myapplication E/ytzn: time 9 = 1637746496863
2021-11-24 17:34:56.863 30507-30558/com.example.myapplication E/ytzn: time 10 = 1637746496863

2.使用join方法的代码

  btn1.setOnClickListener {
            GlobalScope.launch {
                val job = launch {
                    Log.e("ytzn", "time 1 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 2 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 3 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 4 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 5 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 6 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 7 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 8 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 9 = ${System.currentTimeMillis()}")
                    Log.e("ytzn", "time 10 = ${System.currentTimeMillis()}")
                }
                Log.e("ytzn", "A")
                job.cancel()
                Log.e("ytzn", "B")
                job.join()
                Log.e("ytzn", "C")
            }
        }

 

可以看到打印C的日志,一定是在最末尾的, 因为join的原因,需要让协程中的工作先完成在执行后续代码.

结果:

2021-11-24 17:44:41.510 32450-32497/com.example.myapplication E/ytzn: A
2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 1 = 1637747081511
2021-11-24 17:44:41.511 32450-32497/com.example.myapplication E/ytzn: B
2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 2 = 1637747081511
2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 3 = 1637747081511
2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 4 = 1637747081511
2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 5 = 1637747081511
2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 6 = 1637747081511
2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 7 = 1637747081511
2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 8 = 1637747081511
2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 9 = 1637747081511
2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 10 = 1637747081511
2021-11-24 17:44:41.514 32450-32496/com.example.myapplication E/ytzn: C

 使用isActive让循环中的协程取消

        btn1.setOnClickListener {
            GlobalScope.launch {
                val job = launch {
                    var i = 0
                    while (isActive){ //如果这里的isActive 替代成 true 那么这段循环将不会被cancelAndJoin停止
                        i++
                        Log.e("ytzn","i = $i")
                    }
                }
                job.cancelAndJoin()
            }
        }

 使用yield方法让循环中的协程取消

            GlobalScope.launch {
                val job = launch {
                    var i = 0
                    while (true){
                        yield()
                        i++
                        Log.e("ytzn","i = $i")
                    }
                }
                job.cancelAndJoin()
            }

使用finally在取消协程的时候释放资源

          GlobalScope.launch {
                val job = launch {
                    try {
                        var i = 0
                        while (isActive) {
                            i++
                            Log.e("ytzn", "i = $i")
                        }
                    } finally {
                        Log.e("ytzn", "释放资源")
                    }
                }
                job.cancelAndJoin()
            }

结果:

2021-11-24 19:43:26.205 25980-26027/com.example.myapplication E/ytzn: i = 1
2021-11-24 19:43:26.205 25980-26027/com.example.myapplication E/ytzn: i = 2
2021-11-24 19:43:26.205 25980-26027/com.example.myapplication E/ytzn: i = 3
2021-11-24 19:43:26.205 25980-26027/com.example.myapplication E/ytzn: 释放

suspend关键字

suspend关键字在协程开发中非常重要,在协程里是无法调用常规函数方法的会出现报错,所以,在协程里调用的函数方法需要增加suspend关键字.

suspend意思挂起,意思是在协程里调用此方法,将会优先执行此suspend函数方法的内部代码,将外部协程线程挂起.处理完成后在执行外部协程.下面的代码将验证这种说法:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        btn1.setOnClickListener {
            GlobalScope.launch {
                launch {
                    delay(500)
                    Log.e("ytzn","第二个执行");
                }
                Log.e("ytzn","首先执行,验证不会被launch挂起")
                Log.e("ytzn","第三个执行 获取随机值= " + getRandomValue())
                Log.e("ytzn","最后执行,验证会被suspend函数方法挂起")
            }
        }
    }

    suspend fun getRandomValue(): Int {
        delay(1000)//请注意这里是等待了1秒
        return (1..10).random()
    }

请注意,在getRandomValue函数方法里是写了delay(1000) 等待一秒的,并且delay方法并不堵塞线程.但是外部的最后执行的log,依然是最后执行的.这就验证了suspend方法会挂起外部协程 .

结果:

2021-11-06 16:27:32.881 28729-28789/com.example.myapplication E/ytzn: 首先执行,验证不会被launch挂起
2021-11-06 16:27:33.386 28729-28807/com.example.myapplication E/ytzn: 第二个执行
2021-11-06 16:27:33.889 28729-28790/com.example.myapplication E/ytzn: 第三个执行 获取随机值= 2
2021-11-06 16:27:33.889 28729-28790/com.example.myapplication E/ytzn: 最后执行,验证会被suspend函数方法挂起

协程作用域coroutineScope

除了使用GlobalScope构建协程外,还可以使用 coroutineScope 构建器声明自己的作用域。而coroutineScope其实是GlobalScope的父类,GlobalScope只是封装了一些构建的快捷方便的函数

通过传入SupervisorJob上下文创建coroutineScope

用这种方法创建协程作用域的意义有2个:

1.可以创建一个目标线程的全局变量作用域,减少在创建目标线程的模版代码

2.方便取消全部作用域协程, 统一使用SupervisorJob调用释放协程,尽可能的减少因为大量使用协程却忘记释放而导致内存泄露

下面的代码里显示了这种用法,

class MainActivity3 : AppCompatActivity() {
    private val job = SupervisorJob()
    private val mMainScope = CoroutineScope(Dispatchers.Main + job)
    private val mIOScope = CoroutineScope(Dispatchers.IO + job)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main3)
        mMainScope.launch {
            while (isActive) {
                delay(100)
                Log.e("ytzn", "Main Time = ${System.currentTimeMillis()}")
            }
        }
        mIOScope.launch {
            while (isActive) {
                delay(100)
                Log.e("ytzn", "IO Time = ${System.currentTimeMillis()}")
            }
        }
        MainScope()
        button.setOnClickListener {
            job.cancel() //退出之前,取消job上下文控制的所有协程作用域
            finish()
        }
    }
}

如果你只需要一个主线程线程的协程作用域可以使用下面的代码快速创建

    private val mMainScope = MainScope()

源码跟手动创建一样

@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

在协程内部构建coroutineScope的特性

    fun demo1() {
        GlobalScope.launch { // 在后台启动一个新的协程并继续
            Log.e(TAG,"首先执行")
            coroutineScope { // 创建一个协程作用域
                launch {
                    delay(500)
                    Log.e(TAG,"第三个执行")
                }
                Log.e(TAG,"第二个执行")
            }
            Log.e(TAG,"第四个执行,需要等待coroutineScope协程作用域结束后执行")
        }
    }

结果:

2021-10-20 14:17:26.837 7519-7553/com.example.myapplication E/ytzn: 首先执行
2021-10-20 14:17:26.838 7519-7553/com.example.myapplication E/ytzn: 第二个执行
2021-10-20 14:17:27.340 7519-7554/com.example.myapplication E/ytzn: 第三个执行
2021-10-20 14:17:27.341 7519-7554/com.example.myapplication E/ytzn: 第四个执行,需要等待coroutineScope协程作用域结束后执行

堵塞线程式协程runBlocking

runBlocking在Android开发下不太重要,但是在其他开发环境下相当重要,它是堵塞主线程使其存活的重要方法.

调用了 runBlocking 的主线程会一直阻塞直到 runBlocking 内部的协程执行完毕。此方法android开发下一般是不会使用的,因为Android开发最不能做的事情就是让主线程堵塞.但是在下面的代码里是堵塞了Android的主线程,在实际项目里请勿操作

    fun demo1() {
        GlobalScope.launch { // 在后台启动一个新的协程并继续
            delay(1000L)
            Log.e(TAG, "Hello World!")
        }
        val dateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
        Log.e(TAG, "Time1 = " + dateFormat.format(System.currentTimeMillis())) //主线程中的代码会立即执行
        runBlocking {     // 但是这个表达式阻塞了主线程
            delay(2000L)  // ……堵塞2秒
            Log.e(TAG, "主线程id = " + mainLooper.thread.id)
            Log.e(TAG, "当前线程id = " + Thread.currentThread().id)
            Log.e(TAG, "Time2 = " + dateFormat.format(System.currentTimeMillis()))
        }
        Log.e(TAG, "Time3 = " + dateFormat.format(System.currentTimeMillis()))
    }

结果:

2021-10-18 21:03:37.265 27646-27646/com.example.myapplication E/ytzn: Time1 = 21:03:37
2021-10-18 21:03:38.265 27646-27715/com.example.myapplication E/ytzn: Hello World!
2021-10-18 21:03:39.271 27646-27646/com.example.myapplication E/ytzn: 主线程id = 2
2021-10-18 21:03:39.272 27646-27646/com.example.myapplication E/ytzn: 当前线程id = 2
2021-10-18 21:03:39.274 27646-27646/com.example.myapplication E/ytzn: Time2 = 21:03:39
2021-10-18 21:03:39.277 27646-27646/com.example.myapplication E/ytzn: Time3 = 21:03:39

withContext

kotlin 中 GlobalScope 类提供了几个创建协程的构造函数,在上面的讲解中已经提到了3个launch,async,runBlocking,下面会说下withContext与他们的区别

  • launch: 创建一个新的协程
  • async : 创建一个新的带返回值的协程,返回的是 Deferred 类
  • withContext:不创建新的协程,指定协程上运行代码块,它只能在协程内部实现
  • runBlocking:不是 GlobalScope 的 API,可以独立使用,区别是 runBlocking 里面的 delay 会阻塞线程,而 launch 创建的不会

参考代码:

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

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        btn1.setOnClickListener {
            CoroutineScope(Dispatchers.Main).launch {
                val text = withContext(mMainScope.coroutineContext){
                    return@withContext "内容1"
                }
                //也可以这样
                val text1 = withContext(Dispatchers.IO){
                    return@withContext "内容2"
                }
                btn1.text = text1
            }
        }
    }
}

 

 



 

 

End

分类:

技术点:

相关文章: