Kotlin 之旅7 协程

####协程的基本概念

概念:各个子任务协作运行,解决了异步问题。

相对于线程的抢占式的调度,协程是协作运行的,不存在抢占式的调度。

有关于更详细的介绍参考这篇文章:

www.jianshu.com/p/d4a8358e8…

www.wendq.com/wd/201702/1…

#####支持协程的语言

Lua、C#、Kotlin等等

#####协程要解决的问题

异步的问题,例如图片的加载,然后在UI线程回调。

通过类似于同步的代码来实现异步的操作,简化了异步代码。协程是一种轻量级的并发方案,不像线程占用很多的系统资源,它只是一块内存,保存了挂起的位置。

#####Kotliin对协程的支持

1.1开始支持,目前还是实验性的API。但是目前已经趋于稳定了,大家可以放心学习使用。

#####那么如何支持协程呢?

  1. 编译器对suspend函数的编译支持,编译器会对其进行特殊处理
  2. 标准库API的支持
  3. kotlinx.coroutine框架的支持

#####我们需要

我们需要掌握基本的API,了解协程的运行原理,了解kotlinx.coroutine框架。

相关的核心API如下:

创建协程,并不会马上执行
createCoroutine()

创建并且开始执行协程(如果没有创建的话)
startCoroutine()

挂起协程
suspendCoroutine

运行控制类,负责结果和异常的返回
Continuation接口,有resume和resumeWithException两个方法

运行上下文,可以持有资源参数,运行调度,配合ContinuationInterceptor篡改Continuation,从而切换线程
CoroutineContext接口

协程控制拦截器,与CoroutineContext配合处理协程调度
ContinuationInterceptor接口
复制代码

####协程例子1.0--不使用协程

下面以加载网络图片为例子讲解一下协程的使用,下面先来看看不使用协程的时候吧。

这个例子我们要使用JFrame来显示图片,用Retrofit2.0来加载网络图片数据。

先在build.gradle脚本里面加上Retrofit:

dependencies {
	//Retrofit2.0相关依赖库
    compile 'com.squareup.retrofit2:retrofit:2.3.0'
    compile 'com.squareup.retrofit2:converter-gson:2.3.0'
}
复制代码

下面我们通过object单例来对外提供网络访问的HttpService服务:

object HttpService {

    val service by lazy {
        Retrofit.Builder()
                .baseUrl("http://www.imooc.com")
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(API::class.java)
    }

}
复制代码

其中API是我们的获取网络图片的API:

interface API {
    @GET
    fun getLogo(@Url fileUrl: String): Call<ResponseBody>
}
复制代码

接下来需要创建一个窗口,因为这里要用到Java的相关API,因此我们继承JFrame,并且对外提供按钮监听以及设置图片的方法:

class MainWindow : JFrame() {

    private lateinit var button: JButton
    private lateinit var image: JLabel

    fun init() {
        button = JButton("点我获取慕课网Logo")
        image = JLabel()
        image.size = Dimension(200, 80)

        contentPane.add(button, BorderLayout.NORTH)
        contentPane.add(image, BorderLayout.CENTER)
    }

    fun onButtonClick(listener: (ActionEvent) -> Unit) {
        button.addActionListener(listener)
    }

    fun setLogo(logoData: ByteArray) {
        image.icon = ImageIcon(logoData)
    }
}
复制代码

接下来开始正式编写main函数了:

const val LOGO_URL = "/static/img/index/logo.png?t=1.1"

