1.线程就是线程,

2.协程本质就是一种线程框架,仅仅是针对Java中的Thread做了一次更友好的封装。让我们更方便的使用Java中的线程才是Kotlin-JVM中协程的真正目的。

本质上和Handler,AsyncTask,RxJava 基本是一致的。只不过Kotlin中的协程对于切换线程比他们更方便一些。这其中最核心的是suspend这个Kotlin协程中的关键字。suspend就是挂起的意思,挂起就是切换线程 没其他作用,最多就是切到其他线程以后还可以自动切回来,避免过多的callback,

所有被suspend标记的函数 要么在协程里被调用,要么在其他挂起函数里被调用,否则就无法实现 切走以后又可以切回来的效果

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_main)

GlobalScope.launch(Dispatchers.Main) {

getInfo()

getInfoNoContext()

Log.v("wuyue", "我又切回来了 in thread " + Thread.currentThread().name)

}

}

/**

* 挂起就是切换线程 没其他作用,最多就是切到其他线程以后还可以自动切回来,避免过多的callback

* 所有被suspend标记的函数 要么在协程里被调用,要么在其他挂起函数里被调用,否则就无法实现

* 切走以后又可以切回来的效果

*/

suspend fun getInfo() {

/**

* withContext挂起函数 内部实现了挂起的流程,suspend其实并没有这个功能

* kotlin中有很多挂起函数,withContext 应该是最常用的

*/

withContext(Dispatchers.IO) {

Log.v("wuyue", "getInfo in thread " + Thread.currentThread().name)

}

}

/**

* 这个函数 虽然用suspend标记 但是并没有 用withContext 指定挂起,

* 所以是没办法实现切线程的作用的,自然而然也就无法实现 所谓的挂起了

* 个人理解这个suspend关键字的作用就是提醒 调用者注意 你如果调用的是一个被suspend标记的函数

* 那么一定要注意 这个函数可能是一个后台任务,是一个耗时的操作,你需要在一个协程里使用他。

* 如果不在协程里使用,那么kotlin的编译 就会直接报错了。

*

*

* 这点其实对于android来讲还是很有用的,你所有认为耗时的操作都可以用suspend来标记,然后在内部指定

* 这个协程的thread 为 io thread, 如果调用者没有用launch来 call 这个方法,那么编译就报错。

* 自然而然就避免了很多 主线程操作io的问题

*

*/

suspend fun getInfoNoContext() {

Log.v("wuyue", "getInfoNoContext in thread " + Thread.currentThread().name)

}

}

这段代码很简单,可以多看一下注释。很多人都会被所谓Kotlin协程的非阻塞式吓到,其实你就理解成Kotlin中所宣传的非阻塞式,无非是用阻塞的写法来完成非阻塞的任务而已。

试想一下,我们上述Kotlin中的代码 如果用Thread来写,就会比较麻烦了,甚至还需要用到回调(如果你不用handler的话)。这一点上Kotlin 协程的作用和RxJava其实是一致的,只不过Kotlin做的更彻底,比RxJava更优雅更方便更简洁。

考虑一种稍微复杂的场景,某个页面需要2个接口都返回以后才能刷新展示,此种需求,如果用原生的Java concurrent并发包是可以做的,但是比较麻烦,要考虑各种异常带来的问题。

比较好的实现方式是用RxJava的zip操作符来做,在有了Kotlin以后,如果利用Kotlin,这段代码甚至会比zip操作符还要简单。例如:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_main)

GlobalScope.launch(Dispatchers.Main) {

Log.v("wuyue", "time 1==" + System.currentTimeMillis())

val requestA = async { requestA() }

val requestB = async { requestB() }

val sum = requestA.await() +"_____" +requestB.await()

Log.v("wuyue", "time 2==" + System.currentTimeMillis() + " get sum=" + sum)

}

}

/**

* 3s以后 才拿到请求结果 IQOO

*/

suspend fun requestA(): String {

delay(3 * 1000)

Log.v("wuyue", "requestA in " + Thread.currentThread().name)

return "IQOO"

}

