协程的开发人员 Roman Elizarov 是这样描述协程的:协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。
总而言之:协程可以简化异步编程,可以顺序地表达程序,协程也提供了一种避免阻塞线程并用更廉价、更可控的操作替代线程阻塞的方法 -- 协程挂起。
挂起函数能够以与普通函数相同的方式获取参数和返回值,但是调用函数可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起),挂起函数挂起协程时,不会阻塞协程所在的线程。挂起函数执行完成后会恢复协程,后面的代码才会继续执行。但是挂起函数只能在协程中或其他挂起函数中调用。事实上,要启动协程,至少要有一个挂起函数,它通常是一个挂起 lambda 表达式。所以suspend
修饰符可以标记普通函数、扩展函数和 lambda 表达式。
(2)launch函数
先从新建一个协程开始分析协程的创建,最常见的协程创建方式为CoroutineScope.launch {}
,关键源码如下:
public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job { ... coroutine.start(start, coroutine, block) return coroutine }复制代码
从上面函数定义中可以看到协程的一些重要的概念:
CoroutineScope,可以理解为协程本身,包含了 CoroutineContext。
CoroutineContext,协程上下文,是一些元素的集合,主要包括 Job 和 CoroutineDispatcher 元素,可以代表一个协程的场景。
EmptyCoroutineContext 表示一个空的协程上下文。
CoroutineDispatcher,协程调度器,决定协程所在的线程或线程池。它可以指定协程运行于特定的一个线程、一个线程池或者不指定任何线程(这样协程就会运行于当前线程)。coroutines-core
中 CoroutineDispatcher 有三种标准实现Dispatchers.Default
、Dispatchers.IO
,Dispatchers.Main
和Dispatchers.Unconfined
,Unconfined 就是不指定线程。
launch
函数定义如果不指定CoroutineDispatcher
或者没有其他的ContinuationInterceptor
,默认的协程调度器就是Dispatchers.Default
,Default
是一个协程调度器,其指定的线程为共有的线程池,线程数量至少为 2 最大与 CPU 数相同。
Job,任务,封装了协程中需要执行的代码逻辑。Job 可以取消并且有简单生命周期,它有isActive、isCompleted、isCancelled三种状态。Job 完成时是没有返回值的,如果需要返回值的话,应该使用 Deferred,它是 Job 的子类public interface Deferred<out T> : Job
。
CoroutineScope.launch
函数属于协程构建器 Coroutine builders,不阻塞当前线程,在后台创建一个新协程,也可以指定协程调度器,例如在 Android 中常用的GlobalScope.launch(Dispatchers.Main) {}
。
(3)async 函数
获取CoroutineScope.async {}
的返回值需要通过await()
函数,它也是是个挂起函数,调用时会挂起当前协程直到 async 中代码执行完并返回某个值。
(4)runBlocking 函数
runBlocking {}
是创建一个新的协程同时阻塞当前线程,直到协程结束。这个不应该在协程中使用,主要是为main
函数和测试设计的。
(5)withContext 函数
withContext {}
不会创建新的协程,在指定协程上运行挂起代码块,并挂起该协程直至代码块运行完成。
官方文档中有提到协程之间可能存在父子关系,取消父协程时也会取消所有子协程,所以协程间父子关系有三种影响:
父协程手动调用cancel()
或者异常结束,会立即取消它的所有子协程。
父协程必须等待所有子协程完成(处于完成或者取消状态)才能完成。
子协程抛出未捕获的异常时,默认情况下会取消其父协程。
launch
和async
新建协程时,首先都是newCoroutineContext(context)
新建协程的 CoroutineContext 上下文,新的协程的 CoroutineContext 都继承了原来 CoroutineScope 的 coroutineContext,GlobalScope
和普通协程的CoroutineScope
的区别,GlobalScope
的 Job 是为空的,GlobalScope.launch{}
和GlobalScope.async{}
新建的协程是没有父协程的。cancel()
只是将协程的状态修改为已取消状态,并不能取消协程的运算逻辑,协程库中很多挂起函数都会检测协程状态,如果想及时取消协程的运算,最好使用isActive
判断协程状态。SupervisorJob
和supervisorScope
时,子协程出现未捕获异常时也不会影响父协程,它们的原理是重写 childCancelled() 为override fun childCancelled(cause: Throwable): Boolean = false
launch
式协程和async
式协程都会自动向上传播异常,取消父协程。
handleJobException
的实现为空,所以如果 Root Coroutine 为async
式协程,不会有任何异常打印操作,也不会 crash,但是为launch
式协程或者actor
式协程的话,会调用handleExceptionViaHandler()
处理异常。默认情况下,launch
式协程对未捕获的异常只是打印异常堆栈信息,如果在 Android 中还会调用uncaughtExceptionPreHandler
处理异常。但是如果使用了 CoroutineExceptionHandler 的话,只会使用自定义的 CoroutineExceptionHandler 处理异常。
async
式协程只有通过await()
将异常重新抛出,不过可以可以通过try { deffered.await() } catch () { ... }
来捕获异常处理异常时,用coroutineScope或者使用SupervisorJob包装异步调用,通过用coroutineScope包裹async{},当异常发生在async{}内部时,它将会取消这个域内创建的所有其他协程,而没有影响外面的域。
Mutex
,它与synchronized
关键字有些类似,还提供了withLock
扩展函数,替代常用的mutex.lock; try {...} finally { mutex.unlock() }
:fun main(args: Array<String>) = runBlocking<Unit> { val mutex = Mutex() var counter = 0 repeat(10000) { GlobalScope.launch { mutex.withLock { counter ++ } } } println("The final count is $counter") }复制代码
引入JakeWharton 的开源库让Retrofit 直接返回 Deferred<T>:
implementation 'com.squareup.retrofit2:converter-gson:2.4.0' implementation 'com.squareup.retrofit2:retrofit:2.6.1' implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' 复制代码
/** * 用户登录接口 * * @param data * @return */ @POST("/api/login/") fun loginAsync(@Body data: LoginBean): Deferred<Response<LoginResponse>>复制代码
fun buildRetrofit(host: String): Retrofit { return Retrofit.Builder() .baseUrl(host) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(CoroutineCallAdapterFactory()) .client(okHttpClient!!) .build() }复制代码
设计BaseViewModel公共基类并继承LifecycleObserver:
open class BaseViewModel : ViewModel(), LifecycleObserver { private val viewModelJob = SupervisorJob() protected val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) protected val ioScope = CoroutineScope(Dispatchers.IO + viewModelJob) var httpStatus: MutableLiveData<HttpStatus> = MutableLiveData() suspend fun <T>invokeHttpRequest(job: Deferred<Response<T>>): T?{ var body: T? = null try { job.await().apply { if (isSuccessful){ if(body() == null){ uiScope.launch { Toast.makeText(App.getInstance().applicationContext, "服务器无返回值!",Toast.LENGTH_SHORT).show() } }else{ body = body() } }else{ val status = HttpStatus(code(),message()) httpStatus.postValue(status) uiScope.launch { Toast.makeText(App.getInstance(),"HTTP状态码${status.code}:${status.message}", Toast.LENGTH_SHORT).show() } } } }catch (e: Exception){ println(e.message) uiScope.launch { Toast.makeText(App.getInstance(),"网络连接失败!", Toast.LENGTH_SHORT).show() } } return body } override fun onCleared() { super.onCleared() viewModelJob.cancel() } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) open fun onStop() { viewModelJob.cancelChildren() } }复制代码
在BaseViewModel的子类中实现相关的业务逻辑
class LoginViewModel: BaseViewModel() { var status: MutableLiveData<Boolean> = MutableLiveData() fun login(account: String?, password: String?){ uiScope.launch { App.getInstance().run { if (account == null || account.trim() == ""){ Toast.makeText(this, getString(R.string.empty_account), Toast.LENGTH_SHORT).show() status.value = false return@launch } if (password == null || password.trim() == ""){ Toast.makeText(this, getString(R.string.empty_password), Toast.LENGTH_SHORT).show() status.value = false return@launch } } ioScope.launch { val result = invokeHttpRequest(BaseApiService.instance .getApiImp(EduApi::class.java) .loginAsync(LoginBean(account?.trim(),password?.trim()))) result?.run { status.postValue(status_code == 200) if(status.value == true){ saveJwtToken(this) } } } } } }复制代码
在视图层运用ViewModel的观察者模式,实现网络请求调用和对返回数据接收,并将ViewModel绑定LifecycleOwner的生命周期,在页面销毁时取消网络请求。
private fun initViewModel(){ viewModel = ViewModelProvider(this).get(LoginViewModel::class.java) lifecycle.addObserver(viewModel) viewModel.status.observe(this, Observer{ if (it){ startActivity(Intent(this,MainActivity::class.java)) finish() }else{ Toast.makeText(this, getString(R.string.login_fail), Toast.LENGTH_SHORT).show() } ivLoading.visibility = View.INVISIBLE }) }复制代码
CoroutineDispatcher
定义了 Coroutine 执行的线程。CoroutineDispatcher
可以限定 Coroutine 在某一个线程执行、也可以分配到一个线程池来执行、也可以不限制其执行的线程。
CoroutineDispatcher
是一个抽象类,所有 dispatcher 都应该继承这个类来实现对应的功能。标准库中提供了下面几个常用的实现:
Dispatchers.Default
— 如果创建 Coroutine 的时候没有指定 dispatcher,则一般默认使用这个作为默认值。Default dispatcher 使用一个共享的后台线程池来运行里面的任务。Dispatchers.IO
— 顾名思义这是用来执行阻塞 IO 操作的,也是用一个共享的线程池来执行里面的任务。根据同时运行的任务数量,在需要的时候会创建额外的线程,当任务执行完毕后会释放不需要的线程。通过系统 property kotlinx.coroutines.io.parallelism
可以配置最多可以创建多少线程,在 Android 环境中我们一般不需要做任何额外配置。Dispatchers.Unconfined
— 立刻在启动 Coroutine 的线程开始执行该 Coroutine直到遇到第一个 suspension point
。也就是说,coroutine builder
函数在遇到第一个 suspension point
的时候才会返回。而 Coroutine 恢复的线程取决于 suspension function
所在的线程。 一般而言我们不使用 Unconfined。newSingleThreadContext
和 newFixedThreadPoolContext
函数可以创建在私有的线程池中运行的 Dispatcher。由于创建线程比较消耗系统资源,所以对于临时创建的线程池在使用完毕后需要通过 close 函数来关闭线程池并释放资源。asCoroutineDispatcher
扩展函数可以把 Java 的 Executor
对象转换为一个 Dispatcher 使用。Dispatchers.Main
— 是在 Android 的 UI 线程执行。由于子Coroutine
会继承父Coroutine
的 context,所以为了方便使用,我们一般会在 父Coroutine 上设定一个 Dispatcher,然后所有 子Coroutine 自动使用这个 Dispatcher。
在前面介绍的 coroutine builder
函数中,都需要一个 CoroutineContext 参数。CoroutineContext
是很重要的一部分内容。
CoroutineContext
包含了一些用户定义的数据集合,这些数据和当前的 Coroutine 关联。CoroutineContext
和线程的 Thread-local 变量概念类似,区别在于 Thread-local 是可以被修改的而 CoroutineContext
是不可变(immutable)的。由于 CoroutineContext
是非常轻量级的实现,如果遇到 CoroutineContext
需要变化的时候, 只需要使用新的 context 重新创建一个 Coroutine 就可以了。
CoroutineContext
是一个被索引的 Element set 集合,里面的每个元素(Element)都有一个唯一的 Key。定义为一个 set 和 map 的混合,这样里面的每个元素都和 map 一样有个对应的 key,而每个 key 又像 set 一样直接和这个元素关联。
CoroutineContext
有两个非常重要的元素 — Job 和 Dispatcher,Job 是当前的 Coroutine 实例而 Dispatcher 决定了当前 Coroutine 执行的线程。
CoroutineContext
定义了四个核心的操作:
get
可以通过 key 来获取这个 Element。由于这是一个 get 操作符,所以可以像访问 map 中的元素一样使用 context[key]
这种中括号的形式来访问。fold
和 Collection.fold
扩展函数类似,提供便利当前 context 中所有 Element 的能力。plus
和 Set.plus
扩展函数类似,返回一个新的 context 对象,新的对象里面包含了两个里面的所有 Element,如果遇到重复的(Key 一样的),那么用+
号右边的 Element 替代左边的。minusKey
返回删除一个 Element 的 context。通过上面这些函数,context 可以很方便的组合使用,比如一个库定义了一个用来保存已经登录用户 id 的 auth
Element,而另外一个库定义了一个包含一些执行信息的 threadPool
Element, 可以通过 +
号来把这两个 context 组合一起使用:launch(auth + threadPool) {...}
,这样代码看起来更加直观。
标准库中包含了一个空的啥功能都没有的实现 EmptyCoroutineContext
。一般继承 AbstractCoroutineContextElement
这个类来实现自定义的 context。
控制 Coroutine 的执行线程是非常重要的一个功能,而这个功能是通过 CoroutineDispatcher
这个 context 接口实现的。
如果需要在 Coroutine 中创建一个不同 context 的子Coroutine,则可以使用 withContext()
这个函数来实现。