教你如何使用协程(二)初步启动Kotlin协程

协程是什么?

首先kotlin协程是kotlin的扩展库(kotlinx.coroutines)。

上一篇我们简单了解了线程的概念,线程在Android开发中一般用来做一些复杂耗时的操作,避免耗时操作阻塞主线程而出现ANR的情况,例如IO操作就需要在新的线程中去完成。但是呢,如果一个页面中使用的线程太多,线程间的切换是很消耗内存资源的,我们都知道线程是由系统去控制调度的,所以线程使用起来比较难于控制。这个时候kotlin的协程就体现出它的优势了,kotlin协程是运行在线程之上的,它的切换由程序自己来控制,无论是 CPU 的消耗还是内存的消耗都大大降低。所以大家赶紧来拥抱kotlin协程吧_

为Android项目中引入kotlin协程

  1. 添加依赖

首先要确保你的kotlin版本在1.1以上,我们在Android module中的build.gradle的dependencies中添加如下依赖。

api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'

这里我使用的kotlin版本为1.3.50,协程库版本为1.3.2

  1. 添加混淆

在混淆代码中,具有不同类型的字段可以具有相同的名称,并且AtomicReferenceFieldUpdater可能无法找到正确的字段。要避免在混淆期间按类型进行字段重载,请将其添加到配置中:

-keepclassmembernames class kotlinx.** {
    volatile <fields>;
}

回想一下刚学 Thread 的时候

我相信现在接触 Kotlin 的开发者绝大多数都有 Java 基础,我们刚开始学习 Thread 的时候,一定都是这样干的:

val thread = object : Thread(){
    
    
    override fun run() {
    
    
        super.run()
        //do what you want to do.
    }
}
thread.start()

肯定有人忘了调用 start,还特别纳闷为啥我开的线程不启动呢。说实话,这个线程的 start 的设计其实是很奇怪的,不过我理解设计者们,毕竟当年还有 stop 可以用,结果他们很快发现设计 stop 就是一个错误,因为不安全而在 JDK 1.1 就废弃,称得上是最短命的 API 了吧。

既然 stop 是错误,那么总是让初学者丢掉的 start 是不是也是一个错误呢?

哈,有点儿跑题了。我们今天主要说 Kotlin。Kotlin 的设计者就很有想法,他们为线程提供了一个便捷的方法:

val myThread = thread {
    
    
    //do what you want
}

这个 thread 方法有个参数 start 默认为 true,换句话说,这样创造出来的线程默认就是启动的,除非你实在不想让它马上投入工作:

val myThread = thread(start = false) {
    
    
    //do what you want
}
//later on ...
myThread.start()

这样看上去自然多了。接口设计就应该让默认值满足 80% 的需求嘛。

为什么我们要使用协程

上面我们简单介绍协程的设计巧妙的避开了Thread的弊端,但是协程的作用究竟是什么呢?为啥我们的整个项目要直接用协成替换了Thread?它又是如何能够在如此大的项目中直接扮演Thread这么重要的角色?
首先来强调一个概念:协程是一个轻量级的线程。

接下来用一个官方的demo,解释一下协程为什么能够被如此重视:

runBlocking{
    
    
	repeat(100_000){
    
    //循环100000次
		launch{
    
    //开启一个协程
			delay(1000L)
			print(".")
		}
	}
}

案例很简单,开启10万个协程。等等?启动10万个??没错!这里可以很顺畅的启动10万个!这里我们想想,如果我们启动10万个Thread会是什么样子呢?从这点来看,协程的确可以称的上轻量级。那么协程的优点仅此而已吗?不着急,我们一点点来看。

初识协程:

首先我们来瞄一眼协程是长啥样的, 以下引用(copy)了官网的一个例子:

fun main(args: Array<String>) {
    
    
    launch(CommonPool) {
    
    
        delay(1000L) 
        println("World!") 
    }
    println("Hello,")
    Thread.sleep(2000L)
}

运行结果: ("Hello,"会立即被打印, 1000毫秒之后, "World!"会被打印)

Hello, 
World!

姑且不管里面具体的细节, 上面代码大体的运行流程是这样的:

  • 主流程:

1、调用系统的launch方法启动了一个协程, 跟随的大括号可以看做是协程体.
(其中的CommonPool暂且理解成线程池, 指定了协程在哪里运行)
2、打印出"Hello,"
3、主线程sleep两秒
(这里的sleep只是保持进程存活, 目的是为了等待协程执行完)

  • 协程流程:

协程延时1秒
打印出"World!"

解释一下delay方法:
在协程里delay方法作用等同于线程里的sleep, 都是休息一段时间, 但不同的是delay不会阻塞当前线程, 而像是设置了一个闹钟, 在闹钟未响之前, 运行该协程的线程可以被安排做了别的事情, 当闹钟响起时, 协程就会恢复运行.

