Android开发学习笔记——Kotlin协程

Android开发学习笔记——Kotlin协程,第1张

概述Android开发学习笔记——Kotlin协程Android中的异步编程协程基本使用runBlockingCoroutineScope.launch简单使用suspend挂起总结Android中的异步编程我们知道,AndroidApp的进程也是一个DVM,内部有许多线程在执行,比如,主UI线程(MainThread),垃圾回收线程等。其中主UI线

AndroID开发学习笔记——Kotlin协程Android中的异步编程协程基本使用runBlockingCoroutineScope.launch简单使用suspend挂起总结

AndroID中的异步编程

我们知道,AndroID App的进程也是一个DVM,内部有许多线程在执行,比如,主UI线程(Main Thread),垃圾回收线程等。其中主UI线程负责执行我们写的应用代码。对于只做很少的I/O *** 作或耗时 *** 作的App,单一线程开发模式问题不大,但是如果有大量IO或者cpu计算的任务,我们就必须在其他线程内完成了。

在实际的开发过程中,我们通常会要求应用的帧率达到60帧,也就是说每16毫秒就必须进行界面重绘,重绘一帧,这意味着如果我们在主线程上执行的任务超过16毫秒,就会出现丢帧现象,而如果主阻塞时间过长,影响了主线程的运行,甚至还会出现ANR即应用程序无响应错误。这也就意味着,在应用程序的开发中,我们必须在影响主线程的情况下来执行耗时 *** 作。而这也就涉及到了AndroID中的多线程知识,即异步编程。
在AndroID开发中,提供有多种异步编程的方法,如:Thread&Handler、HandlerThread、IntentService和AsnycTask等方法。而这些方法,实际上都是基于多线程实现的,即创建一个子线程,在子线程中执行耗时 *** 作。然而,使用线程来解决耗时问题也是存在一定的缺陷的,如下:

线程安全问题。在AndroID开发中,UI *** 作必须在子线程中执行,因此在处理多线程任务时,我们需要进行不同线程间的通信来更新UI,否则就会导致线程安全问题。线程切换代价高昂。线程并非廉价的,线程需要进行昂贵的上下文切换,而因为线程安全问题,每次耗时任务执行完毕,我们都必须切换到主线程来更新UI。线程并不是无限的。在底层系统的限制下,线程的数量并不是无限的,这就可能导致严重的瓶颈。回调的复杂。由于需要进行异步通信,我们在进行多线程任务时,经常会使用到回调,当情况复杂时就会造成大量的回调嵌套,降低代码的可读性和可维护性。协程

在Kotlin中,进行异步编程的方式是协程。那么什么是协程呢?官方是如此描述的:

协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。

从这个描述中,我们就可以明白,其本质上就可以看作是一个线程框架。其最大优点在于可以快速切换不同的线程,使我们的异步程序以同步代码的形式表达出来,从而避免了回调。如下,当我们需要网络请求获取数据并更新界面时,如果使用线程的形式,我们通常需要通过回调来更新界面,如下:

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) {        ...    }});

我们可以看到,因为需要进行回调 *** 作,整个代码量被提升且可读性降低,但是,如果我们使用协程来完成的话,我们完全可以以一种同步的形式顺序执行,简单而又明了。如下:

