我们知道,AndroID App的进程也是一个DVM,内部有许多线程在执行,比如,主UI线程(Main Thread),垃圾回收线程等。其中主UI线程负责执行我们写的应用代码。对于只做很少的I/O *** 作或耗时 *** 作的App,单一线程开发模式问题不大,但是如果有大量IO或者cpu计算的任务,我们就必须在其他线程内完成了。
在实际的开发过程中,我们通常会要求应用的帧率达到60帧,也就是说每16毫秒就必须进行界面重绘,重绘一帧,这意味着如果我们在主线程上执行的任务超过16毫秒,就会出现丢帧现象,而如果主阻塞时间过长,影响了主线程的运行,甚至还会出现ANR即应用程序无响应错误。这也就意味着,在应用程序的开发中,我们必须在影响主线程的情况下来执行耗时 *** 作。而这也就涉及到了AndroID中的多线程知识,即异步编程。
在AndroID开发中,提供有多种异步编程的方法,如:Thread&Handler、HandlerThread、IntentService和AsnycTask等方法。而这些方法,实际上都是基于多线程实现的,即创建一个子线程,在子线程中执行耗时 *** 作。然而,使用线程来解决耗时问题也是存在一定的缺陷的,如下:
在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相同,且无法取消,因此,我们通常更推荐使用方法三。接下来,我们分别简单介绍这几个方法。
runBlockingrunBlocking是一个阻塞式的函数,也就是说,其中的代码都会阻塞线程,在实际开发中,一般我们很少使用。官方描述:运行一个新的协程并且阻塞当前可中断的线程直至协程执行完成,该函数不应从一个协程中使用,该函数被设计用于桥接普通阻塞代码到以挂起风格(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方法即可。如下所示:
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.IO | IO线程。在主线程之外执行磁盘或网络 I/O。在线程池中执行 |
dispatchers.Unconfined | 在调用的线程中执行 |
启动模式 | 说明 |
---|---|
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秒后输出,运行结果如下:
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,这种方法无疑比各种回调更加清晰明了,从代码看,就像是同步执行一般,顺序执行下来非常清晰。运行程序,主线程也没有被阻塞,在一秒后界面数据被更新,具体结果如下:
同时,我们还可以将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协程所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)