Kotlin Coroutine 入门指南

什么是Coroutine

Coroutines are computer program components that generalize subroutines for non preemptive multitasking, by allowing multiple entry points for suspending and resuming execution at certain locations.

Coroutines are “light-weight threads”, but they are not a thread as we konw them. Compared to threads, coroutines are mostly very cheap in their creation

可以把Coutine简单理解成轻量的线程,但创建Coutine本身不一定会产生新的线程,比如可能有100个Coutine共用10个thread。

异步编程与Callback Hell

对于程序,特别是存在交互的程序而言,我们不能把所有事情都放到一个线程中完成。很多耗时的操作比如网络请求,通常消耗大量的时间。通常我们讲耗时的操作放到后台线程中,拿到结果后再更新UI。

我们看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
client.newCall(request).enqueue(new Callback() {
Handler mainHandler = new Handler(context.getMainLooper());

@Override
public void onFailure(Request request,final Throwable throwable) {
mainHandler.post(new Runnable() {

@Override
public void run() {
this.onFailure(null, throwable);
}
});

}

@Override
public void onResponse(final Response response) throws IOException {
mainHandler.post(new Runnable() {

@Override
public void run() {
if (!response.isSuccessful()) {
this.onFailure(response, null);
return;
}
this.onSuccess(response);
}
});

}
});

在Android平台比较常见的网络处理,我们用到了Okhttp发送网络请求,当请求返回后,我们更新UI。

但事实上我们真正的逻辑只是:

  1. 发送网络请求
  2. 如果成功则使用response更新UI
  3. 如果失败则将UI切换成失败状态
1
2
3
4
5
6
7
// 伪代码
Response response = client.newCall(request).call();
if(!response.isSuccessful) {
this.onFailure(response, null);
} else {
this.onSuccess(response);
}

大量代码做的事情与逻辑无关,而是操作线程切换。

当然也有Rxjava类似很方便的切换线程的方案。但我们还是希望:“写异步代码像写同步代码一样,可读性高,易于维护”

Coroutine 如何解决上面的问题

1
2
3
4
5
6
7
8
9
10
fun myCoroutine() {
launch(UI) {
//在后台处理耗时操作
val result = async(CommonPool) {
...do something asynchronous...
}.await()
//回到UI线程处理请求的结果
myProcessingMethod(result)
}
}

简单的解释上面的代码:

  • 首先调用launch(UI),创建一个coroutine,coroutine在UI线程中执行
  • 调用async(CommonPool) 异步处理耗时操作
  • 当异步操作进行的过程中UI线程是不会被Block的,因为程序只是被“挂起”(suspend),当异步完成后会继续执行下面的代码

有了简单的印象之后我们来看几个帮助理解coroutine的概念,这些概念在查看官方文档以及相关文章中经常出现。

Coroutine的基本使用

launch async

launch

fire and forget, can also be canceled.

1
2
3
4
5
6
7
8
fun main(args: Array<String>) = runBlocking { //(1)
val job = GlobalScope.launch { //(2)
val result = suspendingFunction() //(3)
print("$result")
}
print("The result: ")
job.join() //(4)
}

创建一个coroutine并开始执行,操作本身没有返回结果,可以通过调用返回的job的join方法等待执行完毕

async

returns promise
会返回Deferred 对象,可调用该对象的await方法得到coroutine最终的结果

1
2
3
4
5
6
7
8
9
GlobalScope.launch(Dispatchers.Main) {
//Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [context] argument.
val result = async {
text.text = "can i do it?"
delay(2000)
3
}.await()
text.text = "well well ..." + result
}

错误处理

简单来讲使用try catch包裹coroutine中的代码,当代码出错的时候可以捕捉到具体的exception

1
2
3
4
5
6
try {
val result = SimpleDataSource().getTasks()
result.value?.let { getView().showTask(it) }
} catch (e:Exception){
getView().showError()
}

理解Coroutine的几个核心概念

Dispatchers

Dispatcher类似Rxjava中的Dispatcher,即指定coroutine在哪个线程或线程池中执行,常见的有IO、Main,也可以指定一个线程池供coroutine来使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()

@JvmStatic
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher

@JvmStatic
@ExperimentalCoroutinesApi
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined

@JvmStatic
public val IO: CoroutineDispatcher = DefaultScheduler.IO
}

Structured concurrency & CoroutineScope

这两个概念涉及到coroutine的管理,我们先看一个例子,我们希望组合两张图片。分别下载后调用组合函数:

1
2
3
4
5
suspend fun loadAndCombine(name1: String, name2: String): Image { 
val deferred1 = async { loadImage(name1) }
val deferred2 = async { loadImage(name2) }
return combineImages(deferred1.await(), deferred2.await())
}

如果我们想要取消这个操作的话,通常创建一个coutineScope,将所有的操作放到一个coroutineScope中:

1
2
3
4
5
6
7
8
suspend fun loadAndCombine(name1: String, name2: String): Image =
coroutineScope {
//async 1
val deferred1 = async { loadImage(name1) }
// async 2
val deferred2 = async { loadImage(name2) }
combineImages(deferred1.await(), deferred2.await())
}

当async1抛出异常的情况下,整个coroutineScope中的操作将会被取消。Scope中的子coroutine也会一并取消,async2。

coroutineScope的文档说明:

Defines a scope for new coroutines. Every coroutine builder is an extension on CoroutineScope and inherits its coroutineContext to automatically propagate both context elements and cancellation.Every coroutine builder (like launch, async, etc) and every scoping function (like coroutineScope, withContext, etc) provides its own scope with its own Job instance into the inner block of code it runs.

CoroutineScope should be implemented on entities with well-defined lifecycle that are responsible for launching children coroutines. Example of such entity on Android is Activity.

通常Coroutine创建后我们需要对其进行必要的管理,比如取消Coroutine的执行。


Andrid使用注意

生命周期绑定

通常需要继承CoroutineScope来管理coroutine,使后台操作与activity的生命周期绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MyActivity : AppCompatActivity(), CoroutineScope {
lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}

override fun onDestroy() {
super.onDestroy()
job.cancel() // Cancel job on activity destroy. After destroy all children jobs will be cancelled automatically
}

/*
* Note how coroutine builders are scoped: if activity is destroyed or any of the launched coroutines
* in this method throws an exception, then all nested coroutines are cancelled.
*/
fun loadDataFromUI() = launch { // <- extension on current activity, launched in the main thread
val ioData = async(Dispatchers.IO) { // <- extension on launch scope, launched in IO dispatcher
// blocking I/O operation
}
// do something else concurrently with I/O
val data = ioData.await() // wait for result of I/O
draw(data) // can draw in the main thread
}
}
  • 上面的代码中launch创建的coroutine在“activity的scope中“,当activity销毁后,scope中所有coroutine也将取消。

Coroutine在MVP架构中该怎么写

Coroutine的加入可以让Android的架构更加清晰,业务逻辑也更加直接,很好的将平台与线程切换的代码量降到最低。

当然是可以用Coroutine来优化我们的MVP代码的。

直接上代码:
DataSource:

1
2
3
4
5
6
7
class SimpleDataSource {
suspend fun getTasks() = withContext(Dispatchers.IO) {
delay(2018)
//Result<Task>(error = NullPointerException())
Result<Task>(value = Task(id = 1,title = "wahaha",description = "我们的祖国是花园"))
}
}

我们使用withContext来指定请求在IO相关的线程中执行。

定义一个Presenter的基类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
open class BasePresenter : CoroutineScope {
private lateinit var job: Job

lateinit var view: IView
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job

fun bindView(view: IView) {
this.view = view
job = Job()
}

fun unBindView() {
job.cancel()
}
}

需要注意Presenter继承自CoroutineScope,用来管理coroutine,当View解绑的时候,取消“子coroutine”的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class SimplePresenter : BasePresenter() {

private fun getView(): ISimpleView {
return view as ISimpleView
}

fun getTask() {
launch {
getView().showBusyProgress()

//SimpleDataSource中定义的方法将会切换到IO中执行 注释1
val result = SimpleDataSource().getTasks()
val task = result.value
if (task != null) {
getView().showTask(task)

}

val error = result.error
if (error != null) {
getView().showError()

}
}
}
}

在Presenter中完成了“线程”的切换(当然实际并不一定每个coroutine对应一个线程)