这里一开始不打算介绍什么是协程,虽然标题叫介绍~~html
为了方便理解,这边先作个比喻: 从使用的角度来看,Kotlin的协程像是“另外一种RxJava”,可是比RxJava要高效。java
这里先有个大体的印象,先了解下协程在实际中的做用,回头再去看它的原理,或许会更容易些。android
一开始查了好多关于协程资料(包括官方完档),发现不一样的人说的不大同样,最后越看越乱。因而我决定一开始先不说什么是协程。后端
上面说到,协程用起来“像是另外一种RxJava”。api
那么是否是能够用协程来开启一个异步操做?切换线程? 答案是确定的,不只能够作到,并且写起来也很简单。下面看个栗子网络
举个例子,这里有个登陆操做,须要用两个接口才能完成。 一、使用帐号密码去获取token 二、经过token获取用户信息异步
很明显,这是个嵌套的请求。代码立刻就浮如今脑海中,因而咱们埋头“papapa”,很快就写出了这样的一段:async
reqToken(new CallBack<String>() { //请求token
@Override
public void onSuccess(String token) {
reqUserInfo(token, new CallBack<UserInfo>() { //经过token,获取用户信息
@Override
public void onSuccess(UserInfo userInfo) {
Logger.Companion.i("login success");
}
});
}
});
复制代码
是的,确实没什么问题。不过没以为这要的代码很长吗?ide
因而咱们改用lambda简写,或是kotlin:函数
reqToken{ //请求token
reqUserInfo(it){ //经过token,获取用户信息
Logger.i("login success")
}
}
复制代码
nice,瞬间简洁了好多
确实简洁了不少。不过仍是难逃嵌套结构,若是多来几层,最后可能成了这样:
看得头皮发麻~~
可是!!!,若果用协程就不同了(划重点)
coroutineScope.launch {
val token = getToken()
val userInfo = getUserInfo(token)
Logger.i("login success")
}
复制代码
不只代码少,并且能够用同步的方式来写异步!!!
往下再了解一点?
知道到了他的优(niu)秀(bi)之处,下面来看看是怎么用的
由于是Kotlin的协程,因此项目须要支持Kotlin。怎么支持就不用我说了吧? (不要问我,我不会,由于那是另外一个同事作的。hahaha~~~)
gradle倒入协程依赖
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
复制代码
能够直接new一个MainScope
val mainScop = MainScope()
复制代码
注意记得在销毁的时候调用cancel()
,调用cancel()
后用mainScope
启动的协程都会取消掉。
override fun onDestroy() {
super.onDestroy()
mainScope.cancel()
}
复制代码
经常使用的方式有两种:launch()
、async()
,下面分别来讲明他们的用途。
固然还有其余的建立方式,这里就不说了。
使用launch()
建立一个协程,返回一个Job
对象。
val job = mainScope.launch {
//在协程中的操做
Logger.i("launch end")
}
复制代码
很简单,mainScope.launch{}
就能建立一个协程,大括号中的代码是在协程中执行的。
看下打印的日志,发现这个协程时在主线程中运行的。
"这有什么用?在主线程中运行的协程?那我再里面作耗时操做,是否是会卡住?"
确实,若是直接这样用时会阻塞主线程的。因此这时候,就须要用到withContext()
这里用的是上面的
mainScope
,这个做用域内的调度器是基于主线程调度器的。也就是说,mainScope.launch()
获得的协程默认都是在主线程中。因此println("in main scope")
是在主线程中运行的
withContext()
:**用给定的协程上下文调用指定的暂停块,暂停直到完成,而后返回结果。**也就是说,能够用来切换线程,并返回执行后的结果。
经常使用的有 Dispatchers.Main
:工做在主线程中 Dispatchers.Default
:将会获取默认调度器(子线程) Dispatchers.IO
:IO线程 Dispatchers.Unconfined
:是一个特殊的调度器(说实话,我没搞懂他的用法~~)
这里子线程中请求一个token,而后回到主线程中:
mainScope.launch {
val token = withContext(Dispatchers.Default) {
Logger.i("get token")
val token = api.getToken()//注意!!!这是个同步的网络请求
token
}
Logger.i("token $token")
}
复制代码
再来看下日志
有withContext()后,线程的切换显得是那么简单。只要你开心,能够切来切去。
mainScope.launch {
withContext(Dispatchers.Default) {
Logger.i("切到子线程")
}
withContext(Dispatchers.Main) {
Logger.i("切到主线程")
}
withContext(Dispatchers.IO) {
Logger.i("切到IO线程")
}
Logger.i("launch end")
}
复制代码
除了launch()
,还有个经常使用的方法——async()
,async()
和launch()
类似。不一样的是他能够返回协程执行结束后值。
async()
返回的是一个Deferred
对象,须要经过Deferred#await()
获得返回值。
仍是上面的例子:子线程中请求一个token,而后回到主线程中:
mainScope.launch {
val tokenDeferred = async(Dispatchers.Default) {
Logger.i("get token")
val token = api.getToken()//注意!!!这是个同步的网络请求
token //返回token
}
val token = tokenDeferred.await()
Logger.i("token : $token")
}
复制代码
打印的结果上面同样,就不贴图了。
async()
和launch()
同样,都能指定执行的线程。
因为
Deferred#await()
须要在协程中调用,因此上面在launch()
中使用async()
。
“这有什么用?跟launch()差很少啊?”
额~~ 用处大了,往下看
若是切换线程中的代码不少,想把(withContext(){...}
)的代码抽出来。因而写成这样
fun getToken(): String {
return withContext(Dispatchers.Default) {
//同步请求获得token
val token = api.getToken()
token
}
}
复制代码
BUT,并不能这样用,发现编译器报错了:
withContext()
只能在
协程或**
suspend
**方法中使用。因此,在方法前加上
suspend
就不会报错了。
suspend fun getToken(): String { ... }
复制代码
suspend:申明这是个可挂起的函数,里面能够用协程的一下方法(launch()、async()、withContext()等)。
有了协程,写异步的代码将会方便不少。
回到一开始的栗子,请求token,而后用token请求UserInfo
mainScope.launch {
//获取token
val token = withContext(Dispatchers.Default) {
val token = api.getToken()
token
}
//经过token,获取userInfo
val userInfo = withContext(Dispatchers.Default) {
val userInfo = api.getUserInfo(token)
userInfo
}
//登陆成功
Logger.i("login success, token: $token, userInfo is null: ${userInfo == null}")
}
复制代码
看到这里,你可能会:“不对啊,一开始的栗子没这么复杂~~~”
由于上面的例子中,把请求那部分的代码抽到suspend
方法去了。
mainScope.launch {
//获取token
val token = getToken()
//经过token,获取userInfo
val userInfo = getUserInfo(token)
//登陆成功
Logger.i("login success, token: $token, userInfo is null: ${userInfo == null}")
}
---------------------------------------
suspend fun getUserInfo(token: String): UserInfo {
return withContext(Dispatchers.Default) {
Logger.i("get userInfo, token: $token")
val userInfo = api.getUserInfo(token)
userInfo
}
}
suspend fun getToken(): String {
return withContext(Dispatchers.Default) {
Logger.i("get token")
val token = api.getToken()
token
}
复制代码
稍微调整下,就会发现和上面是栗子是同样的
有时候,遇到“优秀”的后端同窗。一个页面须要请求两个接口,用两个接口返回的数据才能渲染出页面。
这里发起两个连续的请求也能够作到,可是若是能够变成两个并行的请求,岂不美哉?
那么,async()
就能够排上用场了。
mainScope.launch {
val timeMillis = measureTimeMillis { //记录耗时
val deferred1 = async { getData1() }
val deferred2 = async { getData2() }
val data1 = deferred1.await()
val data2 = deferred2.await()
Logger.i("data1: $data1, data1: $data2")
}
Logger.i("timeMillis : $timeMillis")
}
--------------------------------------------------
suspend fun getData1(): String {
return withContext(Dispatchers.Default) {
Thread.sleep(1000)
"value1"
}
}
suspend fun getData2(): String {
return withContext(Dispatchers.Default) {
Thread.sleep(1000)
"value2"
}
}
复制代码
查看日志
会发现,getData2()
和 getData1()
都是延迟1000ms的请求,若是用串行的方式来写,耗时确定超过2000ms。使用async()
耗时也才1051ms。
这里用
measureTimeMillis()
来计算代码耗时。
协程基本的使用到这里就能够告一段落了,主要介绍了协程给我带来了什么,能够在什么场景下用,怎么用。相信这样同步的方式来写异步,这样写出来的代码必定是很是直观、清晰的。
然而,有关什么是协程?有哪些详细的用法和细节?进程、线程和协程又有什么关系?留着后面后面再说。
Kotlin 官网 Kotlin Coroutines(协程) 彻底解析 Kotlin Primer·第七章·协程库 探究高级的Kotlin Coroutines知识 破解 Kotlin 协程
以上有错误之处,感谢指出