coroutinescope.launch(dispatchers.Main) {   // 在主线程开启协程    val user = API.getUser() // IO 线程执行网络请求    nameTv.text = user.name  // 主线程更新 UI}

协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。

当然,协程很重要的一点就是当它挂起的时候,它不会阻塞其他线程。协程底层库也是异步处理阻塞任务,但是这些复杂的 *** 作被底层库封装起来,协程代码的程序流是顺序的,不再需要一堆的回调函数,就像同步代码一样,也便于理解、调试和开发。它是可控的,线程的执行和结束是由 *** 作系统调度的,而协程可以手动控制它的执行和结束。

基本使用

在使用前,首先,我们需要导入协程的依赖,如下:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2"

kotlin中创建协程的方式主要存在三种方式,如下:

//方法一:使用 runBlocking 顶层函数runBlocking {//耗时 *** 作}//方法二:使用 GlobalScope 单例对象直接调用launch开启协程GlobalScope.launch {//耗时 *** 作}//方法三:行通过 CoroutineContext 创建一个 Coroutinescope 对象val coroutinescope = Coroutinescope(dispatchers.Default)coroutinescope.launch {//耗时 *** 作}

其中方法一是线程阻塞的,在实际开发中很少用到;而方法二和三实际上都是本质都是相同的,都是使用了Coroutinescope的launch方法,只不过方法二是一个单例模式,其生命周期和app相同,且无法取消,因此,我们通常更推荐使用方法三。接下来,我们分别简单介绍这几个方法。

runBlocking

runBlocking是一个阻塞式的函数,也就是说,其中的代码都会阻塞线程,在实际开发中,一般我们很少使用。官方描述:运行一个新的协程并且阻塞当前可中断的线程直至协程执行完成,该函数不应从一个协程中使用,该函数被设计用于桥接普通阻塞代码到以挂起风格(suspending style)编写的库,以用于主函数与测试。
也就是说这个函数主要是用来测试的,因为在挂起函数不可以在main函数中被调用,所以,当我们需要测试协程时就可以使用该函数来进行测试.

private fun testRunBlocking(){        Log.e("test", "main thread start")        runBlocking {            repeat(5){                Log.e("test", "runBlocking $it")                delay(1000)            }        }        Log.e("test", "main thread end")    }

运行结果如下图所示:

Coroutinescope.launch

对于上述所说的方法二和方法三,只要我们查看其源码,我们就会发现,实际上二者都是调用了Coroutinescope.launch方法,只不过方法二使用了一个单例模式。所以,对于方法二和三,我们只需要搞懂Coroutinescope.launch方法即可。如下所示:

public fun Coroutinescope.launch(    context: CoroutineContext = EmptyCoroutineContext,    start: Coroutinestart = Coroutinestart.DEFAulT,    block: suspend Coroutinescope.() -> Unit): Job {    val newContext = newCoroutineContext(context)    val coroutine = if (start.isLazy)        LazyStandaloneCoroutine(newContext, block) else        StandaloneCoroutine(newContext, active = true)    coroutine.start(start, coroutine, block)    return coroutine}

从源码可以发现,launch()是Coroutinescope的一个扩展方法。我们目前可以忽略其方法的实现,只看其方法的声明,我们可以发现,该方法存在三个参数,返回一个Job对象。各个参数如下:

context(CoroutineContext):协程上下文。指定协程运行的线程并进行线程切换,kotlini提供了以下四种值,如下:
说明
dispatchers.Default默认值。可以在主线程之外执行 cpu 密集型的工作。例如对列表进行排序和解析 JsON。在线程池中执行。
dispatchers.Main主线程。协程在主线程中进行,可以进行UI *** 作
dispatchers.IOIO线程。在主线程之外执行磁盘或网络 I/O。在线程池中执行
dispatchers.Unconfined在调用的线程中执行
start(Coroutinestart):协程启动模式。kotlin中协程存在四种启动模式,如下表所示:
启动模式说明
Coroutinestart.Default默认的模式,立即执行协程体
Coroutinestart.LAZY只有在需要的情况下运行
Coroutinestart.ATOMIC立即执行协程体,但在开始运行之前无法取消
Coroutinestart.UNdisPATCHED立即在当前线程执行协程体,直到第一个 suspend 调用

block(suspend Coroutinescope.() -> Unit):协程主体。这就是我们执行异步任务的协程主体部分,是一个用suspend关键字修饰的一个无参,无返回值的函数类型。

返回值Job:对当前创建的协程的引用。可以通过Job对象的start、cancel、join等方法来控制协程的启动和取消。

简单使用

对launch方法有了一个基本认识后,我们就可以来学习下简单使用launch方法了。首先,我们创建一个协程,如下:

Log.e("test", "main thread start")GlobalScope.launch {    //耗时 *** 作    Thread.sleep(1000)    Log.e("test", "launch")}Log.e("test", "main thread stop")

运行程序,我们发现主线程并未被阻塞,“launch”在1秒后输出,运行结果如下:


但是,这样简单的使用下,我们会发现这和线程比并没有多大的差别。我们说过,协程的最大优势在于能够快速切换线程,将异步转化为同步形式。在一个常见场景下,我们可能需要通过网络请求获取一些参数,然后再进行界面更新,此时如果我们使用子线程执行的话,我们可能需要通过异步通信Handler等方式来切换到主线程更新UI,但是对于协程而言,我们只需要使用withContext方法切换线程,然后方法中执行完毕后会自动切换到主线程执行,如下:

Log.e("test", "main thread start")        GlobalScope.launch(dispatchers.Main) {            //耗时 *** 作            val result = withContext(dispatchers.IO){                Thread.sleep(1000)                Log.e("test", "launch")                return@withContext "this is a  message"            }            Log.e("test", "update UI")            tvTest.text = result        }        Log.e("test", "main thread stop")

我们可以看到,我们通过withContext方法切换到IO线程获取到result,然后协程在withContext执行完毕后自动切回主线程,然后更新tvTest,这种方法无疑比各种回调更加清晰明了,从代码看,就像是同步执行一般,顺序执行下来非常清晰。运行程序,主线程也没有被阻塞,在一秒后界面数据被更新,具体结果如下:


界面在1秒后正常更新:

同时,我们还可以将withContext放入到一个挂起函数中,如下:

Log.e("test", "main thread start")GlobalScope.launch(dispatchers.Main) {    val result = getMessage()//模拟网络请求获取数据    Log.e("test", "update UI")    tvTest.text = result}Log.e("test", "main thread stop")        /** * 耗时方法 */private suspend fun getMessage() = withContext(dispatchers.IO){    Thread.sleep(1000)    Log.e("test", "launch")    return@withContext "this is a data"}

执行结果完全相同。我们可以看到,如此一来,代码就更加清晰了,这个耗时 *** 作就好像一个同步执行的方法一样,值得注意的是,withContext必须在协程或者挂起函数中使用,否则会报错。挂起函数就是suspend修饰的函数,具体我们之后再讲述。

从上述例子中,我们就可以完全发现协程的优势所在了,将异步 *** 作转换为同步表达,使代码变得更加清晰明了。

从上述例子中,我们就可以完全发现协程的优势所在了,将异步 *** 作转换为同步表达,使代码变得更加清晰明了。接下来,我们就可以来学习一下suspend挂起 *** 作了。

suspend挂起

首先,我们需要明白挂起是什么?挂起即挂起协程,将协程在当前执行其代码的线程中脱离,转由其它线程执行。
虽然使用协程比我们使用多线程方式进行异步 *** 作要方便很多,且使用方法也存在很多差别,但是我们需要明白其内部肯定也是通过多线程的形式来实现的,其只是一个更为简单便捷的线程框架。所以,其内部 *** 作实际上也是在 *** 作线程。因此,协程脱离后肯定是在其它线程中执行了。我们可以修改下上述代码,打印出当前线程ID,如下:

Log.e("test", "main thread start")GlobalScope.launch(dispatchers.Main) {    Log.e("test", "main thread ID : ${Thread.currentThread().ID}")    val result = getMessage()//模拟网络请求获取数据    Log.e("test", "update UI---thread ID : ${Thread.currentThread().ID}")    tvTest.text = result}Log.e("test", "main thread stop")                 /** * 耗时方法 */private suspend fun getMessage() : String{    Log.e("test", "getMessage thread ID : ${Thread.currentThread().ID}")    withContext(dispatchers.IO){        Thread.sleep(1000)        Log.e("test", "launch----thread ID : ${Thread.currentThread().ID}")    }    Log.e("test", "return --getMessage thread ID : ${Thread.currentThread().ID}")    return "this is a data"}

运行结果如下:

从运行结果,我们发现,withContext方法执行时,线程从主线程中被切换到子线程,而在withContext之后的代码此时被阻塞,执行完毕后又自动切换到了主线程,即resume。所以,我们可以知道,协程中的挂起,实际上就是线程的切换,不同的时,协程中,切换线程后会自定resume到原来的的线程中。而对于suspend这个修饰符,我们可以看作协程的代码块中,线程执行到了 suspend 函数这里的时候,就暂时不再执行剩余的协程代码,跳出协程的代码块。当然,实际上从上述例子,我们可以看出suspend并不是真正的挂起函数,而是withContext,只有执行到了withContext后,函数才被挂起。suspend只是起到了一个说明作用。

我们可以这样理解:当执行到挂起函数时,挂起函数及其之后的代码都被切换到另一个线程中执行,而当挂起函数执行完毕后,又会自动切换到原线程,然后在原线程中执行挂起函数之后的代码。

那么挂起函数切换到那个线程执行呢?这就是又调度器决定的了,而dispatchers就会为我们指定到对应的线程中执行。dispatchers 调度器,它可以将协程限制在一个特定的线程执行,或者将它分派到一个线程池,或者让它不受限制地运行。

总结

总之Kotlin 协程并没有脱离 Kotlin 或者 JVM 创造新的东西,它只是将多线程的开发变得更简单了,可以说是因为 Kotlin 的诞生而顺其自然出现的东西,从语法上看它很神奇,但从原理上讲,它并不是魔术。

也就是说,Kotlin协程尽管比我们使用Thread&Handler、AsncyTask等方式处理异步任务方便许多,但是其本质实际上并没有脱离多线程的使用,我们可以将其看作是一个线程处理框架。了解了launch和挂起函数的意义后,我们就对kotlin协程有了一个大致的了解,当然协程中还有一些知识,如async等,这些还需要去学习。

总结

以上是内存溢出为你收集整理的Android开发学习笔记——Kotlin协程全部内容,希望文章能够帮你解决Android开发学习笔记——Kotlin协程所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/1055439.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-25
下一篇 2022-05-25

发表评论

登录后才能评论

评论列表(0条)

保存