使用协程前,肯定是要先添加协程依赖:核心库和平台库,关于依赖这里就不多说了,网上很多例子。现在直接进入正题。
delay(timeMillis)前的逻辑会立即执行,delay(timeMillis)后的逻辑会延时timeMillis后才会执行。
他是一个特殊的挂起函数,它并不会造成函数阻塞,但是会挂起协程。
fun delayFunction(){
GlobalScope.launch(Dispatchers.Main) {
println("TARGTARG:协程开启的子线程的名字:${Thread.currentThread().name},执行时间:${System.currentTimeMillis()}")
delay(2000)
println("TARGTARG:第一次延时执行时间:${System.currentTimeMillis()}")
delay(2000)
println("TARGTARG:第二次延时执行时间:${System.currentTimeMillis()}")
}
}
运行结果如下:
delay(timeMillis: Long)和 Thread.sleep(timeMillis: Long)的区别:
————thread线程是针对整个kotlin/Java系统的;而delay仅仅是协程里面的概念,也只能在协程里面才能够调用,其他地方调用直接报错。
————delay()是一个特殊的挂起函数,它并不会造成函数阻塞,但是会挂起协程;Thread.sleep()则是让当前线程处于休眠状态,时间到了之后再自动唤醒,休眠期间会交出当前线程的执行权。
提到launch{…}就不得不提到创建于启动协程的三种方式:
async/Deferred{…}、launch/Job {…}、runBlocking{…}
但是runBlocking通常用于单元测试的场景,而业务开发中不会用到这个函数,因为它是阻塞线程的。
因此我们更多的是使用前面两个async/Deferred和launch/Job 来创建并启动一个协程。
那这两种方式有什么区别呢?
——相同点:它们都可以用来启动一个协程,返回的都是 Coroutine,我们这里不需要纠结具体是返回哪个类。
——不同点:async 返回的 Coroutine 多实现了 Deferred 接口。同时async 还用于并发需求
关于 Deferred简单理解意思就是延迟,也就是结果稍后才能拿到。我们调用 Deferred.await()就可以得到结果了。
这里我们先讲launch/Job {…}。
通过launch/Job {…}来创建并启动协程的方式很多,常见的有比如上面已经出现的:
GlobalScope.launch(Dispatchers.Main){...}
不过这里需要注意的是launch()可以通过Dispatchers在“()”里面指定当前线程,也可以不指定当前线程,指定当前线程的话,那么通过他创建并启动的线程运行在自定的线程内;如果不指定当前线程,那么就是运行在子线程里面。Dispatchers取值说明如下:
Dispatchers.Main:指定协程在主线程里面运行;
Dispatchers.Unconfined:当前未指定协程运行的线程,默认在主线程里面运行;
Dispatchers.IO:指定协程在子线程里面运行,多用于数据和文件的IO读写操作;
Dispatchers.Default:在默认线程(也就是子线程)里面运行,多用于需要CPU进行高密度数据计算的时候,各种算法等。
通过GlobalScope.launch(Dispatchers.Main){…}来创建并启动协程这里不再多说,可以参考上面的例子;不指定运行线程的创建方式:
var job = GlobalScope.launch {
println("TARGTARG:协程开启的子线程的名字:${Thread.currentThread().name},执行时间:${System.currentTimeMillis()}")
delay(2000)
println("TARGTARG:第一次延时执行时间:${System.currentTimeMillis()}")
}
然后我们说说另一种创建方式:
CoroutineScope(Dispatchers.xxx).launch {...}
CoroutineScope这个api主要用于方便地创建一个子域,并且管理域中的所有子协程。注意这个方法只有在所有 block中创建的子协程全部执行完毕后。
同时,它只是挂起,执行完毕之后会释放底层线程用于其他用途,并不会阻塞线程。所以我们称它为挂起函数
具体使用方式如下:
suspend fun coroutineScopeFun(){
CoroutineScope(Dispatchers.Unconfined).launch {
println("TARGTARG:协程开启的子线程的名字:${Thread.currentThread().name},执行时间:${System.currentTimeMillis()}")
delay(2000)
println("TARGTARG:第一次延时执行时间:${System.currentTimeMillis()}")
//delay(timeMillis)前的逻辑会立即执行,delay(timeMillis)后的逻辑会延时timeMillis后才会执行
delay(2000)
println("TARGTARG:第二次延时执行时间:${System.currentTimeMillis()}")
}
}
也可以这样来创建:
suspend fun coroutineScopeFun(){
coroutineScope {
launch(Dispatchers.IO) {
println("TARGTARG:协程开启的子线程的名字:${Thread.currentThread().name},执行时间:${System.currentTimeMillis()}")
delay(2000)
println("TARGTARG:第一次延时执行时间:${System.currentTimeMillis()}")
}
}
}
另外,我们还可以把上面的“coroutineScope”替换为“supervisorScope”
通过GlobalScope.launch(Dispatchers.xxx)、CoroutineScope/supervisorScope(Dispatchers.xxx)都可以创建并启动一个新的协程,那么我们在实际运用中,我们该采用哪种方式最好呢?
当我们使用 GlobalScope.launch 创建并启动协程时,我们会创建一个顶级协程,但是这不是我们所推荐的方式,特别是如果我们忘记了对新启动协程的引用,它还是会继续运行。所以在实际应用中,我们更推荐 : 在执行操作所在指定作用域内启动协程,而非随意使用。也就是推荐优先通过CoroutineScope/supervisorScope(Dispatchers.xxx)创建协程。
在前面我们提到,通过launch创建的协程,会返回一个job对象。Job继承了CoroutineContext.Element, 他是协程上下文的一部分。 Job有两个重要的子类实现:JobSupport、AbstractCoroutine,JobSupport提供Job 的父子关系管理,但是JobSupport已经被废弃;AbstractCoroutine,可以直接理解为是一个协程对象。
Job对象持有所有的子job实例,可以取消所有子job的运行。
Job的join方法会等待自己以及所有子job的执行, 所以Job给予了CoroutineScope一个管理自己所有子协程的能力。
使用launch 或者async方法都会实例化出一个AbstractCoroutine 的协程对象,而这个协程对象因为继承了CoroutineScope,所以拥有一个协程上下文。 一个协程的协程上下文的Job值就是他本身:
suspend fun launchJob(){
val job = CoroutineScope(Dispatchers.Main).launch {
......
}
}
Job对象包含非常丰富的api,包括join()、start()、cancel()等等。
注意上面几种不同的创建协程的方式对于指定运行线程的位置的不同,总结起来就是:
suspend fun launchJob(){
//方式一
var jobWithThread = GlobalScope.launch(Dispatchers.Default) { ...... }
//方式二
var jobNullThread = GlobalScope.launch { ...... }
}
suspend fun coroutineScopeFun(){
//方式三
val jobWithThread = coroutineScope {
launch(Dispatchers.IO) { ...... }
}
//方式四
val jobNullThread = coroutineScope {
launch { ...... }
}
}
suspend fun supervisorScopeFun(){
//方式五
val coroutineJob = supervisorScope {
launch(Dispatchers.IO) { ...... }
}
//方式六
val supervisorJob = supervisorScope {
launch { ...... }
}
}
对于协程,我们还可以嵌套使用:
CoroutineScope.launch(Dispatchers.IO) {
val image = getImage(imageId)
launch(Dispatch.Main) { //在主线程里面更新UI
avatarIv.setImageBitmap(image)
}
}
只是单纯的嵌套,这并没有多少作用。协程有一个非常好用的函数 :withContext。这个函数可以切换到指定线程,并在闭包中的逻辑执行完后自动把线程切回去继续执行:
CoroutineScope(Dispatchers.Main).launch {
//在IO线程获取到Bitmap图片,代码执行到这里,会先挂起。待请求完成拿到Bitmap图片后再继续往后执行
val bitmap = withContext(Dispatchers.IO) {
getImage()
}
iv.setImageBitmap(bitmap)//主线程更新UI
}
因为可以自动切回来,我们甚至还可以对上面的代码做进一步的简化:
suspend fun getImage(): Bitmap = withContext(Dispatchers.IO) {
//.....
}
Android开发示例:
CoroutineScope(Dispatchers.Main).launch {
val bitmap = getImage()
ivTitle.setImageBitmap(bitmap)
}
suspend fun getImage(): Bitmap = withContext(Dispatchers.IO) {
val url = "https://dss0.bdstatic.com/6Ox1bjeh1BF3odCf/it/u=4256581120,3161125441&fm=193"
OkHttpClient().newCall(Request.Builder().url(url).get().build())
.execute().body?.byteStream().use {
BitmapFactory.decodeStream(it)
}
}
这个简单, 就是“超时后抛出异常”,直接上代码:
suspend fun timeOut(){
//超时抛出异常
withTimeout(1300L) {
delay(1400)
}
}
运行结果如下:
需要注意的是:withTimeout(timeMillisOut: Long)的参数timeMillisOut的值必须小于等于delay(timeMillisDelay: Long)的参数timeMillisDelay的值,才会抛出异常,否则会正常执行。
但是在测试过程中发现:
timeMillisOut=1410L,timeMillisDelay=1400的时候还是会抛出异常,但是timeMillisOut=1420L的时候,就正常了,timeMillisOut<1410L时也会抛出异常。
可以理解为这两个参数的值,相差越大,可靠性越高。
withTimeout还可以像下面这样用:
设置超时时间,超过预期时间,抛出异常。可以用于倒计时等。
coroutineScope {
try {
withTimeout(1000){
println("超过设定的时间就失败")
delay(2500)
}
}catch (e:TimeoutCancellationException){
println(e.message)
println("好的好的,我知道了,别啰嗦了")
}
}
超时后抛出null指针:
finally有多个作用,一是:在finally中释放资源;二是:在finally中重新挂起协程。
先来看看释放资源:
suspend fun releaseFinally(){
coroutineScope {
val a = launch {
try {
repeat(1000) { i ->
println("模拟循环输出")
delay(500)
}
} finally {
println("回收资源")
}
}
delay(1000)
println("延迟工作")
a.cancelAndJoin()//取消作业并等待它结束
}
}
coroutineScope {
val a = launch {
try {
repeat(1000) { i ->
println("模拟循环输出")
delay(500)
}
} finally {
withContext(NonCancellable){
println("重新挂起一个被取消的协程")
delay(1000)
println("取消挂起")
}
}
}
delay(1000)
println("延迟工作")
a.cancelAndJoin() //取消一个作业并等待它结束
}
作用是:组合挂起函数,默认顺序调用挂起函数
suspend fun measure(){
val time = measureTimeMillis {
playGame()
playEatAndSleep()
}
println("整个过程经过了$time ms")
}
suspend fun playGame() {
delay(1000)
println("打豆豆1秒钟")
}
suspend fun playEatAndSleep() {
delay(1000)
println("吃饭睡觉1秒钟")
}
在第2小节处我们首先提到了async函数,但是没有展开细说,现在我们来看看他具体怎么用。
首先,async用于并发执行的需求。前面我们列举到的场景都是按顺序执行的,可以理解为“同步”,但是在我们实际开发中,更多的是希望并行执行,提高执行效率,那么这可以借助于 async 来实现。
注意
在概念上,async 就类似于 launch。它创建并启动一个单独的协程,不同之处在于 launch 返回一个 Job 并且不附带任何结果值,而 async 返回一个 Deferred —— 一个的非阻塞 future, 这代表了一个将会在稍后提供结果的 promise。你可以使用 .await() 在一个延期的值上得到它的最终结果, 但是 Deferred 也是一个 Job,所以如果需要的话,你可以取消它。
suspend fun asyncFun(){
measureTimeMillis {
coroutineScope {
async { playGame() }
async { playEatAndSleep() }
}
}.let {
println("并发执行所费时间+ $it")
}
}
playGame()和playPP()同第6小节。
运行结果如下:
可以看到在并发执行的环境下,两个任务所花费的时间远远小于按顺序执行所花费的时间。
我们还可以通过更改 async 的属性来实现惰性模式,在这个模式下,只有通过 await 或者 async的返回值 job调用start,才会启动。
suspend fun inertiaasyn(){
measureTimeMillis {
coroutineScope {
val game = async(start = CoroutineStart.LAZY) { playGame() }
val pp = async(start = CoroutineStart.LAZY) { playEatAndSleep() }
game.start()
pp.start()
println("启动完成")
}
}.let(::println)
}
运行效果:
注意:
如果直接调用await,那么结果将会是顺序执行
suspend fun inertiaasyn(){
measureTimeMillis {
coroutineScope {
val game = async(start = CoroutineStart.LAZY) { playGame() }
val pp = async(start = CoroutineStart.LAZY) { playEatAndSleep() }
game.await()
pp.await()
println("启动完成")
}
}.let(::println)
}
们可以定义异步风格的函数来异步调用 playGame 和 playPP,并使用 async 协程建造器并带有一个显式的 GlobalScope引用
suspend fun someThing(){
measureTimeMillis {
val somethingPlayGame = somethingPlayGame()
val somethingPlayPP = somethingPlayEatAndSleep()
runBlocking {
somethingPlayGame.await()
somethingPlayPP.await()
}
}.let(::println)
}
fun somethingPlayGame() = GlobalScope.async {
playGame()
}
fun somethingPlayPP() = GlobalScope.async {
playEatAndSleep()
}
suspend fun playGame() {
delay(1000)
println("1秒钟打豆豆")
}
suspend fun playEatAndSleep() {
delay(1000)
println("吃饭睡觉1秒钟")
}
注意:这样的写法我们并不推荐。如果 val somethingPlayGame = somethingPlayGame() 和 somethingPlayGame.await() 有逻辑错误,程序将抛出异常,但是 somethingPlayEatAndSleep 依然在后台执行,但是因为前者异常,所有协程都将被关闭,所以 somethingPlayEatAndSleep 操作也会被终止
协程总是运行在以 coroutineContext 接口为代表的上下文中,协程上下文是各种不同元素的集合,事实上, coroutineContext 就是一个存储协程信息的context,详见《协程上下文Context》
调度器dispatchers,前面我们讲launch的时候已经讲过了,其主要作用就是我们可以借助其限制协程的工作线程。不在多说。
当一个协程被其他协程在CoroutineScope启动时,它将通过CoroutineScope.CoroutineContext来承袭上下文,并且这个新协程将成为父协程的子作业。当一个父协程被取消时,同时意味着所有的子协程也会取消。
然而,如果此时用GlobalScope.launch启动子协程,则它与父协程的作用域将无关并且独立运行。
suspend fun childCoroutine(){
val job = GlobalScope.launch {
GlobalScope.launch {
println("使用GlobalScope.launch创建并启动协程")
delay(1000)
println("GlobalScope.launch-延迟结束")
}
launch {
println("单独使用launch创建并启动协程")
delay(1000)
println("launch-延迟结束")
}
}
delay(500)
println("取消父launch协程")
job.cancel()
delay(1000)
}
运行效果:
简单来说就是,当我们取消某个“父协程”,但是又不希望该父协程的某个或某些子协程也一并被取消,那么我们就可以用GlobalScope.launch来启动子协程,以此让它与父协程的作用域将无关并且独立运行。
使用 join 等待所有子协程执行完任务。
var childJob = launch {......}
childJob.join()
suspend fun joinCoroutine(){
val job = GlobalScope.launch {
GlobalScope.launch {
println("使用GlobalScope.launch创建并启动协程")
delay(1000)
println("GlobalScope.launch启动的协程运行结束")
}
launch {
println("单独使用 launch 创建并启动协程")
delay(1000)
println("单独使用 launch 创建并启动协程运行结束")
}
}
job.join()
}
运行效果:
上面的代码中,如果我们去掉最后的“ job.join()”,那么我再执行,就会发现,控制台不会输出任何信息:GlobalScope.launch启动的协程将立即独立执行,如果不使用join,则joinCoroutine()函数可能瞬间执行完成,无法看到执行效果。使用join方法可以让joinCoroutine()函数所在的协程暂停,直到 GlobalScope.launch执行完成。
这个简单:
GlobalScope.launch(Dispatchers.Default+CoroutineName("指定的协程名称")){
println(Thread.currentThread().name)
}.join()