/**

* 5秒以后拿到请求结果 B

*/

suspend requestB(): String {

delay(5 * 1000)

Log.v("wuyue", "requestB in " + Thread.currentThread().name)

return "X27"

}

}

总结:

1、Kotlin-JVM中所谓的协程是假协程,本质上还是一套基于原生Java Thread API 的封装。

2、Kotlin-JVM中所谓的协程挂起,就是开启了一个子线程去执行任务,并且可以自动切回原来的线程

3、Kotlin-JVM中的协程最大的价值是写起来比RxJava的线程切换还要方便。几乎就是用阻塞的写法来完成非

阻塞的任务

协程的使用

如何启动一个协程(Coroutine)

在 kotlinx.coroutines 库中,我们可以使用 launch 或 async 来启动一个新的 coroutine。

从概念上讲,async 和 launch 是类似的,区别在于 launch 会返回一个 Job 对象,不会携带任何结果值。而 async 则是返回一个 Deferred - 一个轻量级、非阻塞的 future,代表了之后将会提供结果值的承诺(promise),因此可以使用 .await() 来获得其最终的结果,当然 Deferred 也是一个 Job,如果需要也是可以取消的。

如果你对于 future, promise, deferred 等概念感到困惑,可以先阅读并发 Promise 模型或其他资料了解相关概念。

Coroutine context

在 Android 中我们经常使用两类 context:

uiContext: 用于执行 UI 相关操作。

bgContext: 用于执行需要在后台运行的耗时操作。

// dispatches execution onto the Android main UI thread

private val uiContext: CoroutineContext = UI

// represents a common pool of shared threads as the coroutine dispatcher

private val bgContext: CoroutineContext = CommonPool

这里 bgContext 使用 CommonPool,可以限制同时运行的线程数小于 Runtime.getRuntime.availableProcessors() - 1。

launch + async (execute task)

父协程(The parent coroutine)使用 uiContext 通过 *launch *启动。

子协程(The child coroutine)使用 CommonPool context 通过 *async *启动。

注意:

父协程总是会等待所有的子协程执行完毕。

如果发生未检查的异常,应用将会崩溃。

下面实现一个简单的读取数据并由视图进行展示的例子:

private fun loadData() = launch(uiContext) {

view.showLoading() // ui thread

val task = async(bgContext) { dataProvider.loadData("Task") }

val result = task.await() // non ui thread, suspend until finished

view.showData(result) // ui thread

}

launch + async + async (顺序执行两个任务)

下面的两个任务是顺序执行的:

private fun loadData() = launch(uiContext) {

view.showLoading() // ui thread

// non ui thread, suspend until task is finished

val result1 = async(bgContext) { dataProvider.loadData("Task 1") }.await()

// non ui thread, suspend until task is finished

val result2 = async(bgContext) { dataProvider.loadData("Task 2") }.await()

val result = "$result1 $result2" // ui thread

view.showData(result) // ui thread

}

launch + async + async (同时执行两个任务)

private fun loadData() = launch(uiContext) {

view.showLoading() // ui thread

val task1 = async(bgContext) { dataProvider.loadData("Task 1") }

val task2 = async(bgContext) { dataProvider.loadData("Task 2") }

val result = "${task1.await()} ${task2.await()}" // non ui thread, suspend until finished

view.showData(result) // ui thread

}

启动 coroutine 并设置超时时间

可以通过 withTimeoutOrNull 来给 coroutine job 设置时限,如果超时将会返回 null。

private fun loadData() = launch(uiContext) {

view.showLoading() // ui thread

val task = async(bgContext) { dataProvider.loadData("Task") }

// non ui thread, suspend until the task is finished or return null in 2 sec

val result = withTimeoutOrNull(2, TimeUnit.SECONDS) { task.await() }

view.showData(result) // ui thread

}

如何取消一个协程(coroutine)

var job: Job? = null

fun startPresenting() {

job = loadData()

}

fun stopPresenting() {

job?.cancel()

}

