什么是协程
kotlin中的协程可以把它理解成是一种轻量级的线程,也可以把它理解成是一个线程框架,它的引入可以让开发以一种非并发的编码方式来实现并发的需求。简化异步编程,程序的异步逻辑可以借助协程来进行顺序的表达。
协程和线程的区别
- 线程是由系统调度的,线程的切换或阻塞开销都比较大。协程依赖于线程,但是协程的挂起时不需要阻塞线程。
- 线程的执行和结束是由操作系统来调度,但是协程是可以手动控制它的执行和结束
如何使用协程
添加依赖
kotlin并没有把协程放入其标准库的API中,为此我们想要使用协程的功能时,需要手动添加如下依赖,第二个是android开发需要的,此处我们也要加上。
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1"
创建协程
协程的创建主要有以下几种方式,下面一一介绍:
1. GlobalScope.launch
每次创建的都是一个顶层协程,这种协程当应用程序运行结束时也会跟着一起结束.
fun testGlobalScope () {
GlobalScope.launch {
println("hello in coroutines")
}
//Thread.sleep(1000)
}
以上代码不会有任何输出, 因为打印语句没有机会运行。如果将当前线程sleep 1秒种,会有打印信息输出。
再来看以下代码:
fun testGlobalScope () {
GlobalScope.launch {
println("hello in coroutines")
/**
* delay()函数是一个非阻塞式的挂起函数,它只会挂起当前协程
*/
delay(1000)
println("world in coroutines")
}
}
同样,这部份代码也不会有输出,虽然加了delay,但是因为其在协程内部。实际运行的情况是该协程都没有机会运行。
2. runBlocking
runBlocking函数同样会创建一个协程的作用域,但是它可以保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。需要注意的是,runBlocking函数通常只应该在测试环境下使用,在正式环境中使用容易产生一些性能上的问题。
3. launch
launch 必须运行在协程环境中,它会在当前协程的作用域下创建子协程。子协程的特点是如果外层作用域的协程结束了,该作用域下的所有子协程也会一同结束。
runBlocking {
launch {
println("hello")
delay(1000)
println("word")
}
launch {
println("hello2")
delay(1000)
println("word2")
}
}
4.sync
async函数必须在协程作用域当中才能调用,它会创建一个新的子协程并返回一个Deferred对象,如果我们想要获取async函数代码块的执行结果,只需要调用Deferred对象的await()方法即可。
调用了async函数之后,代码块中的代码就会立刻开始执行。当调用await()方法时,如果代码块中的代码还没执行完,那么await()方法会将当前协程阻塞住,直到可以获得async函数的执行结果
runBlocking {
var start = System.currentTimeMillis();
var result1 = async {
delay(1000)
5 + 5
}.await()
var result2 = async {
delay(1000)
5 + 5
}.await()
println("result is ${result1 + result2}.")
var end = System.currentTimeMillis();
println("cost ${end - start} ms.")
}
5. coroutineScope
coroutineScope函数也是一个挂起函数,因此可以在任何其他挂起函数中调用。它的特点是会继承外部的协程的作用域并创建一个子协程,借助这个特性,我们就可以给任意挂起函数提供协程作用域。
suspend fun printHello2()= coroutineScope {
launch {
println("Hello")
delay(1000)
}
}
coroutineScope函数和runBlocking函数还有点类似,它可以保证其作用域内的所有代码和子协程在全部执行完之前,外部的协程会一直被挂起。但是coroutineScope函数只会阻塞当前协程,既不影响其他协程,也不影响任何线程,因此是不会造成任何性能上的问题的。而runBlocking函数由于会挂起外部线程,如果你恰好又在主线程中调用它的话,那么就有可能会导致界面卡死的情况,所以不太推荐在实际项目中使用。
withContext
可以用withContext来切线程,如下:
runBlocking {
var result = withContext(Dispatchers.Default) {
5 + 5
}
println(result)
}
withContext必须运行在协程作用域或者suspend修饰的挂起函数中。
在Android中,最典型的应用场景莫过于在子线程中去处理耗时操作,然后切到主线程中来更新UI
withContext(Dispatchers.IO) {
// 切换IO线程
}
// .. 在UI线程执行
withContext(Dispatchers.IO) {
// 切换IO线程
}
// .. 在UI线程执行
withContext(Dispatchers.IO) {
// 切换IO线程
}
// .. 在UI线程执行
挂起函数
当协程作用域中的代码逻辑越来越复杂,需要将代码提取到一个单独的函数中。suspend关键字声明的函数就是挂起函数,它表明该函数的函数体中是有协程作用域的。
没用suspend修饰的方法不能参与协程任务,suspend修饰的方法只能在协程中只能与另一个suspend修饰的方法交流。
- 挂起函数只能在协程里面调用, 不能在协程外面调用
- 挂起函数和其它函数的本质区别是挂起函数是异步返回。
协程的终止
在启动协程后,如果想终止一个协程可以调用cancel方法。
var job = launch {
for (i in 1..10) {
println(i)
delay(1000)
}
}
job.cancel()
launch函数在执行后,会返回一个Job对象,如果取消,可以调用cancel方法。
当我们在用async方法后,返回的是一个Deferred, 它其实也是继承自Job, 也是可以直接调用cancel方法。
var deferred = async {
delay(1000)
5 + 5
}
deferred.cancel()
//defered.await()
在调了cancel方法后,再调用await方法会报错。
协程和OKHttp
当我们来用协程和OKHttp配合起来进行网络通信时,我们的代码也可以非常简洁,如下:
class WanAndroidFetcher {
suspend fun getBanner(): NetResult<List<Banner>> {
val request = Request.Builder()
.url("https://www.wanandroid.com/banner/json")
.build()
val response = OkHttpClient().newCall(request).await()
// If the network request wasn't successful, throw an exception
if (!response.isSuccessful) throw HttpException(response)
return withContext(Dispatchers.IO) {
response.body!!.use { body ->
Gson().fromJson(body.string(), object : TypeToken<NetResult<List<Banner?>?>?>() {}.type)
}
}
}
}
我们定义一个OkHttpExtensioins.kt文件,用来扩展Call类,增加了一个await方法, 其代码如下:
suspend fun Call.await(): Response = suspendCancellableCoroutine { continuation ->
enqueue(
object : Callback {
override fun onResponse(call: Call, response: Response) {
continuation.resume(response) {
// If we have a response but we're cancelled while resuming, we need to
// close() the unused response
if (response.body != null) {
response.closeQuietly()
}
}
}
override fun onFailure(call: Call, e: IOException) {
continuation.resumeWithException(e)
}
}
)
continuation.invokeOnCancellation {
try {
cancel()
} catch (t: Throwable) {
// Ignore cancel exception
}
}
}
在调用时:
private fun sendRequestWithOkHttp() {
runBlocking {
var data = WanAndroidFetcher().getBanner()
println("data: $data");
}
}
协程与retrofit
主线程不能发网络请求(协程也不例外)
我们知道在android 6.0之后,就不允许在主线程中发送网络请求了。如果你这么做了,一定会遇一个android.os.NetworkOnMainThreadException异常。我们定义如下接口:
interface NetApi {
@GET("banner/json")
suspend fun getBannerInfo():NetResult<List<Banner>>
@GET("banner/json")
fun getBannerNoSuspend(): Call<NetResult<List<Banner>>>
companion object {
private const val BASE_URL = "https://www.wanandroid.com/"
private var service: NetApi? = null
fun getApi(): NetApi {
if (null == service) {
val client = OkHttpClient.Builder().build()
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
service = retrofit.create(NetApi::class.java)
}
return service!!
}
}
}
我们在主线程中调用
runBlocking { NetApi.getApi().getBannerNoSuspend().execute() }
会发现程序报错了,抛出了android.os.NetworkOnMainThreadException, 虽然上面用runBlocking创造了一个协程的环境,但getBannerNoSuspend方法的返回值是一个Call对象,调用该实例的execute方法并不会涉及到线程的切换,因此还是在主线程中运行,最终的结果仍然后报错。
如果我们在主线程中调用的是
runBlocking {
var data = NetApi.getApi().getBannerInfo()
println("data: ---> " + data)
}
网络请求发送成功,并且有正确的返回,这是因为retrofit在底层帮我们切换了线程。
注意: retrofit的版本必须是2.6或以上,否则是不支持suspend,在使用时会报错Could not locate call adapter for class java.lang.Object
协程 + retrofit + LiveData组合
当前的Android开发官方提倡用mvvm架构,这里我们采用jetpack中的ViewModel来写一个Demo, 此处我们省略了repository层,直接在ViewModel中发起网络请求。代码如下:
class ResourceViewModel: ViewModel() {
private var bannerInfo = MutableLiveData<Result<NetResult<List<Banner>>>>()
fun getBannerInfo( bannerType: Int) {
viewModelScope.launch{
var result = try {
Result.success(NetApi.getApi().getBannerInfo())
} catch (e: Exception) {
e.printStackTrace()
Result.failure(e)
}
bannerInfo.postValue(result)
}
}
}
在ViewModel中起一个协程环境可以用viewModelScope.launch。
采用协程+retrofit处理网络请求
因为想了解一下采用协程去处理网络请求的最佳实践,最近看了不少开源项目中这一块的实现,总体来讲其做法主要分为以下几类:
suspend函数
基于retrofit 2.6 版本
也就是我们上面举例的使用方式
@GET("banner/json")
suspend fun getBannerInfo():NetResult<List<Banner>>
在使用时, 直接用如下代码,非常的方便
NetApi.getApi().getBannerInfo()
采用这种方式,需要我们的retrofit版本在2.6及以上
async + Deferred
基于retrofit 2.4 版本
@GET("banner/json")
fun getBanner(): Deferred<NetResult<List<Banner>>>
使用时(retrofit会帮我们去切线程):
var data = async {
NetApi.getApi().getBannerAsync().await()
}
println("data: ---> " + data.await().data)
在创建retrofit实例时,需要指定CoroutineCallAdapterFactory这种方式,仅作了解,现在也很少使用
val client = OkHttpClient.Builder().build()
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
service = retrofit.create(NetApi::class.java)
retrofit + flow
kotlin中的flow可以取代rxjava的功能,而flow的使用也非常的简单, 我们的仓库层可能是这么写的(直接调发送网络请求)
class UserRepository(private val service: WanAndroidService) {
suspend fun loginFlow(userName: String, password: String) = service.login(userName, password)
}
如果我们想将网络请求用flow包一下,方便后期数据的处理,可以这么写
class UserRepository(private val service: WanAndroidService) {
suspend fun loginFlow(userName: String, password: String) = flow {
var data = service.login(userName, password)
emit(data)
}
}
当然,如果项目中并没有复杂的数据流操作,完全可以不用引入flow, 直接用协程 + LiveData就可以处理业务需求。