Kotlin协程简介

1.  什么是协程

关于协程的定义有很多,在Kotlin语言中,协程比较合理的定义应该是一个线程框架(扔物线)或者说是一种并发设计模式(官方)。它是由官方设计的一套API方便开发者进行多线程开发。

2. 协程能干什么

协程主要用途总结下来就是可以用更优雅的代码实现多线程代码。主要体现在可以用同步的方式完成原来需要异步加回调才能完成的工作。举个简单例子,从服务器获取用户信息然后显示出来,常规写法如下:


    api.sendRequest(object : Callback {

        override fun onFail(err: Error) {

        }

        override fun onSucc(resp: String) {
            funOnUiThread {
                updateUI(resp)
            }
        }
    })    

上面是一个比较简单的例子,进行了两次嵌套,如果网络请求有依赖,那实现起来会更复杂:


        api.fetchUserInfo(object : Callback {
            override fun onFail(err: Error) {
                
            }
            override fun onSucc(resp: String) {
                api.fetchDetail(object : Callback {
                    override fun onFail(err: Error) {

                    }
                    override fun onSucc(resp: String) {
                        funOnUiThread {
                            updateUI(resp)
                        }
                    }
                })
            }
        })
    

下面我们用协程实现这个工作:

        launch(Dispatcher.Main) {
            val user = withContext(Dispatchers.IO) {
                api.fetchUserInfo()
            }
            var detail = withContext(Dispatchers.IO) {
                api.fetchUserInfo(useruser)
            }
            updateUI(detail)
        }

可以看到代码简洁了很多,上面的代码块我们可以理解为一个协程,这个代码块在主线程执行,当要请求用户信息时,切换到了IO线程去执行,因为网络请求是个耗时操作,所以IO线程被阻塞,等待网络请求结果。此时这个协程会挂起等待请求结果,但是它并不会阻塞UI线程,我们可以理解为它从当前执行它的线程(UI线程)脱离了,释放出了当前线程。当网络请求拿到结果,代码重新被挂载到UI线程继续执行。

从上面的代码我们可以看到,通过协程我们可以用看起来同步的代码完成实际上需要在不同线程切换的工作。

3. 协程的用法

1. 简单启动一个协程

GlobalScope.launch(Dispatchers.Main) {            
    Log.v("Tag", "Coroutines in Thread.currentThread().name")        
}

启动协程有launch和async两种方法,上面使用launch方法在主线程运行协程,系统主要提供了下面三个调度程序,Main是在主线程执行程序,通常做UI相关的工作,IO是在IO线程上运行程序,此线程做了专门优化,适用于磁盘和网络的I/O操作,Default是专门做了优化,适用于大量CPU运算的工作。

Dispatchers.Main        
Dispatchers.Default            
Dispatchers.IO

2. 切换线程

启动一个协程后,可以使用withContext()在不使用回调的情况下控制代码运行的线程池:

GlobalScope.launch(Dispatchers.Main) {
    Log.v("Tag", "xxxxxx1 in " + Thread.currentThread().name)
    withContext(Dispatchers.IO) {
        Thread.sleep(1000)
        Log.v("Tag", "xxxxxx2 in " + Thread.currentThread().name)
    }
    Log.v("Tag", "xxxxxx3 in " + Thread.currentThread().name)
}

上面的代码运行结果:

Tag: xxxxxx1 in main
Tag: xxxxxx2 in DefaultDispatcher-worker-1
Tag: xxxxxx3 in main

我们可以看到,代码在主线程运行,然后切到了IO线程,运行完毕后又切回主线程继续运行,这个过程并没有阻塞主线程,只是把协程挂起了。

3. suspend函数

上面切换线程的例子中,通过withContext()函数使当前协程挂起,但是如果想把切换线程后做的工作放到一个独立的函数中,需求声明此函数为suspend才可以。suspend函数只能在协程中或者另外一个suspend函数中调用,调用suspend函数时,协程会挂起,即从当前的线程脱离,等这个函数执行完毕再切回原线程继续执行。

