【Kotlin】协程(一)——入门

1,523 阅读5分钟

介绍

这里一开始不打算介绍什么是协程,虽然标题叫介绍~~

为了方便理解,这边先做个比喻: 从使用的角度来看,Kotlin的协程像是“另一种RxJava”,但是比RxJava要高效。

这里先有个大致的印象,先了解下协程在实际中的作用,回头再去看它的原理,或许会更容易些。

一开始查了好多关于协程资料(包括官方完档),发现不同的人说的不大一样,最后越看越乱。于是我决定一开始先不说什么是协程。

作用

上面说到,协程用起来“像是另一种RxJava”。

那么是不是可以用协程来开启一个异步操作?切换线程? 答案是肯定的,不仅可以做到,而且写起来也很简单。下面看个栗子

栗子

举个例子,这里有个登录操作,需要用两个接口才能完成。 1、使用账号密码去获取token 2、通过token获取用户信息

很明显,这是个嵌套的请求。代码马上就浮现在脑海中,于是我们埋头“papapa”,很快就写出了这样的一段:

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:

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"

创建协程作用域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()得到的协程默认都是在主线程中。所以println("in main scope")是在主线程中运行的

withContext

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")
}

async

除了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()差不多啊?”

额~~ 用处大了,往下看

suspend

如果切换线程中的代码很多,想把(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 协程

以上有错误之处,感谢指出