【原创】Kotlin Coroutine协程——1.协程是什么

Kotlin Coroutine协程

  • Kotlin Coroutine 协程是什么? Coroutine协程这个概念在20世纪60年代就有了,可谓久远,Wiki 百科上也有解释。有人说它是控制流的让出和恢复,也有说能像线程并发处理但不会阻塞,官方说它比线程更轻量化。咱们并不须要把Kotlin Coroutine神化,它到底是什么?

经过最近一年多阅读文档、使用及阅读源码的感觉:java

  • 运行在线程里,实际是运行在线程池里。
  • 由于运行在线程里,只要使用不当,依然会存在阻塞的状况,例如使用sleep(),或者死锁。
  • 让协程与new Thread()新建线程的方式来比较性能消耗,来显得更轻量化是不厚道的。我以为应该与Executors.newCachedThreadPool()来比才合适。
  • 让编写异步代码变得容易,特别是在一个多个异步同时处理的时候。
  • 异步代码简单了以后,咱们能够把UI线程解脱出来,使用更多异步风格,优化了UI性能。
  • 在UI线程和IO线程切换十分的方便。
  • 少了回调及广播的方式来处理异步,代码更容易阅读。

第一个Kotlin Coroutine

实现一个从网络请求用户信息,而且把用户昵称显示在UI上的功能: 实现第一个android

GlobalScope.launch(Dispatchers.Main) {
    val userInfo = getUserFromNetwork(userId)//网络请求,运行在后台
    textView.text = userInfo.name//更新UI,运行在主线程
}

suspend fun getUserFromNetwork(userId: String): String {
    ...  //具体这里的实现忽略,下文会分析到
}
复制代码

从上面的协程示例中看到:api

  • 网络请求与UI写在一个方法序列里,没有回调
  • 2行逻辑是运行在不一样的线程里的
  • getUserFromNetwork()suspend这个修饰符
  • GlobalScope.launch使用了这个方法来启动一个协程

回调的困境

例如须要在一个列表中显示,用户的在线状态及等级。bash

//须要经过网络请求后台api
api.getUserInfo(userId)//1.查询用户信息
api.getOnlineStatus(userId)//2.查询在线状态
api.getLevelInfo(userId)//3.查询等级
复制代码

3个api之间没有依赖关系,最合理的方式应该是3个一块儿并发请求再组装数据。网络

但在用回调时,要实现这样的逻辑会变得很困难,权衡之下会把它写成了在回调中串行执行,如此整个网络的延时就会是原来的3倍多线程

回调的串行实现