再看协程:

我们可以把协程认为是一个轻量的线程。像线程一样,协程同样可以并行运行,彼此等待并进行通信。协程和线程最大的不同就是,协程很轻量,我们可以创建上千个,并且只消耗很少的性能。线程从开始到保持都要耗费很多资源,而且对现在机器来说上千个线程是一个很严峻的挑战。我们可以通过launch{}方法开启一个协程,默认情况下协程运行在一个共享的线程池上。线程仍然可以运行在一个基于协程开发的程序中,一个线程可以运行很多个协程,所以我们将不再需要很多的线程。示例如下:

import kotlinx.coroutines.experimental.*

fun main(args: Array<String>) {
    
    
    println("Start")

    // Start a coroutine
    launch {
    
    
        delay(1000)
        println("Hello")
    }

    Thread.sleep(2000) // wait for 2 seconds
    println("Stop")
}

运行结果:
运行结果

说明:上述代码中我们开启了一个协程,一秒后打印hello。我们使用delay()方法,就像使用Thread.sleep()方法,但是delay方法会更好一些,它不会阻塞线程,它只是暂停协程本身。当协程正在等待时,线程返回到池中,并且当等待完成时,协程将在池中的空闲线程上恢复。
如果你想在main函数中使用非阻塞的delay方法,会发生一个编译错误Suspend functions are only allowed to be called from a coroutine or another suspend function,因为我们没有在协程中执行,我们将它包装在runBolcking{}中使用。runBlocking{}会启动协程并等待协程执行完成


import kotlinx.coroutines.experimental.*

fun main(args: Array<String>) {
    
    
   println("Start")

   // Start a coroutine
   launch {
    
    
       delay(1000)
       println("Hello1")
   }

runBlocking {
    
    
    delay(2000)
    println("Hello2")
}

   Thread.sleep(2000) // wait for 2 seconds
   println("Stop")
}

运行结果

kotlin协程的三种启动方式

到这里,已经我们已经看了两个简单的协程案例了,这两个案例我们都是用了launch关键字来启动协程,其实协程有三种通用的启动方式

  1. runBlocking:T

  2. launch:Job

  3. async/await:Deferred

第一种启动方式(runBlocking:T)

runBlocking 方法用于启动一个协程任务,通常只用于启动最外层的协程,例如线程环境切换到协程环境。
官方解释

上图是官方源码中给出的该方法的解释,意思就是说runBlocking启动的协程任务会阻断当前线程,直到该协程执行结束。

代码示例:

runBlocking启动

执行结果:可以清楚的看到先将协程中的任务完成才执行主线程中的逻辑
结果

第二种启动方式(launch:Job)

我们最常用的用于启动协程的方式,它最终返回一个Job类型的对象,这个Job类型的对象实际上是一个接口,它包涵了许多我们常用的方法。例如join()启动一个协程、cancel() 取消一个协程

注⚠️:该方式启动的协程任务是不会阻塞线程的

代码示例:

launch方式启动
执行结果:可以清楚的看到主线程没有被阻塞

执行结果

第三种启动方式(async/await:Deferred)

1.async和await是两个函数,这两个函数在我们使用过程中一般都是成对出现的。

2.async用于启动一个异步的协程任务,await用于去得到协程任务结束时返回的结果,结果是通过一个Deferred对象返回的。

代码示例:
async/await方式启动

执行结果:可以看到当协程任务执行完毕时可以通过await()拿到返回结果

执行结果
虽然有三种启动方式,但是大部分情况,我们都是使用Launch这种方式。
协程是可以被取消的和超时控制,可以组合被挂起的函数,协程中运行环境的指定,也就是线程的切换

协程启动后还可以取消

launch方法有一个返回值, 类型是Job, Job有一个cancel方法, 调用cancel方法可以取消协程, 看一个数羊的例子:

fun main(args: Array<String>) {
    
    
    val job = launch(CommonPool) {
    
    
        var i = 1
        while(true) {
    
    
            println("$i little sheep")
            ++i
            delay(500L)  // 每半秒数一只, 一秒可以输两只
        }
    }

    Thread.sleep(1000L)  // 在主线程睡眠期间, 协程里已经数了两只羊
    job.cancel()  // 协程才数了两只羊, 就被取消了
    Thread.sleep(1000L)
    println("main process finished.")
}

运行结果是:

1 little sheep
2 little sheep
main process finished.

如果不调用cancel, 可以数到4只羊。

在开发过程中通过cancel我们可以及时释放不必要的资源。

猜你喜欢

转载自blog.csdn.net/abc6368765/article/details/102972682
今日推荐