fun main(args: Array<String>) {

	//初始化窗口
    val frame = MainWindow()
    frame.title = "Coroutine@Bennyhuo"
    frame.setSize(200, 150)
    frame.isResizable = true
    frame.defaultCloseOperation = EXIT_ON_CLOSE
    frame.init()
    frame.isVisible = true

	//设置监听
    frame.onButtonClick {
        HttpService.service.getLogo(LOGO_URL)
                .enqueue(object : Callback<ResponseBody> {
                    override fun onResponse(call: Call<ResponseBody>?, response: Response<ResponseBody>?) {
						//使用apply之后,其内部就可以直接调用成员函数了
                        response?.apply {
                            if (isSuccessful) {
								//获取网络图片的输入流,通过readBytes读取字节数据
                                val imageData = body()?.byteStream()?.readBytes()
								//如果没有数据,那么抛出异常
                                if (imageData == null) {
                                    throw HttpException(HttpError.HTTP_ERROR_NO_DATA)
                                } else {
									//如果正常,那么显示到窗口上面,但是需要通过SwingUtilities的invokeLater方法,从IO线程切换到UI线程
                                    SwingUtilities.invokeLater {
                                        frame.setLogo(imageData)
                                    }
                                }
                            }
                        }
                    }

                    override fun onFailure(call: Call<ResponseBody>?, t: Throwable?) {
						//获取图片失败,直接抛出异常
                        throw HttpException(HttpError.HTTP_ERROR_UNKNOWN)
                    }
                })
    }

}
复制代码

其中HttpException是我们自定义的一种异常:

object HttpError{
    const val HTTP_ERROR_NO_DATA = 999
    const val HTTP_ERROR_UNKNOWN = 998
}

data class HttpException(val code: Int): Exception()
复制代码

总结一下,我们不使用协程的时候,就是我们平常的写法,代码有点恶心。

####协程例子2.0--协程,非异步版本

协程是不会帮我们切换线程的,我们先来一个非异步的版本吧。首先需要定义一个协程方法,注意需要加上suspend关键字:

/**
 * 开始协程
 */
fun startCoroutine(block: suspend () -> Unit) {
    block.startCoroutine(BaseContinuation())
}
复制代码

其中BaseContinuation是我们自定义的类:

class BaseContinuation : Continuation<Unit> {
    override val context: CoroutineContext = EmptyCoroutineContext

    override fun resume(value: Unit) {

    }

    override fun resumeWithException(exception: Throwable) {

    }

}
复制代码

然后,需要定义一个下载图片的方法,在这个方法里面执行了图片下载工作,注意线程是不会切换的:

/**
 * 加载图片
 */
suspend fun startLoadImage(url: String) = suspendCoroutine<ByteArray> { continuation ->
    log("下载图片")
    try {
        val responseBody = HttpService.service.getLogo(url).execute()
        responseBody.apply {
            if (isSuccessful) {
                body()?.byteStream()?.readBytes()?.let(continuation::resume)
            } else {
                continuation.resumeWithException(HttpException(responseBody.code()))
            }
        }
    } catch (e: Exception) {
        continuation.resumeWithException(e)
    }
}
复制代码

最后,在main函数中就可以这样调用了,十分简洁明了:

frame.onButtonClick {
    log("协程之前")
    startCoroutine {
        log("协程开始")
        val imageData = startLoadImage(LOGO_URL)
        log("拿到图片")
        frame.setLogo(imageData)
    }
    log("协程结束")
}
复制代码

其中,上面的log方法是这样定义的:

val dateFormat = SimpleDateFormat("HH:mm:ss:SSS")

val now = {
    dateFormat.format(Date(System.currentTimeMillis()))
}

fun log(msg: String) = println("${now()} [${Thread.currentThread().name}] $msg")
复制代码

最终打印出来的结果如下:

12:44:40:823 [AWT-EventQueue-0] 协程之前
12:44:40:828 [AWT-EventQueue-0] 协程开始
12:44:40:830 [AWT-EventQueue-0] 下载图片
12:44:51:340 [AWT-EventQueue-0] 拿到图片
12:44:51:372 [AWT-EventQueue-0] 协程结束
复制代码

可以看到,协程默认情况下是不会帮我们切换线程的,是顺序执行的。

####协程例子3.0--协程,异步版本

在2.0版本的基础之上,我们加入异步代码:

定义一个AsyncTask:

private val pool by lazy {
    Executors.newCachedThreadPool()
}

class AsyncTask(val block: () -> Unit) {
    fun execute() {
        pool.execute(block)
    }
}
复制代码

接下来需要有线程切换,因此我们定义一个包装类UiContinuationWrapper:

