相信你们都对协程这个词颇有兴趣,网上也有大量介绍协程的文章,可是大多数都是介绍概念及理论的,不多看到有使用实际案例的文章,笔者刚看到协程的概念时也是欣喜不已,以为很是有用,很强大,能解决不少实际问题,可是总以为不知道该如何下手去运用到实际项目中去,所以打算专门撰写这篇协程实战的文章,经过一个HTTP网页URL页面内容的下载实践来说述解释协程的原理与使用,但愿能对你们学习使用并理解协程有所帮助。android
协程 - 轻量级线程
虽然Kotlin中使用线程已经很方便了,但仍是推荐使用协程代替线程。
协程主要是让原来要使用“异步+回调方式”写出来的复杂代码, 简化成能够用看似同步的方式写出来(对线程的操做进一步抽象)。 这样咱们就能够按串行的思惟模型去组织本来分散在不一样上下文中的代码逻辑,而不须要去处理复杂的状态同步问题,基本上也再也不须要接口处理代码了。安全
先来看看以下代码:网络
fun startCoroutine(name: String) {
println(" ### 1. Coroutine start in ${Thread.currentThread()}")
val c1 = GlobalScope.launch(Dispatchers.Default) {
println(" *** 2. ${name} launch start in ${Thread.currentThread()}")
delay(1000)
println(" *** 3. ${name} End of launch in ${Thread.currentThread()}")
}
println(" ### 4. Coroutine End. in ${Thread.currentThread()}")
}
startCoroutine("CO1")
复制代码
输出结果:多线程
### 1. Coroutine start in Thread[main,5,main]
### 4. Coroutine End. in Thread[main,5,main]
*** 2. CO1 launch start in Thread[DefaultDispatcher-worker-1,5,main]
*** 3. CO1 End of launch in Thread[DefaultDispatcher-worker-3,5,main]
复制代码
GlobalScope.launch(Dispatchers.Default) 用于启动协程。 从输出结果能够看出,启动协程以前,是在主线程中,可是协程启动后,协程的代码Block是在子线程中执行的。这不是重点,重点在于delay事后,协程的代码必定是在子线程执行的,哪怕launch指定了Unconfined参数,协程一开始将在主线程中执行,可是delay依然不会阻塞主线程,但它的确能够在指定的时间事后返回代码块继续执行后面的代码。这就是delay的强大之处,这个delay是不能够在协程外部的代码中调用的。app
协程调度器 | 功能描述 |
---|---|
Dispatchers.Default | 运行在 Dispatchers.Default 的线程池中 |
Dispatchers.Main | 运行在主线程中 |
Dispatchers.IO | 运行在 IO 线程中 |
Dispatchers.Unconfined | 运行在当前线程中 |
PS:以前低版本的那套launch/await 全局函数已经废弃,新版本必须使用GlobalScope.xxx。异步
协程的做用,就是让开发者感受是在多线程中工做同样,能够异步处理耗时操做,但实际上可能并无真正使用线程,而就在同一线程中切换。协程的切换是由编译器来完成的,于是开销很小,并不依赖系统资源,你能够开100000个协程,而没法启动100000个线程。async
delay跟线程的sleep很类似,都是延时一段时间,可是不一样点在于,delay不会阻塞当前线程,而是挂起协程自己,从而将线程资源释放出来,供其它协程使用。函数
咱们所必需要了解的是,在协程中,当你的耗时任务作完以后,你的代码极可能不在刚才的线程当中,此时必需要注意代码的线程安全问题,例如访问UI,你可使用runOnUiThread { }。学习
在startCoroutine的结尾处,可使用c1.join()来等待协程结束,一旦使用join,编译器便提醒必须添加suspend关键字,该函数也必须在协程中调用。测试
再来看看修改后的代码:
suspend fun startCoroutine(name: String) {
println(" ### 1. Coroutine start in ${Thread.currentThread()}")
val c1 = GlobalScope.launch(Dispatchers.Default) {
println(" *** 2. ${name} launch start in ${Thread.currentThread()}")
delay(3000)
println(" *** 3. ${name} End of launch in ${Thread.currentThread()}")
}
c1.join()
println(" ### 4. Coroutine End. in ${Thread.currentThread()}")
}
复制代码
该方法由于添加了suspend关键字,所以只能在协程中调用:
GlobalScope.launch(Dispatchers.Main) {
startCoroutine("CO1")
}
复制代码
输出结果以下:
### 1. Coroutine start in Thread[main,5,main]
*** 2. CO1 launch start in Thread[DefaultDispatcher-worker-2,5,main]
*** 3. CO1 End of launch in Thread[DefaultDispatcher-worker-3,5,main]
### 4. Coroutine End. in Thread[main,5,main]
复制代码
能够看到,代码中的日志顺序,是按一、二、三、4的顺序输出的了,join函数会等待协程结束。因为我指定了startCoroutine在Dispatchers.Main父协程中运行,所以当join等待子协程完成以后,又回到了主线程执行,这种方式来更新UI的话,都再也不须要使用runOnUiThread了,很适合用于作动画。
咱们经过一个网络URL加载Web数据的实例,来展现协程对于异步处理的强大之处。
首先,须要在build.gradle中添加:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
复制代码
在AndroidManifest.xml中添加:
<uses-permission android:name="android.permission.INTERNET" />
复制代码
新建一个UrlDownload类:
class UrlDownload {
// kotlin没有static方法,而是要使用伴生对象来替代
companion object {
suspend fun asyncDownload(url: String): String? {
return GlobalScope.async(Dispatchers.Default) {
download(url)
}.await()
}
fun download(url: String): String {
var urlConn : HttpURLConnection? = null
var strBuffer = StringBuffer()
var inputStream: InputStream? = null
var buffer: BufferedReader? = null
var inputReader: InputStreamReader? = null
try {
urlConn = URL(url).openConnection() as HttpURLConnection
inputStream = urlConn.getInputStream()
inputReader = InputStreamReader(inputStream)
buffer = BufferedReader(inputReader)
do {
var line = buffer.readLine()
strBuffer.append(line)
} while (line != null)
} catch (e: Exception){
e.printStackTrace()
} finally {
inputReader?.close()
buffer?.close()
inputStream?.close()
urlConn?.disconnect()
}
return strBuffer.toString()
}
}
}
fun startDownload() {
var url = "https://m.weibo.cn/"
GlobalScope.launch(Dispatchers.Default) {
var content = UrlDownload.asyncDownload(url) // 这是一个异步执行的耗时的操做
println(content)
}
}
复制代码
执行以上程序,在主线程调用startDownload()函数,能够看到控制台打印出了网页内容。请注意整个程序没有定义任何回调接口,但结果的确是在业务层打印出来的,阅读代码就好像是同步执行的同样,你也能够看的出,以上代码并不会阻塞主线程。
若是须要一层一层的往上传递,那么将startDownload作个简单改造便可:
suspend fun startDownload(url: String): String? {
return GlobalScope.async(Dispatchers.Default) {
UrlDownload.asyncDownload(url)
}.await()
}
fun appStartDownload() {
var url = "https://m.weibo.cn/"
GlobalScope.launch(Dispatchers.Default) {
var content = startDownload(url)
println(content)
}
}
复制代码
GlobalScope.launch 启动一个协程,并返回这个协程对象,咱们能够调用 join()来等待协程结束,join没有返回值。
而 await() 则有返回值,能够返回数据,要使用await(),必须使用GlobalScope.async来启动协程。再来看看上述启动代码的学习修改版本:
suspend fun startDownload(url: String): String? {
println("### 1. startDownload start in ${Thread.currentThread()}")
var r = GlobalScope.async(Dispatchers.Default) {
println(" ### 2. startDownload in ${Thread.currentThread()}")
UrlDownload.asyncDownload(url)
println(" ### 3. startDownload in ${Thread.currentThread()}")
}.await()
println("### 4. startDownload End. in ${Thread.currentThread()}")
return "### startDownload TEST ###"
}
复制代码
输出结果以下:
### 1. startDownload start in Thread[DefaultDispatcher-worker-1,5,main]
### 2. startDownload in Thread[DefaultDispatcher-worker-2,5,main]
### 3. startDownload in Thread[DefaultDispatcher-worker-3,5,main]
### 4. startDownload End. in Thread[DefaultDispatcher-worker-3,5,main]
复制代码
从日志能够看出,虽然日志顺序也是严格按照代码中一、二、三、4的顺序执行的,可是4号日志跟1号日志已经不在同一个线程,而是跟3号日志在同一个线程。这就是异步等待await的结果,因此该方法必须使用suspend关键字,告诉编译器这个是协程函数,必须在协程中调用。否则随意切换客户代码的线程,确定要出乱子的。这就是协程的关键,也是协程的强大之处,可是越是强大的东西,使用时必定要知道它的特色,虽然使用起来很简单。
刚才的代码,有一个费解的地方:
suspend fun startDownload(url: String): String? {
return GlobalScope.async(Dispatchers.Default) {
UrlDownload.asyncDownload(url) // 其实await是将这行代码的返回结果做为返回值了
}.await()
}
复制代码
那么细心的同窗可能会问,若是我在这里写了两行代码呢?既然是实战学习,固然不能放过这个问题,继续编写学习测试代码:
suspend fun startDownload(url: String): String? {
return GlobalScope.async(Dispatchers.Default) {
UrlDownload.asyncDownload(url)
UrlDownload.asyncDownload("https://www.xxx.com/")
"### 返回值 ###"
}.await()
}
复制代码
测试发现,await会将最后一个表达式的值做为返回值,而前面的多个asyncDownload都会执行,并且是顺序执行,缘由是asyncDownload内部自己也使用了协程await()来等待,咱们把那个协程叫子协程,启动子协程的协程叫父协程。那么若是咱们但愿两个下载任务可以同时并行进行呢,固然有办法,那就是再启动一个新的父协程去执行UrlDownload.asyncDownload便可,要知道kotlin中是能够启动100000个协程的,上线只受内存限制。
至此,相信读者对于协程的概念、使用都能很好的理解了,测试代码就再也不贴出来了,有兴趣的同窗能够自行编写代码来验证,以加深理解。
Kotlin快速入门 - 安卓开发新趋势,Java转Kotlin开发,花一天时间就够了 以前写的一篇文章,可是不知怎么在掘金发布不了,因而附上一个简书连接。