一、Kotlin协程概念
Kotlin简单来说是一种轻量级的线程,是一种在能够在编程语言级别实现不同‘线程’的切换,准确的说是协程之间的切换。而对于线程而言,不同的线程之间切换是由操作系统来执行的。协程允许我们在单线程的情况下模拟多线程编程效果,代码在执行时的挂起和恢复完全是由编程语言来控制的。这里的挂起的意思就是CPU不给时间片去执行这块的代码,转而去执行其他的线程或协程中的代码。
二、Kotlin创建协程
fun main(args: Array<String>) {
coroutine1()
}
fun coroutine1(){
// delay 是一个特殊的挂起函数 它不会造成线程阻塞 但会挂起协程 并且只能在协程作用域中使用
GlobalScope.launch { // 在后台启动一个新的协程并继续
delay(1000L) //非阻塞的等待 1秒钟(默认时间单位是毫秒)
println("hello world")
}
println("hello ") // 协程已在等待时主线程还在继续
Thread.sleep(2000) // 阻塞主线程2 秒钟保证 JVM存活
}
上面 GlobalScope.launch() 函数就可以创建一个协程作用域,GlobalScope.launch() 每次创建的都是一个*协程,这种协程当应用程序结束的时候就会跟着一起结束,所以为了将协程中代码执行,可以用 Thread.sleep 函数将主线程阻塞,保证JVM存活,进而使得协程有时间去执行代码。delay 是一个挂起函数,只能在协程作用域中调用,而 GlobalScope.launch 就创建了一个协程作用域,因此可以在其中调用。
fun main(args: Array<String>) {
coroutine2()
}
fun coroutine2(){
println("hello, ") // 协程已在等待时主线程还在继续
runBlocking { // 这个表达式阻塞了主线程 调用runBlocking 的主线程会一直 阻塞到runBlocking 内部的协程执行完毕
delay(2000) // 我们延迟了2秒来保证JVM的存活
println("coroutine finished...")
}
println("main..")
}
输出如下:
hello,
coroutine finished...
main..
runBlocking 函数同样也会创建一个协程作用域,但是它可以保证协程作用域内所有的代码和子协程在没有执行结束之前会一直阻塞当前线程,所以可以看到 main 输出是在 coroutine finished 之后。注意:runBlocking 函数通常只是在测试环境中使用,在正式的环境中会有性能问题,原因是会阻塞当前线程,而如果这个线程是主线程,那么就会出现问题。
上面都介绍了协程的相关概念,但是协程的好处在哪里,协程的好处在并发编程的应用场景下,可以在单线程中得到并发线程的运行效果。
fun coroutine3(){
repeat(100000){
Thread{ run {
Thread.sleep(100)
print(".")
}}.start()
}
println("main...")
}
fun coroutine4(){
runBlocking {
repeat(100000){
launch {
delay(100)
print(".")
}
}
}
println("main...")
}
可以通过对比以上两段代码,可以得出明显的结论就是协程确实比线程可以更快的执行“多线程”操作。上面的 launch 函数可以创建协程,这个launch 函数与GlobalScope.launch 函数不同,launch 函数必须在协程作用域中调用。
三、Kotlin 挂起函数
挂起函数顾名思义就是可以让CPU挂起该函数,让CPU去执行其他部分代码,后面再恢复执行。前面我们利用 launch函数起了一个协程,并在里面写了一些业务代码,如果这些业务代码变得更加复杂之后,我们需要将这些代码抽成一个函数提取出来,但是 launch 函数是提供了一个协程作用域的,提取出来的函数不具有协程作用域,那么业务代码中的 delay 挂起函数就不能够正常调用,为此kotlin提供了一个suspend 关键字,使用它就可以将普通的函数声明为挂起函数,挂起函数之间是可以相互调用的。
// 提取函数重构
fun main() {
runBlocking {
launch {
doWorld()
}
println("Hello ")
}
}
// 挂起程序
suspend fun doWorld() {
delay(1000L)
println("world")
}
三、coroutineScope 函数
coroutineScope 函数的特点是会继承外部的协程作用域并创建一个新的作用域,借助此特点,可以给任意挂起函数提供协程作用域。coroutineScope 类似于runBlocking 函数,保证其作用域内的素有代码和子协程在没有执行结束之前一直会阻塞当前协程。
fun main() {
runBlocking {
coroutineScope {
launch {
for (i in 0..5){
println(i)
delay(1000)
}
}
//launch 不阻塞当前协程
println("launch..")
}
//coroutineScope 阻塞当前协程直到协程中的代码或者子协程执行结束
println("coroutineScope finished...")
}
println("main finished...")
}
输出:
launch..
0
1
2
3
4
5
coroutineScope finished...
main finished...
四、作用域构建器
前面学习了GlobalScope.launch,runBlocking,launch, coroutineScope 都是作用域构建器,但是 GlobalScope.launch 和 runBlocking 是可以在任意地方调用,而 launch 只能在协程作用域中调用,coroutineScope 只能在协程作用域或者挂起函数中调用。前面已经学习知道,runBlocking 由于会阻塞线程,这里只建议在测试中使用,而GlobalScope.launch 由于每次都创建顶层协程,一般也不太建议使用,除非明确要创建顶层协程。
协程的取消,无论 GlobalScope.launch 函数还是 launch 函数都会返回 Job对象,只要调用 Job对象的 cancel 方法就可以取消协程。
1. 取消顶层协程
fun main() {
//注意:GlobalScope.launch是顶层协程,所以不需要嵌套在其他协程作用域中
val job = GlobalScope.launch {
delay(1000)
println("GlobalScope.launch finished...")
}
job.cancel() // 取消了协程
Thread.sleep(4000)
println("main finished...")
}
2. 取消一般子协程
fun main() {
runBlocking {
val job = launch {
delay(2000)
println("launch finished...")
}
job.cancel()
println("runBlocking finished...")
}
println("main finished...")
}
3. 实际项目中比较常用的写法
fun main() {
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
println("launch1....")
doSomeThings()
}
scope.launch {
println("launch2...")
readSomethings()
}
println("...")
Thread.sleep(4000)
job.cancel() // 在主线程阻塞了4秒之后,取消由scope所创建的所有协程
println("main...")
}
suspend fun doSomeThings(){
for (i in 0..5) {
delay(1000)
println("doSomethings $i")
}
println("doSomethings finished...")
}
suspend fun readSomethings(){
delay(5000)
println("readSomethings finished...")
}
输出:
launch1....
...
launch2...
doSomethings 0
doSomethings 1
doSomethings 2
main...
这种写法的好处就是所有调用 CoroutineScope 的 launch 函数所创建的协程,都会被关联到Job对象的作用域下面。这样只需要调用一次 cancel() 方法,就可以将同一作用域内的所有协程全部取消,从而大大降低了协程的管理成本。注意:这里用的 Thread.sleep() 函数是为了模拟主线程的情况,不让程序立即终止,在正式的Android开发过程中,我们不能这么写。
在实际开发过程中,CoroutineScope() 函数更常用,而 runBlocking 函数只是在一些测试场景下比较适用,比如在 main 函数中写一些测试用例。
五、aysnc 函数
上面我们已经学习了如何在协程中进行逻辑处理,但是如果要在协程中对结果进行返回,应该怎么处理?Kotlin 为我们提供了 async 函数, async 函数必须在协程作用域中才能调用,它会创建一个新的子协程并返回一个Deferred对象 (Deferred 即推迟的意思),如果要想获取async 函数代码块的执行结果,只需要调用 Deferred对象的 await 方法即可,代码如下:
fun main() {
runBlocking {
val finalResult = async {
val a = 10
val b = 20
val result = a+b
result
}.await()
println("final result $finalResult")
}
}
输出:
final result 30
实际上,async 函数在调用后代码块中的代码会立即执行。而 await 函数可以阻塞当前协程直到协程中的代码执行结束,所以我们可以利用这一特性进行一些多任务的结果的合并操作,比如处理两个计算逻辑代码块,并最终获取两个代码块结果之和,代码如下:
fun main() {
runBlocking {
val start = System.currentTimeMillis()
val result1 = async {
delay(1000)
val a = 10
val b = 20
val result = a+b
result
}.await()
println("final result $result1")
val result2 = async {
delay(1000)
4+5
}.await()
println("final result ${result1+result2}")
val end = System.currentTimeMillis()
println("total spentTime ${end-start}")
}
}
输出如下:
final result 30
final result 39
total spentTime 2023
从运行的过程来看,这两个子协程的是按照顺序来执行的,符合 async 函数阻塞当前协程的概念,注意这里的当前协程指的是 runBlocking。但是上述写法也有问题,它是不能进行并行运算的,必须是前一个执行完成之后下一个才能开始执行,如果要同时执行是否可以并缩短执行的时间呢?可以的,我们可以执行代码,再通过 await 函数获取结果,代码如下:
fun main() {
runBlocking {
val startTime = System.currentTimeMillis()
val deferred1 = async {
delay(1000)
val a =10
val b = 20
val result = a+b
result
}
val deferred2 = async {
delay(1000)
4+5
}
println("result ${deferred1.await()+deferred2.await()}")
val endTime = System.currentTimeMillis()
println("total spentTime ${endTime-startTime}")
}
}
输出如下:
result 39
total spentTime 1020
效果是比较明显的,时间从原来的2秒缩短到1秒,两个协程在运行上的时间关系由串行变成并行,由此可见await 是可以阻塞当前协程并直到协程执行结束为止。同时,我们一般在获取结果的时候可以先执行后获取结果。
六、WithContext
一种比较特殊的协程作用域构建器:withContext() 函数,withContext() 函数是一个挂起函数,大体可以看成 async 函数的一种简写版本。
fun main() {
runBlocking {
val result = withContext(Dispatchers.Default){
delay(1000)
10+4
}
println(result)
}
}
输出:
14
从运行和输出结果来看,withContext() 确实和 async 差不多一样都会可以阻塞当前协程直到协程内部运行结束。但是有一点不同于 async,withContext 函数可以指定线程参数,线程参数的意思就是说可以指定协程运行在哪种线程中,kotlin 提供了三种线程参数,分别是 Dispathchers.Default 、Dispathcers.IO 、Dispatchers.Main。那么什么情况下需要指定这些参数呢?我们都知道在Android应用的网络请求过程是不能放在主线程中的,而是需要在工作线程中执行,那么如果是运行在主线程中的协程呢?也是不可以的。在请求网络的情况下可以指Dispathcers.IO来切换到工作线程,执行结束之后又通过WithContext函数(设置线程参数 Dispatchers.Main) 切回到主线程的协程中来。Dispatchers.Default 一般用于计算密集型的业务处理,Dispatchers.IO 一般用于较高并发的线程策略,要执行的代码大多数是在阻塞或者等待中,比如网络请求或者文件读写等耗时操作。Dispathcers.Main 一般表示不会开启子线程,而是在Android主线程中执行代码。
我们以Android网络请求为例:
1. 先添加 Okhttp 依赖
//OkHttp
implementation 'com.squareup.okhttp3:okhttp:4.2.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.8.1'
2. 在清单文件中添加网络请求权限
<uses-permission android:name="android.permission.INTERNET"/>
3. 网络请求代码
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch {
sendHttpRequest()
}
}
private val TAG = "MainActivity"
private suspend fun sendHttpRequest(){
coroutineScope {
val result = async {
val okHttpClient = OkHttpClient()
val requestBuilder = Request.Builder()
val request = requestBuilder.get().url("https://www.baidu.com/").build()
val response = okHttpClient.newCall(request).execute()
println("response ${response.code}")
if (response.code == 200){
response.body?.string()
} else{
"some error happens"
}
}.await()
withContext(Dispatchers.Main){
println("Thread ${Thread.currentThread().name}")
println("data $result")
tv_result.text = result
}
}
}
}
以上就是网络请求的相关写法,但是有问题,在没有网络的情况会崩溃, 进行如下改造:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch {
sendHttpRequest()
}
}
private val TAG = "MainActivity"
private suspend fun sendHttpRequest() {
coroutineScope {
val result = async {
val okHttpClient = OkHttpClient()
val requestBuilder = Request.Builder()
val request = requestBuilder.get().url("https://www.baidu.com/").build()
val data: String?
data = try {
val response = okHttpClient.newCall(request).execute()
println("response ${response.code}")
if (response.code == 200){
response.body?.string()
} else {
"some error happens"
}
} catch (e: Exception){
e.message
}
data
}
withContext(Dispatchers.Main){
tv_result.text = result.await()
}
}
}
}
这样就比较好的解决了请求中的错误异常。
七、使用协程简化回调的方法
在以往的网络编程过程中,我们总是回写很多的回调处理,这样看起来代码很是复杂,现在 Kotlin 提供了协程就可以帮我们简化这样复杂的写法。借助 suspendCoroutine() 函数就能将传统的回调机制大大简化。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val job = Job()
val coroutine = CoroutineScope(job)
coroutine.launch {
//这么写的好处就是不用重复写回调了,我们只需要在协程里面进行一次回调处理即可
//否则,在一般情况下我们有几次请求就需要写几次回调处理,很是麻烦,这里就一次即可。不管是请求成功与否,所有的最终结果都是在协程中的挂起函数进行处理。
val result = sendBaiDuRequest()
withContext(Dispatchers.Main){
tv_result.text = result
}
}
}
private val TAG = "MainActivity"
private suspend fun sendBaiDuRequest():String {
try {
val data = request("https://www.baidu.com/")
println("ThreadName ${Thread.currentThread().name} -> data $data")
return data;
} catch (e: Exception) {
Log.e(TAG, "onCreate: ${e.message}")
}
return ""
}
//简化回调的写法
suspend fun request(url:String) :String {
return suspendCoroutine { continuation->
sendGetRequest(url, object : HttpCallBackListener{
override fun one rror(e: Exception) {
continuation.resumeWithException(e)
}
override fun onFinish(data: String?) {
continuation.resume(data!!)
}
})
}
}
private fun sendGetRequest(address:String, listener: HttpCallBackListener) {
val okHttpClient = OkHttpClient()
val builder = Request.Builder()
val request = builder.get().url(address).build()
okHttpClient.newCall(request).enqueue(object :Callback{
override fun onResponse(call: Call, response: Response) {
if (response.code == 200){
val data = response.body?.string()
listener.onFinish(data)
} else{
listener.onError(RuntimeException("Server error"))
}
}
override fun onFailure(call: Call, e: IOException) {
listener.onError(e)
}
})
}
interface HttpCallBackListener {
fun onFinish(data: String?)
fun one rror(e:Exception)
}
}
suspendCoroutine() 函数被调用之后当前协程就立即被挂起,而lambda表达式中的代码则会在普通线程中执行。continuation.resume() 函数是恢复被挂起的协程,并传入请求回来的数据,该值会成为 suspendCoroutine() 函数的返回值。如果请求失败,则调用 continuation.resumeWithException() 函数恢复挂起协程,并传入具体的异常原因,该原因会继续向上抛,直到在某个挂起函数中进行异常处理。