协程 官方提供的线程API,类似于Executor,方便对线程做操作

GlobalScope.launch(Dispatchers.Main) {
    val str = withContext(Dispatchers.IO) {
        optList()
    }
    tv_content.text = str
}

Launch函数创建一个新的协程,可以指定运行的线程,如 Dispatchers.Main、Dispatchers.IO 等。{}中的代码块就是协程。

withContext是一个挂起函数,这里指定到IO线程运行,函数体是耗时操作,执行到withContext函数时,协程会被从当前线程(Main)挂起,即当前线程和协程分离,不在执行协程的代码(相当于launch函数{}中的代码提前执行完毕),线程就继续工作,而线程就在挂起函数知道的线程中继续执行,即执行这里的optList(),挂起函数执行完成之后,协程会自动切回之前的线程(Main)执行后续代码(tv_content.text = str,更新UI等)。
挂起,可理解为暂时切走,等挂起函数执行完毕后再切回之前的线程,就是两步切线程操作,切回来的操作称为恢复,恢复是协程的操作,所以挂起函数只能直接或间接的在协程中被调用(协程或其他挂起函数)。

挂起函数:

suspend 关键字修饰的函数

private suspend fun testSuspend() {
    LogUtils.e("suspend0323", "testSuspend")
}

这个挂起函数只是限制了只能在协程中调用,并不会有挂起协程,切换线程之类的操作,真正要挂起协程,需要在这个挂起函数中调用协程自带的内部实现了挂起代码的挂起函数,如:

private suspend fun testSuspend() {
    LogUtils.e("suspend0323", "testSuspend")
    withContext(Dispatchers.IO) {
        LogUtils.e("suspend0323", "testSuspend withContext")
    }
}

执行到withContext函数的时候才会挂起协程,执行线程切换。上述方法并没有实际意义,一般有耗时操作时使用挂起函数才更有意义,如网络请求、文件读写、大量计算等。

除了launch,还有async也是创建协程的函数,都是CoroutineScope的扩展方法。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

Deferred继承自Job,两个方法都返回Job,可以执行Job.cancel()来取消协程,或者调用CoroutineScope的cancel方法取消,常用于网络请求中页面销毁等与生命周期相关的地方调用。例如MVP模式中BasePresenter中同意管理创建和取消

protected val presenterScope: CoroutineScope by lazy {
    CoroutineScope(Dispatchers.Main + Job())
}

override fun detachView() {
    if (mView != null) {
        mView!!.clear()
        mView = null
    }
    cancelCoroutine()
    unSubscribe()
}

private fun cancelCoroutine() {
    presenterScope.cancel()
}

等页面销毁是调用BasePresenter的方法统一处理,防止内存泄漏。
协程中的代码是顺序执行的,遇到挂起函数,就会阻塞后续代码,直到挂起函数执行完成之后,才继续执行,若需要并发执行,可使用async。

private suspend fun test1(): Int {
    val startTime = System.currentTimeMillis()
    delay(1000)
    val endTime = System.currentTimeMillis() - startTime
    LogUtils.e("testSuspend", "one startTime = $startTime")
    LogUtils.e("testSuspend", "one time = $endTime")
    return 1
}

private suspend fun test2(): Int {
    val startTime = System.currentTimeMillis()
    delay(2000)
    val endTime = System.currentTimeMillis() - startTime
    LogUtils.e("testSuspend", "two startTime = $startTime")
    LogUtils.e("testSuspend", "two time = $endTime")
    return 2
}

private fun testSuspend() {
    LogUtils.e("suspend0323", "testSuspend")
    runBlocking {
        val v1 = async {
            test1()
        }
        val v2 = async {
            test2()
        }
        val one = v1.await()
        val two = v2.await()
        LogUtils.e("testSuspend", "result = ${one + two}")
    }
}

one startTime = 1616492750544
one time = 1005
two startTime = 1616492750547
two time = 2003
result = 3
从执行结果来看,test1和test2是并发执行的,并且等到两个方法都执行完毕后才执行后续代码,实际中可用于两个没有先后关系的并发请求,又要将请求结果合并处理的情况。
如果结合MVVM框架使用,使用ViewModel的viewModelScope创建协程,无需手动处理取消等操作,ViewModel的生命周期管理,也不会发生内存泄漏。