api.getUserInfo(userId, object : Callback<UserInfo> {
    override fun onResponse(response: Response<UserInfo>) {
      val userInfo = response.body
      api.getOnlineStatus(userId, object: Callback<OnlineStatus> {
        override fun onResponse(response: Response<OnlineStatus>) {
          val onlineStatus = response.body
          api.getLevelInfo(userId, object: Callback<LevelInfo> {
            override fun onResponse(response: Response<LevelInfo>) {
              val levelInfo = response.body
              val composeInfo = compose(userInfo, onlineStatus, levelInfo)//组装数据
              getLiveData().postValue(composeInfo)//刷新UI
            }
         }
      }
    }
})
复制代码

Coroutine的并行实现

实现上述的业务逻辑,协程是怎么作的?并发

GlobalScope.launch {
  //async里的3个block是同时请求,无需等待前一个的结果
  val userInfo = async { api.getUserInfo(userId) }
  val onlineStatus = async { api.getOnlineStatus(userId) }
  val levelInfo = async { api.getLevelInfo(userId) }
  //等待3个请求所有返回了,再组装数据
  val composeInfo = compose(userInfo.await(), onlineStatus.await(), levelInfo.await())
  getLiveData().postValue(composeInfo)//刷新UI
}
复制代码

咱们能够用顺序的方式来让多线程执行起来,在同步和异步之间灵活的切换。这样的特性,可让咱们写出以前很难才能作出的逻辑,这是Coroutine的优点。异步

  • 使用async来启动一个新的协程@userInfo,返回一个Deferred,调用Deferred.await()方法,此时当前协程会挂起,等待@userInfo执行结束。

runBlocking 上面已经讲了2种启动协程的方式,分别是launch,async。然而还有一种叫runBlocking的方式,在官方文档也有写到,同时也发现了会有同窗对这个方式存在一些使用上的误解状况。 当运行到这个runBlocking()的时候当前线程会被阻塞住。特别注意这个方法不该在Coroutine内部使用。根据官方文档说明,它是被用在阻塞main线程及测试的时候使用的。不建议你们使用async

runBlocking {
    api.getUserFromNetwork(userId)
    api.getOnlineStatus(userId)
}
...//直到getUserFromNetwork运行完才会被执行到
复制代码

在Coroutine里运行在不一样的线程上

这是一个第一个示例的全版:ide

GlobalScope.launch(Dispatchers.Main) {
    val userInfo = getUserFromNetwork(userId)//网络请求,运行在后台
    textView.text = userInfo.name//更新UI,运行在主线程
}

suspend fun getUserFromNetwork(userId: String) = withContext(Dispatchers.IO) {
    HttpSerivce.getUser(userId)
}
复制代码

withContext

Coroutine里有一个withContext()的函数,它能够指定协程在哪一个线程里执行,并让后续代码等待,按顺序去执行。

有了这个函数,能够消除在切换线程时致使的Callback嵌套。

若是咱们经过启动不一样的协程来切换线程,代码是长这样的:

GlobalScope.launch(Dispatchers.IO) {
    ...
    launch(Dispatchers.Main) {
    	...
      launch(Dispatchers.IO) {
        ...
      }
    }
}
复制代码

是否是又有一种回调的感受回来了

而使用withContext()则可让协程摆脱上面的嵌套写法。

GlobalScope.launch(Dispatchers.Main) {
    val result0 = withContext(Dispatchers.IO) {...}
    val result1 = withContext(Dispatchers.Main) {...}
    val result2 = withContext(Dispatchers.IO) {...}
}
复制代码

这里须要跟前面并发请求的状况区分开来,使用的场景不一样进行选择。

suspend(挂起)

GlobalScope.launch(Dispatchers.Main) {
    val userInfo = getUserFromNetwork(userId)//网络请求,运行在后台
    textView.text = userInfo.name//更新UI,运行在主线程
}

suspend fun getUserFromNetwork(userId: String) = withContext(Dispatchers.IO) {
    HttpSerivce.getUser(userId)
}
复制代码

可见,对于suspendGetUserInfo()内的逻辑运行在什么线程里,能够不禁调用者决定的,能够由实现者决定的。

咱们去设计本身的挂起函数时,若是须要在特定的线程里,最好的方式是咱们函数内部去指定。好比操做文件读写的逻辑时,定义运行在Dispatchers.IO里,这样也不用担忧外面会使用错误。

何为挂起?!

  • 既不是函数被挂起,也不是线程被挂起,而是当前协程被挂起,正在运行这个协程的线程,从挂起的那时候开始,再也不执行这个协程了。
  • 协程执行到挂起函数的地方时,就会脱离运行它的线程,这条线程并不会阻塞,它会去干别的事情。
  • 协程脱离后,并非指它中止了,而是等待被系统安排其它的线程在适当的时机来运行它。
launch(Dispatchers.Main) {
    val userInfo = suspendGetUserInfo(userId)
    textView.text = userInfo.name
}
suspend fun suspendGetUserInfo(userId: Long): UserInfo {
  //可切换线程IO线程执行,原来执行的Main线程将空闲执行其它工做
  return withContext(Dispatchers.IO) {
    getUserFromNetwork(userId)
  }
}
复制代码
//在Main UI线程执行
log.debug("run in click starting")
GlobalScope.launch(Dispatchers.Main) {
    log.debug("run in launch")
    val userInfo = suspendGetUserInfo(userId)
}
log.debug("run in click finishing")
复制代码

打印的顺序是?

运行结果

D: 11:50:30.825 main: run in click starting
D: 11:50:30.869 main: run in click finishing
D: 11:50:30.872 main: run in launch
D: 11:50:30.873 main: run in suspendGetUserInfo starting
复制代码

协程运行的时候,尽管仍是在Main里运行,实际上也是在下个一个Main Looper时才运行到。

suspend fun suspendGetUserInfo(userId: Long): UserInfo {
    //可切换线程IO线程执行,原来执行的Main线程将空闲执行其它工做
    log.debug("run in suspendGetUserInfo starting")
    GlobalScope.launch(Dispatchers.Main) {
        log.debug("Main is available")
    }
    val userInfo = withContext(Dispatchers.IO) {
        delay(100)
        log.debug("run in withContext")
        getUserFromNetwork(userId)
    }
    log.debug("run in suspendGetUserInfo finishing")
    return userInfo
}
复制代码

打印的顺序是?

D: 12:03:06.469 main : run in suspendGetUserInfo starting
D: 12:03:06.475 main : Main is available
D: 12:03:06.582 DefaultDispatcher-worker-2 : run in withContext
D: 12:03:06.585 main : run in suspendGetUserInfo finishing
复制代码

因为withContext是挂起函数,已经切换到IO上执行,所以Main是空闲的,下一个Looper的时候就能够执行launch里的代码。

suspend的语法规则

  • Kotlin协程规定,一个suspend方法的调用者必须是suspend方法或者是在launch()/async()/runBlocking()启动的协程调用。
  • 协程的基础库有很多带有suspend的函数,当咱们要去使用时,要留意符合上面的规则。反之,没有用到suspend函数的地方,并不须要给本身函数加上,Android Studio也会相应的代码提示。

其它挂起函数

除了withContext(),还有

  • 像上面所用过的await()
  • delay()表示挂起必定时间后再运行,协程里记得不要用sleep()
  • withTimeout { }表示block执行若是超过必定的时间则会抛出TimeoutCancellationException
public suspend fun <T> withContext( context: CoroutineContext, block: suspend CoroutineScope.() -> T
): T 
复制代码
public suspend fun delay(timeMillis: Long)
复制代码

如何将Callback代码改成Coroutine

以前咱们开发的过程当中若是使用了Callback的形式来处理异步逻辑时,此时咱们想把Callback的API改成Coroutine要怎么实现呢?

suspendCancellableCoroutine/suspendCoroutine

//原来的Callback形式方法
fun saveToRepositoryCallback(callback: (Int) -> Unit) { ... }

GlobalScope.launch(Dispatchers.IO) {
  val returnValue = saveToRepository()
}

//协程的形式
suspend fun saveToRepository(): Int {
  return suspendCoroutine { continuation ->
    saveToRepositoryCallback {
      continuation.resume(it)
    }
  }
}
复制代码

经过一个回调的参数continuation来设置返回的数据,此时就至关于把Callback改成了直接返回的形式。

以外若是须要Coroutine能抛出异常时,可使用resumeWithException来把异常抛出来。

//原来的Callback形式方法,第二个参数为当出错返回的异常回调
fun saveToRepositoryCallback(success: (Int) -> Unit, error: (Throwable) -> Unit) {...}

GlobalScope.launch(Dispatchers.IO) {
  try {
  	val returnValue = saveToRepository()
  } catch (t: Throwable) {
    // do something on error
  }
}

//调用该方法有可能会throw exception
suspend fun saveToRepository(): Int {
  return suspendCoroutine { continuation ->
    saveToRepositoryCallback({
      continuation.resume(it)
    }, {
      continuation.resumeWithException(it)
    })
  }
}
复制代码

suspendCancellableCoroutine特性

使用suspendCoroutine启动的Job,想经过cancel()来中止是不行的,依然会继续执行

val job = GlobalScope.launch(Dispatchers.IO) {
    val returnValue = saveToRepository()
    LogUtil.debug("returnValue: $returnValue")
}
//协程的形式
suspend fun saveToRepository(): Int {
    return suspendCoroutine { continuation ->
        saveToRepositoryCallback {
            LogUtil.debug("callback: $it")
            continuation.resume(it)
        }
    }
}
//触发取消
job.cancel()

I: [main] callback: 0
I: [DefaultDispatcher-worker-2] returnValue: 0

复制代码

若是使用suspendCancellableCoroutine,运行returnValue的代码则不会被运行到

val job = GlobalScope.launch(Dispatchers.IO) {
    val returnValue = saveToRepository()
    LogUtil.debug("returnValue: $returnValue")
}

suspend fun saveToRepository(): Int {
    return suspendCancellableCoroutine { continuation ->
        saveToRepositoryCallback {
            LogUtil.debug("callback: $it")
            continuation.resume(it)
        }
    }
}

I: [main] callback: 0
复制代码

总结

对于Kotlin Coroutine来讲,更像是一个跨线程工具。能够把它当作是相似于AsyncTask,Exeutors,Handler,Rxjava之类的工具库。 碰到下面的状况时建议使用协程:

  • 有多个并发的任务同时进行,或者想经过并发提升性能的时候
  • 须要在UI线程和工做线程里作切换的时候

思考

  • 协程能作到彻底避免阻塞问题吗?
  • 比用线程轻量化吗?

附录

如何添加Kotlin Coroutine的依赖:在build.gradle中添加依赖库。

buildscript {
    ext {
    	kotlin_version = '1.3.50'
    	coroutines_android_version = '1.3.2'
    }
}
dependencies {
    //依赖kotlin
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    //这是协程android的库,同时也依赖了kotlin coroutine库
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_android_version"
}
复制代码

若是你们以为这篇文章有用的话,欢迎点赞、评论、收藏、分享。

相关文章
相关标签/搜索