介绍
这里一开始不打算介绍什么是协程,虽然标题叫介绍~~
为了方便理解,这边先做个比喻: 从使用的角度来看,Kotlin的协程像是“另一种RxJava”,但是比RxJava要高效。
这里先有个大致的印象,先了解下协程在实际中的作用,回头再去看它的原理,或许会更容易些。
一开始查了好多关于协程资料(包括官方完档),发现不同的人说的不大一样,最后越看越乱。于是我决定一开始先不说什么是协程。
作用
上面说到,协程用起来“像是另一种RxJava”。
那么是不是可以用协程来开启一个异步操作?切换线程? 答案是肯定的,不仅可以做到,而且写起来也很简单。下面看个栗子
栗子
举个例子,这里有个登录操作,需要用两个接口才能完成。 1、使用账号密码去获取token 2、通过token获取用户信息
很明显,这是个嵌套的请求。代码马上就浮现在脑海中,于是我们埋头“papapa”,很快就写出了这样的一段:
代码语言:javascript复制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");
}
});
}
});
是的,确实没什么问题。不过没觉得这要的代码很长吗?
于是我们改用lambda简写,或是kotlin:
代码语言:javascript复制reqToken{ //请求token
reqUserInfo(it){ //通过token,获取用户信息
Logger.i("login success")
}
}
nice,瞬间简洁了好多
确实简洁了很多。不过还是难逃嵌套结构,如果多来几层,最后可能成了这样:
蜜汁嵌套
看得头皮发麻~~
但是!!!,若果用协程就不一样了(划重点)
代码语言:javascript复制coroutineScope.launch {
val token = getToken()
val userInfo = getUserInfo(token)
Logger.i("login success")
}
不仅代码少,而且可以用同步的方式来写异步!!!
往下再了解一点?
使用
知道到了他的优(niu)秀(bi)之处,下面来看看是怎么用的
因为是Kotlin的协程,所以项目需要支持Kotlin。怎么支持就不用我说了吧? (不要问我,我不会,因为那是另一个同事做的。hahaha~~~)
倒入依赖
gradle倒入协程依赖
代码语言:javascript复制implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
创建协程作用域CoroutineScope
可以直接new一个MainScope
val mainScop = MainScope()
注意记得在销毁的时候调用cancel()
,调用cancel()
后用mainScope
启动的协程都会取消掉。
override fun onDestroy() {
super.onDestroy()
mainScope.cancel()
}
创建一个协程
常用的方式有两种:launch()
、async()
,下面分别来说明他们的用途。
当然还有其他的创建方式,这里就不说了。
launch
使用launch()
创建一个协程,会返回一个Job
对象。
val job = mainScope.launch {
//在协程中的操作
Logger.i("launch end")
}
很简单,mainScope.launch{}
就能创建一个协程。
看下打印的日志,发现这个协程时在主线程中运行的。
"这有什么用?在主线程中运行的协程?那我再里面做耗时操作,是不是会卡住?"
确实,如果直接这样用是会阻塞主线程的。所以这时候,就需要用到withContext()
mainScope
这个作用域内的调度器是基于主线程调度器的。也就是说,mainScope.launch()
得到的协程默认都是在主线程中。也可以直接创建CoroutineScope
指定对应的调度器。
withContext
withContext()
:用给定的协程上下文调用指定的暂停块,暂停直到完成,然后返回结果。也就是说,可以用来切换线程,并返回执行后的结果。
常用的有
Dispatchers.Main
:工作在主线程中
Dispatchers.Default
:将会获取默认调度器(子线程)
Dispatchers.IO
:IO线程
Dispatchers.Unconfined
:是一个特殊的调度器(说实话,我没搞懂他的用法~~)
这里子线程中请求一个token,然后回到主线程中:
代码语言:javascript复制mainScope.launch {
val token = withContext(Dispatchers.Default) {
Logger.i("get token")
val token = api.getToken()//注意!!!这是个同步的网络请求
token
}
Logger.i("token $token")
}
再来看下日志
有withContext()后,线程的切换显得是那么简单。只要你开心,可以切来切去。
代码语言:javascript复制mainScope.launch {
withContext(Dispatchers.Default) {
Logger.i("切到子线程")
}
withContext(Dispatchers.Main) {
Logger.i("切到主线程")
}
withContext(Dispatchers.IO) {
Logger.i("切到IO线程")
}
Logger.i("launch end")
}
async
除了launch()
,还有个常用的方法——async()
,async()
和launch()
相似。不同的是他可以返回协程执行结束后值。
async()
返回的是一个Deferred
对象,需要通过Deferred#await()
得到返回值。
还是上面的例子,子线程中请求一个token,然后回到主线程中:
代码语言:javascript复制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()差不多啊?”
额~~ 用处大了,往下看
suspend
suspend:申明这是个可挂起的函数,里面可以用协程的一下方法(launch()、async()、withContext()等)。
如果切换线程中的代码很多,想把(withContext(){...}
)的代码抽出来。于是写成这样
fun getToken(): String {
return withContext(Dispatchers.Default) {
//同步请求得到token
val token = api.getToken()
token
}
}
BUT,并不能这样用,发现编译器报错了:
发现withContext()
只能在协程或suspend
方法中使用。所以,在方法前加上suspend
就不会报错了。
suspend fun getToken(): String { ... }
实际应用
有了协程,写异步的代码将会方便很多。
串行的请求
回到一开始的栗子,请求token,然后用token请求UserInfo
代码语言:javascript复制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()
来计算代码耗时。
总结
协程基本的使用到这里就可以告一段落了,主要介绍了协程给我带来了什么,可以在什么场景下用,怎么用。相信这样同步的方式来写异步,这样写出来的代码一定是非常直观、清晰的。
然而,有关什么是协程?有哪些详细的用法和细节?进程、线程和协程又有什么关系?留着后面再说。