class UiContinuationWrapper<T>(val continuation: Continuation<T>) : Continuation<T> {
    override val context: CoroutineContext = EmptyCoroutineContext

    override fun resume(value: T) {
        SwingUtilities.invokeLater {
            continuation.resume(value)
        }
    }

    override fun resumeWithException(exception: Throwable) {
        SwingUtilities.invokeLater {
            continuation.resumeWithException(exception)
        }
    }
}
复制代码

在使用的时候,通过AsyncTask包裹实现异步,通过UiContinuationWrapper包装实现线程切换:

/**
 * 加载图片
 */
suspend fun startLoadImage(url: String) = suspendCoroutine<ByteArray> { continuation ->
    log("异步任务开始前")

    val uiContinuationWrapper = UiContinuationWrapper(continuation)

    AsyncTask {
        try {
            log("加载图片")
            val responseBody = HttpService.service.getLogo(url).execute()
            responseBody.apply {
                if (isSuccessful) {
                    body()?.byteStream()?.readBytes()?.let{
                        SwingUtilities.invokeLater {
                            continuation.resume(it)
                        }
                    }
                } else {
                    uiContinuationWrapper.resumeWithException(HttpException(responseBody.code()))
                }
            }
        } catch (e: Exception) {
            uiContinuationWrapper.resumeWithException(e)
        }
    }.execute()
}
复制代码

####协程例子4.0--协程,异步版本加强版

上面的例子里面我们直接篡改了continuation,我们也可以通过Context来实现:

先来实现一个AsyncContext,对continuation进行拦截,篡改:

class AsyncContext : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {

    override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
        return UiContinuationWrapper(continuation.context.fold(continuation) { continuation, element ->
			//如果不是自身,以及具有拦截能力,那么就用UiContinuationWrapper进行包装篡改;
			//否则的话,直接返回
            if (element != this && element is ContinuationInterceptor) {
                element.interceptContinuation(continuation)
            } else continuation
        })
    }

}
复制代码

然后实现一个ContextContinuation,目的就是为了在实例化的时候可以传入自定义的Context:

class ContextContinuation(override val context: CoroutineContext = EmptyCoroutineContext) : Continuation<Unit> {

    override fun resume(value: Unit) {

    }

    override fun resumeWithException(exception: Throwable) {

    }

}
复制代码

最后,在使用的时候,只需要在startCoroutine的时候初始化一次,以后就不用每次都篡改了:

/**
 * 开始协程
 */
fun startCoroutine(block: suspend () -> Unit) {
    block.startCoroutine(ContextContinuation(AsyncContext()))
}

/**
 * 加载图片
 */
suspend fun startLoadImage(url: String) = suspendCoroutine<ByteArray> { continuation ->
    log("异步任务开始前")

    AsyncTask {
        try {
            log("加载图片")
            val responseBody = HttpService.service.getLogo(url).execute()
            responseBody.apply {
                if (isSuccessful) {
                    body()?.byteStream()?.readBytes()?.let {
                        SwingUtilities.invokeLater {
                            continuation.resume(it)
                        }
                    }
                } else {
                    continuation.resumeWithException(HttpException(responseBody.code()))
                }
            }
        } catch (e: Exception) {
            continuation.resumeWithException(e)
        }
    }.execute()
}
复制代码

####协程例子5.0--协程,异步版本加强版封装

主要的封装就是把加载图片的代码抽取出来,封装一个专门协程异步调用的耗时操作executeTask方法:

/**
 * 开始协程
 */
fun startCoroutine(block: suspend () -> Unit) {
    block.startCoroutine(ContextContinuation(AsyncContext()))
}

/**
 * 耗时操作
 */
suspend fun <T> executeTask(block: () -> T) = suspendCoroutine<T> { continuation ->
    AsyncTask {
        try {
            continuation.resume(block())
        } catch (e: Exception) {
            continuation.resumeWithException(e)
        }
    }.execute()
}


/**
 * 加载图片
 */
