经过最近一年多阅读文档、使用及阅读源码的感觉:java
sleep()
,或者死锁。new Thread()
新建线程的方式来比较性能消耗,来显得更轻量化是不厚道的。我以为应该与Executors.newCachedThreadPool()
来比才合适。实现一个从网络请求用户信息,而且把用户昵称显示在UI上的功能: 实现第一个android
GlobalScope.launch(Dispatchers.Main) {
val userInfo = getUserFromNetwork(userId)//网络请求,运行在后台
textView.text = userInfo.name//更新UI,运行在主线程
}
suspend fun getUserFromNetwork(userId: String): String {
... //具体这里的实现忽略,下文会分析到
}
复制代码
从上面的协程示例中看到:api
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
}
}
}
}
})
复制代码
实现上述的业务逻辑,协程是怎么作的?并发
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运行完才会被执行到
复制代码
这是一个第一个示例的全版: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)
}
复制代码
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) {...}
}
复制代码
这里须要跟前面并发请求的状况区分开来,使用的场景不一样进行选择。
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
方法的调用者必须是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的形式来处理异步逻辑时,此时咱们想把Callback的API改成Coroutine要怎么实现呢?
//原来的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)
})
}
}
复制代码
使用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之类的工具库。 碰到下面的状况时建议使用协程:
如何添加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"
}
复制代码