private fun loadData() = launch(uiContext) {

view.showLoading() // ui thread

val task = async(bgContext) { dataProvider.loadData("Task") }

val result = task.await() // non ui thread, suspend until finished

view.showData(result) // ui thread

}

当父协程被取消时,所有的子协程将递归的被取消。

在上面的例子中,如果 stopPresenting 在被调用时 dataProvider.loadData 正在运行,那么 view.showData 方法将不会被调用。

如何处理异常

try-catch block

我们还是可以像平时一样用 try-catch 来捕获和处理异常。不过这里推荐将 try-catch 移到 dataProvider.loadData 方法里面,而不是直接包裹在外面,并提供一个统一的 Result 类方便处理。

data class Result(val success: T? = null, val error: Throwable? = null)

private fun loadData() = launch(uiContext) {

view.showLoading() // ui thread

val task = async(bgContext) { dataProvider.loadData("Task") }

val result: Result = task.await() // non ui thread, suspend until the task is finished

if (result.success != null) {

view.showData(result.success) // ui thread

} else if (result.error != null) {

result.error.printStackTrace()

}

}

async + async

当通过 async 来启动父协程时,将会忽略掉任何异常:

private fun loadData() = async(uiContext) {

view.showLoading() // ui thread

val task = async(bgContext) { dataProvider.loadData("Task") }

val result = task.await() // non ui thread, suspend until the task is finished

view.showData(result) // ui thread

}

在这里 loadData() 方法会返回 Job 对象,而 exception 会被存放在这个 Job 对象中,我们可以用 invokeOnCompletion 函数来进行检索:

var job: Job? = null

fun startPresenting() {

job = loadData()

job?.invokeOnCompletion { it: Throwable? ->

it?.printStackTrace() // (1)

// or

job?.getCompletionException()?.printStackTrace() // (2)

// difference between (1) and (2) is that (1) will NOT contain CancellationException

// in case if job was cancelled

}

}

launch + coroutine exception handler

我们还可以为父协程的 context 中添加 CoroutineExceptionHandler 来捕获和处理异常:

val exceptionHandler: CoroutineContext = CoroutineExceptionHandler { _, throwable -> throwable.printStackTrace() }

private fun loadData() = launch(uiContext + exceptionHandler) {

view.showLoading() // ui thread

val task = async(bgContext) { dataProvider.loadData("Task") }

val result = task.await() // non ui thread, suspend until the task is finished

view.showData(result) // ui thread

}

怎么测试协程(coroutine)?

要启动协程(coroutine)必须要指定一个 CoroutineContext。

class MainPresenter(private val view: MainView,

private val dataProvider: DataProviderAPI) {

private fun loadData() = launch(UI) { // UI - dispatches execution onto Android main UI thread

view.showLoading()

// CommonPool - represents common pool of shared threads as coroutine dispatcher

val task = async(CommonPool) { dataProvider.loadData("Task") }

val result = task.await()

view.showData(result)

}

}

因此,如果你想要为你的 MainPresenter 写单元测试,你就必须要能为 UI 和 后台任务指定 coroutine context。

最简单的方式就是为 MainPresenter 的构造方法增加两个参数,并设置默认值:

class MainPresenter(private val view: MainView,

private val dataProvider: DataProviderAPI

private val uiContext: CoroutineContext = UI,

private val ioContext: CoroutineContext = CommonPool) {

private fun loadData() = launch(uiContext) { // use the provided uiContext (UI)

view.showLoading()

// use the provided ioContext (CommonPool)

val task = async(bgContext) { dataProvider.loadData("Task") }

val result = task.await()

view.showData(result)

}

}

现在,就可以在测试中传入 kotlin.coroutines 提供的 EmptyCoroutineContext 来让代码运行在当前线程里。

@Test

fun test() {

val dataProvider = Mockito.mock(DataProviderAPI::class.java)

val mockView = Mockito.mock(MainView::class.java)

val presenter = MainPresenter(mockView, dataProvider, EmptyCoroutineContext, EmptyCoroutineContext)

presenter.startPresenting()

...

}

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