fun startLoadImage(url: String): ByteArray {
    log("加载图片")
    val responseBody = HttpService.service.getLogo(url).execute()
    if (responseBody.isSuccessful) {
        responseBody.body()?.byteStream()?.readBytes()?.let {
            return it
        }
        throw HttpException(HttpError.HTTP_ERROR_NO_DATA)
    } else {
        throw HttpException(responseBody.code())
    }
}
复制代码

通过这样的封装之后,startLoadImage就变成了简单的函数了,可以用普通的方式来调用。(注意suspend方法需要在suspendCoroutine内使用)

在调用的时候,也比较简单:

frame.onButtonClick {
   startCoroutine{
       executeTask {
           startLoadImage(LOGO_URL)
       }
   }
}
复制代码

####解决线程安全问题

在下面的代码中,startLoadImage方法是异步执行的,而当传入的LOGO_URL是var可变类型的话,就可能会导致线程安全问题(外部数据共享问题):

frame.onButtonClick {
   startCoroutine{
       executeTask {
           startLoadImage(LOGO_URL)
       }
   }
}
复制代码

解决的办法就是,通过Context去携带URL:

class DownloadContext(val url: String) : AbstractCoroutineContextElement(Key) {
    companion object Key : CoroutineContext.Key<DownloadContext>
}
复制代码

接下来修改协程的代码:

/**
 * 开始协程,需要传进来一个context,例如上面自定义的DownloadContext。Context都是可以叠加的。
 */
fun startCoroutine(context: CoroutineContext = EmptyCoroutineContext, block: suspend () -> Unit) {
    block.startCoroutine(ContextContinuation(context + AsyncContext()))
}

/**
 * 耗时操作,给block添加一个Receiver为CoroutineContext,相当于扩展的Lambda表达式
 */
suspend fun <T> executeTask(block: CoroutineContext.() -> T) = suspendCoroutine<T> { continuation ->
    AsyncTask {
        try {
            continuation.resume(block(continuation.context))
        } catch (e: Exception) {
            continuation.resumeWithException(e)
        }
    }.execute()
}
复制代码

UiContinuationWrapper里面的context应该从continuation中获取:

class UiContinuationWrapper<T>(val continuation: Continuation<T>) : Continuation<T> {
    override val context: CoroutineContext = continuation.context

    override fun resume(value: T) {
        SwingUtilities.invokeLater {
            continuation.resume(value)
        }
    }

    override fun resumeWithException(exception: Throwable) {
        SwingUtilities.invokeLater {
            continuation.resumeWithException(exception)
        }
    }
}
复制代码

最后调用的代码如下:

frame.onButtonClick {
    startCoroutine(DownloadContext(LOGO_URL)) {
        executeTask {
            startLoadImage(this[DownloadContext]!!.url)
        }
    }
}
复制代码

通过这样就可以解决了线程安全的问题。

######Tpis:关于协程的异常捕获,直接在外面try/catch即可。

####协程简单原理分析

协程实际上会被编译器编译成状态机,suspend函数即为状态转移,整个协程的执行过程如下:

其中,正常的结果通过resume返回,异常的结果通过resumeWithException抛出,完整的协程的执行过程如下:

有关于更深入的原理分析可以去研究编译出来的字节码以及调试程序观看调用栈。

####协程的例子--序列生成器

fun main(args: Array<String>) {

	//每次迭代的时候,就会将挂起的协程执行(yield),返回结果,然后重新挂起    
	for (i in fibonacci) {
        println(i)
        if (i > 100) {
            break
        }
    }
}

//生成斐波那契懒数列
val fibonacci = buildSequence {
    yield(1)
    var current = 1
    var next = 1

    while (true) {
        yield(next)
        val tmp = current + next
        current = next
        next = tmp
    }
}
复制代码

####Kotlinx.coroutine框架介绍

Kotlinx.coroutine框架是官方对协程在不同平台下的封装,基本的模块如下:

有兴趣可以到GitHub找相关的代码学习。

如果觉得我的文字对你有所帮助的话,欢迎关注我的公众号:

我的群欢迎大家进来探讨各种技术与非技术的话题,有兴趣的朋友们加我私人微信huannan88,我拉你进群交(♂)流(♀)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值