GlobalScope.launch(Dispatchers.Main) {
    Log.v("Tag", "xxxxxx1 in " + Thread.currentThread().name)
    doIO()
    Log.v("Tag", "xxxxxx3 in " + Thread.currentThread().name)
}

suspend fun doIO() {
    withContext(Dispatchers.IO) {
        Thread.sleep(1000)
        Log.v("Tag", "xxxxxx2 in " + Thread.currentThread().name)
    }
}

4.  协程取消与超时

如果我们在一个页面启动了一个协程,当页面关掉时我们需要取消协程,要不然等协程切回主线程更新UI时就会出错,还有一个场景就是我们在做网络请求时,通常需要设置一个超时时间。下面是协程取消与超时的例子:

GlobalScope.launch(Dispatchers.Main) {
    var job = GlobalScope.launch(Dispatchers.Default) {
        repeat(10) {
            Log.v("Tag", "xxxxxx1 $it ")
            delay(500)
        }
    }
    delay(2000)
    job.cancel()
    //job.join()
}

上面是一个协程取消的例子,一个协程其实是一个job,调用cancel()后取消此协程。 下面的join()函数是一个suspend函数,它的意思是等待协程工作完成。也就是说等待job协程工作完成,当前协程再继续执行。

GlobalScope.launch(Dispatchers.Main) {
    var user = doIO()
    Log.v("Tag", "xxxxxx result $user")
}

suspend fun doIO(): String? {
    var res = withContext(Dispatchers.IO) {
        withTimeoutOrNull(4000) {
            delay(3000)
            "done"
        }
    }
    return res
}

上面是一个超时的例子,withTimeoutOrNull()函数表示,如果在指定的时间内完成了工作,就返回下面的结果("done"),如果未能完成的话会返回null。

5. async和await

前面讲过,协程在切线程后或者调用suspend函数后,虽然没有阻塞协程所在线程,但协程是被阻塞的(挂起),例如在一个协程内需要做两个不相关的请求,但是他们也是一个执行完毕再执行另外一个:

GlobalScope.launch(Dispatchers.Main) {            
    Log.v("Tag", "xxxx start")             
    var resp1 = doRequest1()
    var resp2 = doRequest2()
    Log.v("Tag", "xxxx $resp1, $resp2")
}

suspend fun doRequest1(): String? {
    delay(2000)
    return "resp1"
}

suspend fun doRequest2(): String? {
    delay(2000)
    return "resp2"
}

一个请求需求2秒的时间,那么这里2个请求就需要4秒的时间。这种情况很显然不合理,Kotlin可以使用async启动一个异步协程,并且可以通过一个挂起函数await()拿到返回结果,通过这种方式就可以实现多个协程并行工作:

GlobalScope.launch(Dispatchers.Main) {            
    Log.v("Tag", "xxxx start")             
    var defer1 = async {
        doRequest1()
    }
    var defer2 = async {
        doRequest2()
    }
    Log.v("Tag", "xxxx ${defer1.await()}, ${defer2.await()}")
}

suspend fun doRequest1(): String? {
    delay(2000)
    return "resp1"
}

suspend fun doRequest2(): String? {
    delay(2000)
    return "resp2"
}

上面通过async()启动一个协程,并且开始执行代码。async返回的是一个Deferred对象,它是Job的子类。

6. 协程扩展

GlobalScope的声明周期依赖于进程,进程结束才会退出。GlobalScope.launch的协程不能完成时需要手动cancel(),否则会造成内存泄露。为了方便使用,android提供了两个扩展  lifecycleScope  和 viewModelScope,自动绑定声明周期。在做开发时推荐使用 lifecycleScope 和 viewModelScope,使用前只需要引入下面依赖即可,lifecycleScope在Fragment和Activity中使用,viewModelScope在ViewModel中使用。

implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值