协程中的操作如果出现了异常,需要手动处理,launch方式创建的协程,发生异常就会抛出,可以使用try-cache方式,async方式在调用await方法是才会抛出,使用try-cache方式能够捕获到异常,但是程序依然会停止运行(说是异常会被传递到父协程,将父协程使用try-cache捕获依旧崩溃,不清楚为什么),可以在创建的时候指定CoroutineExceptionHandler统一处理异常。

fun <T, M> BaseViewModel.requestMerge(
    block: suspend () -> BaseResponse<T>,
    block1: suspend () -> BaseResponse<M>,
    resultState: MutableLiveData<ResultState<BaseMergeResponse<T, M>>>
): Job {

    val handler = CoroutineExceptionHandler { _, ex ->
        resultState.paresException(ex)
    }
    //用CoroutineExceptionHandler统一处理异常
    return viewModelScope.launch(handler) {
        val one = async { block() }
        val two = async { block1() }
        val oneResult = one.await()
        val twoResult = two.await()
        resultState.paresResult(paresMerge(oneResult, twoResult))
    }
}

以上是MVVM中对ViewModel进行扩展,处理两个并发请求并合并结果,项目中可以根据需求,封装不同的协程使用。

fun <T> BaseViewModel.launch(
    block: () -> T,
    success: (T) -> Unit,
    error: (Throwable) -> Unit = {}
) {
    viewModelScope.launch {
        runCatching {
            withContext(Dispatchers.IO) {
                block()
            }
        }.onSuccess {
            success(it)
        }.onFailure {
            error(it)
        }
    }
}

这个方法实现简单的切换线程的操作,block()一般是耗时函数,如大量的数据处理,需要切换到子线程执行。runCatching是自带的函数,内部try-cache实现,有成功和失败的回调。

@InlineOnly
@SinceKotlin("1.3")
public inline fun <T, R> T.runCatching(block: T.() -> R): Result<R> {
    return try {
        Result.success(block())
    } catch (e: Throwable) {
        Result.failure(e)
    }
}

调用launch函数的方式跟普通函数相同,传递对应的参数即可,若不需要异常处理,无需传递error,定义时已给定默认值。创建协程。线程切换都在launch函数内部实现,与调用者无关。如:

fun createList() {
    launch({doCreateList()}, {
        createList.value = ResultState.onAppSuccess(it)
    }, {
        createList.value = ResultState.onAppError(ExceptionHandle.handleException(it))
    })
}
doCreateList模拟耗时函数。
private fun doCreateList(): List<String> {
    var list: MutableList<String> = mutableListOf()
    for (i in 0..10000000) {
        list.add("$i")
    }
    return list
}

实际项目中更多使用到协程的应是网络请求,retrofit 2.6 版本之后就支持了kotlin协程。
2.6之前的写法

@GET("test")
fun test(): Deferred<String>

然后再使用async await方法完成网络请求
2.6之后,可以使用suspend关键字

@GET("test")
suspend fun test(): BaseResponse<UserDetailBean>

就可以将这个网络请求当做挂起函数,在协程中调用,并且不需要使用withContext函数来切换线程。项目中可以简单封装后使用。

fun <T> BaseViewModel.request(
    block: suspend () -> BaseResponse<T>,
    resultState: MutableLiveData<ResultState<T>>
): Job {
    return viewModelScope.launch {
        runCatching {
            block()
        }.onSuccess {
            //网络请求成功
            resultState.paresResult(it)
        }.onFailure {
            it.message?.logE()
            resultState.paresException(it)
        }
    }
}

block就是之前定义的retrofit 的API接口,resultState是LiveData,网络请求结束之后,改变resultState并通知观察者。

接口的定义使用与结合RxJava的使用方式大致相同,只是多了suspend关键字和只能在协程中调用的限制。简单调用如下:

fun getUserDetail() {
    request(
        { apiService.test() }, userDetail
    )
}

View中注册观察者

mainViewModel.userDetail.observe(this, Observer { resultState ->
    parseState(resultState, {
        LogUtils.e(TAG, "name = ${it.nickname}")
    }, {
        LogUtils.e("ccc0311", "请求失败${it.errCode},${it.errorMsg}")
    }, null, {
        LogUtils.e("ccc0311", "token 错误${it.errCode},${it.errorMsg}")
    })
})

当网络请求完成之后,观察者就会收到数据更新通知,再根据业务逻辑做后续操作。

协程与 LiveData 、ViewModel 、LifecycleScope 等JetPack组件的结合使用更多会在后续文章中记录。