Kotlin入门系列:Coroutine协程

1 协程的概念和基本使用

1.1 什么是协程

协程 Coroutine 其实就是在Kotlin提供的一套线程API,让我们不用过多关心线程也可以方便的写出并发操作(即协程就是一套线程框架,让我们在Kotlin中方便的使用线程,方便的地方在于它能够在同一个代码块里进行多次的线程切换)。

1.2 协程的基本使用

在我们使用协程时,一般都会使用一个函数 launch() 创建一个协程,然后在 launch() 函数指定要切换的线程,比较常用的有 Dispatchers.IODispatchers.Main 表示后台线程和主线程。在 launch() 函数块中包裹的代码,就是协程。

// 切换到后台执行耗时操作
launch(Dispatchers.IO) {
	saveToDatabase(data)
}
// 切换到前台更新界面操作
launch(Dispatchers.Main) {
	updateViews(data)
}
// Thread
thread {
	...
}

而我们在实际开发中,更多的是网络请求执行耗时操作,然后获取到结果后再更新UI,线程切换上还可以使用 withContext() 进行线程的切换,执行完后会自动的把线程切换回来,这就是协程。

// 不要使用这种方式写协程,典型的回调地狱!!!
launch(Dispatchers.IO) {
	val user = api.getUser()
	launch(Dispatchers.Main) {
		nameTv.text = user.name
	}
}

// 正确的写法
// Coroutine协程强大的地方在于可以直接进行线程切换
launch(Dispatchers.Main) {
	// 使用withContext()切换线程处理后会自动将线程切换回来
	val user = withContext(Dispatchers.IO) {
		api.getUser() // 网络请求:后台线程
	}

	withContext(Dispatchers.IO) { ... }
	withContext(Dispathers.IO) { ... }	
	...
	
	nameTv.text = user.name // 更新UI:主线程
}

// 也可以将withContext()写成一个挂起函数抽离出来让代码更加具有可读性
launch(Dispatchers.Main) {
	val user = suspendingGetUser()
	nameTv.text = user.name
}

suspend fun suspendingGetUser() {
	withContext(Dispatchers.IO) {
		api.getUser()
	}
}

launch(Dispatchers.Main) {
	val avatar = async { api.getAvatar(user) } // 异步后台获取用户头像
	val logo = async { api.getCompanyLogo(user) } // 异步获取公司logo
	val merged = suspendingMerge(avatar, logo) // 合并两行结果
	show(merged) // 显示
}

2 suspend挂起

2.1 什么是协程的挂起

挂起的是什么?挂起的是协程。上面讲到协程就是那些被 launch() 函数包裹的代码,在代码执行到挂起函数的时候,协程就会被从当前线程挂起;当协程执行完操作后会自动的把线程切回到当前线程(简单理解挂起其实就是切了一个线程,具体说就是一个稍后会被自动切换回来的线程切换)。

launch(Dispatchers.Main) {
	// 被launch()包裹的代码就是协程,也就是下面的两句代码
	// 在执行到这个挂起函数suspendingGetImage()的时候,协程就会被从当前线程挂起
	// 说白了就是下面的两句代码从正在执行它的线程上脱离了,从suspendingGetImage()这句代码开始线程不再运行协程了,
	// 线程不理下面的两句协程,让协程自己去干自己的事情
	// 那协程干嘛去了?suspendingGetImage()它切换到一个后台线程Dispatchers.IO去调接口了
	// 那执行完之后呢?执行完之后它会自动的帮我们把线程自动切换回当前线程,从Dispatchers.IO切换到Dispatchers.Main
	val image = suspendingGetImage(imageId)
	avatarIv.setImageBitmap(image)
}

// 一个用关键字suspend声明的挂起函数
suspend fun suspendingGetImage(imageId: String) {
	witchContext(Dispatchers.IO) {
		api.getImage(imageId)
	}
}

2.2 suspend的作用

2.2.1 为什么suspend挂起函数要在协程或在另一个挂起函数中调用

在上面的代码中,我们在 launch() 中使用 suspendingGetImage() 的时候如果没有在函数中声明 suspend 关键字会在IDE报错,提示挂起函数要在协程中调用或者在另一个挂起函数中调用。

从上面的解释我们知道,挂起它是需要恢复 resume 的,是要在挂起函数切换线程后将线程切换回来,而恢复resume这个功能它是协程的,所以说如果一个挂起函数不在协程中被调用,那么它就没办法让我们在挂起函数切换线程后再将线程重新切换回来。

2.2.2 suspend关键字的作用是什么

suspend fun suspendingPrint() {
	println("Thread: ${Thread.currentThread().name}")
}

suspend fun suspendingGetImage(imageId: String) {
	withContext(Dispatchers.IO) {
		api.getImage(imageId)
	}
}

launch(Dispatchers.Main) {
	// 打印结果是Main主线程,因为你的挂起函数没有指定切换的线程,所以它还是执行在主线程
	// 也就是说,挂起函数被挂起的时候,不是在执行了suspendingGetImage()的时候就被挂起了,而是执行到withContext(Dispatchers.IO)切换线程的时候它才被挂起
	// 所以suspend关键字并不起到挂起协程的功能或切换线程的作用
	suspendingPrint() 
}

根据上面的分析,suspend 关键字并不起到挂起协程的功能或切换线程的作用,那么它到底是用来干嘛的?

suspend 关键字是用来提醒的,函数的创建者对函数的调用者的提醒。suspend 关键字声明的挂起函数是一个耗时操作,函数的创建者用挂起的方式将操作放在了后台运行,所以请函数的调用者在协程里调用这个函数。

我们在写Java的时候如果不留意里面的代码在主线程调用了一个函数操作,就会导致线程卡顿甚至ANR;如果我们在Kotlin中用 suspend 关键字声明了挂起函数提醒调用者这个函数是一个耗时后台执行的任务,就能避免这种在主线程调用耗时操作的问题。

3 非阻塞式挂起

3.1 什么是非阻塞式挂起

非阻塞式,其实就是不卡线程。无论是使用Kotlin的挂起函数切线程了,还是用Java的Thread切线程了,其实都是非阻塞式的。

所以协程的非阻塞式挂起,其实就是用阻塞的方式写出了非阻塞的代码而已,本质上耗时操作还是得切线程,更新界面还是得主线程(任何代码都是阻塞的,只是耗时操作的代码在人类感知中比较明显而已)。

3.2 协程和线程的关系

在Kotlin里,协程就是线程而实现的一套更上层的工具API框架,类似于Java的 Executors 和 Android的 Handler API。协程的本质还是线程。

4 总结

  • 协程就是切线程

  • 挂起就是可以自动切回来的切线程

  • 非阻塞式就是协程可以用看起来阻塞式的代码写出非阻塞的操作

你可能感兴趣的:(Kotlin)