1. 先看一个下载图片的示例
传统代码
// 1.创建一个子线程,下载图片
Thread{
val bitmap = getImage("http://baidu.com/美女图片")
// 2.图片下载完成,切换到主线程
runOnUIThread{
// 3.把图片设置到控件上
imageView.bitmap = bitmap
}
}.start()
协程代码
// 1. 通过scope 启动一个协程
GlobalScope.launch(Dispatchs.Main){
// 2. 调用挂起函数,下载图片
val bitmap = getImage("http://baidu.com/美女图片")
// 3. 把图片设置到Ui上
imageView.bitmap = bitmap
}
可以看到协程的代码没有回调嵌套,可读性更好
getImage
函数的实现
suspend fun getImage(url:String):Bitmap {
// 切换到IO线程
return Dispathchs.IO {
// 下载图片
HttpUtil.get(url).toBitmap()
}
}
2.下面讲讲协程的优势
2.1. 提升代码可读性,解决Callback hell问题
看一个弹对话框的例子
业务要求:
先弹出一个权限提示的对话框,
关闭后,再弹出一个登录的对话框
关闭后,再弹出一个提示签到的对话框
传统代码
Dialog("1.提示申请权限")
.doOnDismiss{
Dialog("2.提示登录")
.doOnDismiss{
Dialog("3.提示签到")
.doOnDismiss{
}
.show()
}
.show()
}
.show()
协程版本
GlobalScope(Dispatchs.Main){
showDialog("提示申请权限")
showDialog("提示登录")
showDialog("提示签到")
}
showDialog
代码
suspend fun showDialog(title:String) = suspendCoroutine{cont->
Dialog(title)
.doOnDismiss{
cont.resume(Unit)
}
.show()
}
通过回调转supsend函数的操作,让原来的回调嵌套代码变的线性
2.2. 保证函数的线程安全,并保证调用顺序
这是一个显示加载对话框的函数
普通版本
fun showLoading(context:Context){
LoadingDialog(context).show()
}
showLoading
只能在主线程被调用,在其他的线程被调用会导致崩溃,因为只有主线程才能操作UI
比较通用的解决办法是这样
fun showLoading(context:Context){
// 增加线程检查
if(!isMainThread())
throw NotMainThreadException()
}
LoadingDialog(context).show()
}
这种fail-fast 的实践,测试排查错误很有用,但是有时候也会有漏网之鱼,造成线上崩溃。
也可以这样改
fun showLoading(context:Context){
runOnUIThread {
LoadingDialog(context).show()
}
}
这样虽然也可以解决问题,让dialog,一定在主线程弹出,但是会有调用顺序的问题
showLoading(context)
println("dialog显示出来了")
期望是先弹出dialog,再打印日志,实际上是先打印日志再弹出dialog。
协程版本
suspend fun showLoading(context:Context){
Dispatchs.Main{
LoadingDialog(context).show()
}
}
GlobalScope.launch{
showLoading(context)
println("dialog显示出来了")
}
协程版本有三项保证:
- 保证了dialog一定是在主线程弹出的
-
showLoading
函数是可以在任意线程被调用的 - 保证了先弹出dialog,再打印日志,也就是方法执行顺序
到这里,你可以会较真,我用锁也可以实现,CountDownLatch
不就可以。
fun showLoading(context:Context){
val latch = CountDownLatch(1)
runOnUIThread {
LoadingDialog(context).show()
latch.countDown()
}
latch.await()
}
这样不就可以了,也实现了三项保证。
但是,你阻塞了线程,kotlin是挂起,挂起后,调用showloading
的线程还可以执行别的任务,如果是阻塞,就不可以了,这就是协程相比线程的性能优势。如果这里面没懂,没关系,本文的第3部分还会再讲解这点。
并不是其他线程框架做不到三项保证,同时保证性能,比如Rxjava就可以
再来个Rxjava版本的
fun showLoading(context:Context):Completable{
return Completable
.fromAction{ LoadingDialog(context).show() }
.SubscribeOn(AndroidSchedulers.Main)
}
fun test(){
showLoading(context)
.subscribe{
println("dialog显示出来了")
}
}
这样也可以实现和协程一样的效果,具有三项保证,且不阻塞线程,但是代码可读性就比较差了
对比可以发现:
- Rxjava实现的版本具有三项保证,并且没有阻塞线程,但是代码极度不直观。
- CountDownLatch实现的版本也具有三项保证,但是阻塞了线程,性能不如Rxjava的版本
- Coroutine实现的版本具有三项保证,不阻塞线程,而且代码比其他两种更加直观
2.3 挂起比阻塞高效
挂起的本质是基于事件循环机制,让线程离开当前函数,去执行别的函数,合适的时候再回到这里来执行
阻塞的本质,就是卡住线程
不是说使用协程比使用线程要高效,而是如果做相同的事情用到的线程越少,越高效。
所以不阻塞,就可以在挂起的这段时间,让线程干别的事情,这样就比阻塞使用的线程小,就更高效了。
所谓高效,指的是协程,非阻塞,提高了线程的利用率。
3. 总结一下协程的优势
- 代码直观: 回调变为suspend 函数,有效解决了回调嵌套问题,且比Rxjava的观察者模式更易懂
- 线程安全: suspend 函数可以在任意线程执行,内部通过withContext()切换到指定的线程
- 高性能:通过挂起而不是阻塞,利用较少的线程,可以执行更多的任务。
- 代码执行顺序: 通过suspend 函数保证了异步代码具有和同步代码一样的执行顺序