前言
使用kotlin有一段时间了,但在自己入门协程的时苦于小白一看就懂的资料太少,入门很艰难。所以俺就暗暗下决心一定要以一贯的秒懂作风填补上国内这个空白,一来可以帮助我们可爱的猿猿们,二来也可以加深自己对协程的理解。至于更加高级的用法,那就是入门后的修炼了,能练到第几层全凭天赋和努力了…
阅读本文后你会有如下收获:
- 对协程有一个清楚的理解,认识可能不太深刻,但是你会非常清楚,至于深度就需要靠你后天发育了。
- 清楚协程用来解决什么问题
- 掌握kotlin协程的入门用法
- 增强你的求学信心
简介
要学习协程,首先应该明白它是用来解决什么问题的?其次要明白它到底是什么?最后才是基于某种语言的实现和使用方法。再次强调一下,知道使用某个知识来解决某个问题,比掌握了这个知识而不知道其有什么用更加重要!
协程解决什么问题
简单来说:协程主要用来解决异步编程问题。
异步编程是一个非常大的话题,简单来说就是不阻塞程序。大白话就是别卡,我们现在干什么都怕卡:打游戏怕卡、听音乐怕卡、看电影怕卡、秒杀商品怕卡…。从上古时期程序员鼻祖就已经在和它缠斗了,经过几代程序员的努力发展出了几个对付它的大招:
- Threading 多线程
- Callbacks 回调
- Futures, Promises 这两个一般不翻译
- Reactive Extensions Rx系列, 例如rxjava
- Coroutines 协程
以上方法的详情可以参考Asynchronous Programming Techniques
有的同学产生了好奇:当我手机网络不好时,我的微信页面上一直在转菊花,这是不是由于腾讯码农太菜没有使用异步编程导致的呢?腾讯码农:纳尼?屏幕上不是有个菊花在转嘛,哪里卡了?难不成还的给你放一段提前下载好的日本小电影?你还说你喜欢欧美的?就一菊花,爱看不看…
什么是协程
先上个比较学术的定义吧,如果只是为了应用而理解的话,我觉得还是后面白话解释更适合:
协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程
Coroutines = Co + Routines , Co是Cooperation,即协作;Routines 的意思是Functions,即方法。连起来就是多个方法互相协作来完成一个任务,这些方法可以被暂停(suspend)也可以被恢复(resume)。协程其是以线程为基础的,可以看做是一个线程的管理框架。
协程最初被提出是在20世纪60年代,我的妈呀可真够久远的,可以说是IT上古时期的产物了,此时就连大名鼎鼎的C语言还没出生呢。当时协程之所有市场主要是因为计算机操作系统调度算法不成熟,没有发展出抢占式调度算法,而主要使用协作式算法,再加上不支持多线程的缺陷,使得协程找到了自己的位置。计算机一次只能干一件事,假设你现在要一边听音乐(TaskA)一边敲代码(TaskB),那最好的做法就是执行一会A,然后让出执行时间给B,执行一会B再把CPU时间让给A。那要是A突然有一天发疯不配合,老子今天就是不配合,就是不让出执行时间,那么B任务肯定被阻塞了,就是卡了。。。
所以A和B就好比两个Routines,需要Cooperation 来共同完成一个任务,简称Coroutines。后来抢占式算法加上多线程的出现,协程就被打入冷宫了,这一打就是50多年啊,最近几年由于多线程高并发中存在的一些问题,例如线程太重了,而且很多时候还不够用,它又换发了第二春。
实例
让我们举个程序上的例子吧,毕竟你是程序猿,代码才是你最好的语言
有个方法getAndShowName()
作用为:调用网络Api然后将内容打印出出来,如下所示
suspend fun getAndShowName(){
val name= requestApi()
changeUi(name)
}
调用
getAndShowName()
doOtherTask()
因为网络请求requestApi()
是耗时操作,如果同步执行的话程序会被阻塞,因为changeUi(name)
要等requestApi()
的结果返回才能继续执行,changeUi(name)
等结果是正常的,因为有依赖,但是getAndShowName()
下面的doOtherTask()
不依赖getAndShowName()
结果,所以不应该被阻塞。
协程是这么干的:当执行到requestApi()
时,发现是耗时方法,就把包含它的getAndShowName()
方法suspend,让线程去执行其后面的内容,当网络请求完成后再resume方法getAndShowName()
继续执行其里面的 changeUi()
方法,给人的感觉就是代码就这么顺序执行下来了…
优势
- 可以以同步的方式写异步代码
- 轻量级,在服务器端表现的非常抢眼,但是在客户端,例如Android,中意义一般
- 编程模型和API和普通方法一样。
这一点我非常喜欢,不用学习新的东西,却掌握了一种新的技能,想想你入门RxJava时候那个痛苦,大部分人现在也没有完全掌握RxJava的那些API.
Kotlin中的协程
在理解了上面的内容后,就是Kotlin协程登场的时候了:
基本概念
要想上手Kotlin协程必须清楚几个概念
-
协程scope
协程代码都必须包含在一个范围内,就好比说:只有在这个圈里可以使用协程。这个scope用于跟踪管理其内部的协程。例如:协程标准库中的
GlobalScope
、Android的viewModelScope
-
协程builder
其协程代码与非协程代码的桥梁。其负责在非协程代码里启动协程,即在协程scope里build一个协程代码块例如:
launch
与async
-
协程dispatcher
它负责具体执行线程。使用协程builder构建了协程后,总的有人去执行啊,这个任务就是由dispatcher完成的,它会调度线程来运行协程代码。例如:
Dispatchers.Default
、Dispatchers.IO
,Dispatchers.Main
清楚以上3个概念就可以轻松上手协程写代码了,一段协程代码,以上3个角色缺一不可。
协程初体验
我第一次写协程代码的的时的切身体验是这样的:老子要写协程了…咦,我日,怎么写呢?就是完全不知道怎么下笔那种感觉,看了一堆别人的博客,还是迷迷糊糊…
所以我现在要分步骤教你如何下笔,等你真的入门了,后天发育全看你的操作骚不骚了…
其实写一个协程就简单的3步:
第一步:找一个协程scope,自己定义(实现CoroutineScope
接口)或者使用库中已经提供的。
我这里使用Android jetpack中的viewModelScope
第二步:选择一个协程builder
我这里选择launch
第三步:选择一个协程dispatcher
我这里选择Dispatchers.IO
在scope实例上调用builder,将dispatcher作为builder的参数即可
viewModelScope.launch(Dispatchers.IO) {
//这里可以调用suspend函数,也可以调用普通函数
val name= requestApi()
changeUi(name)
}
就这样我就构建并启动了一个协程,我们可以在里面执行协程方法了,就是那些使用suspend
标记的普通方法。
//耗时网络请求
suspend fun requestApi():String{
delay(2_000)
return "ShuSheng007"
}
fun changeUi(name:String){
println("欢迎:$name")
}
难吗?so easy 有没有?让我们按照上面的步骤在Android中演示一下:
协程在Android中的实战
我们在android中使用kotlin协程实现一个获取网络数据并展示的一个小Demo。
先看一下效果图,从网络上获取信息并展示,同时不阻塞UI动画。
第一步:引入协程相关的库
//协程相关
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
//viewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
第二步:在ViewModel中编写网络请求
class CoroutinesViewModel : ViewModel() {
//用于更新UI
val nameLiveData = MutableLiveData<String>()
//Main-safe, 意味着可以直接从UI线程启动
fun checkTheWomen() {
// Dispatchers.Main可以省略
viewModelScope.launch(Dispatchers.Main) {
//这块指定网络请求使用IO线程
val name = withContext(Dispatchers.IO) {
searchFromNet()
}
nameLiveData.value = name
}
}
//耗时网络请求
private suspend fun searchFromNet(): String {
Log.d("coroutine","searchFromNet: ${Thread.currentThread().name}")
delay(3_000)
return "ShuSheng007媳妇"
}
}
第三步:在Activity中发起请求
class CoroutinesActivity : AppCompatActivity() {
private lateinit var viewModel: CoroutinesViewModel
private lateinit var tvWomanName: TextView
private lateinit var btnSearch: Button
override fun onCreate(savedInstanceState: Bundle?) {
...
viewModel = ViewModelProvider(this).get(CoroutinesViewModel::class.java)
...
viewModel.nameLiveData.observe(this) {
//当网络请求结果回来后更新UI
tvWomanName.text = it
...
}
btnSearch.setOnClickListener {
//从UI线程发起网络请求
viewModel.checkTheWomen()
...
//播放图片动画,证明UI没有被阻塞
animateImage(findViewById<ImageView>(R.id.img_my_wife))
}
}
fun animateImage(img: ImageView) {
...
}
}
让我复盘一下这个简单的程序是如何执行的,因为这真的太重要了,理解了这块,后期发展就会顺利很多。
- 点击查询按钮,在UI线程调用
checkTheWomen()
并发起UI动画,如果此方法是同步的话,UI动画会被阻塞 -
checkTheWomen()
启动协程,并在IO线程上调用方法searchFromNet()
,由于其是是suspend
的,随即在UI线程上被挂起,UI线程继续去执行动画 - 在UI线程上挂起的方法
searchFromNet()
找了一条IO线程执行网络请求,执行完后带着结果被在UI线程上恢复执行 - 于是在UI线程上设置LiveData,通知到Activity去更新那个名字
可以看到在Android中使用协程非常的简单舒服,因为Android主推kotlin,主推协程,通过Jetpack项目一切都给你整的明明白白的…