协程1- 协程的优势

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显示出来了")
}

协程版本有三项保证:

  1. 保证了dialog一定是在主线程弹出的
  2. showLoading函数是可以在任意线程被调用的
  3. 保证了先弹出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 函数保证了异步代码具有和同步代码一样的执行顺序

你可能感兴趣的:(协程1- 协程的优势)