public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
...省略
}
private fun request() {
lifecycleScope.launch {
coroutineScope { // 协同作用域,抛出未捕获异常时会取消父协程
launch { }
}
supervisorScope { // 主从作用域,抛出未捕获异常时不会取消父协程
launch { }
}
}
}
注意这两个函数的作用只是定义了2个作用域而已,如果想要启动新的子协程请在里面调用launch。如果需要异步请使用async。
还可以进行一些简单的封装,比如我们可以定义一个 suspend 方法,内部返回一个 coroutineScope 作用域对象来执行一个传入的协程代码块:
private suspend fun saveLocal(coroutineBlock: (suspend CoroutineScope.() -> String)? = null): String? {
return coroutineScope {
// 以下几种写法等价,都是执行block代码块
// coroutineBlock!!.invoke(this)
// coroutineBlock?.invoke(this)
// if (coroutineBlock != null) {
// coroutineBlock.invoke(this)
// }
coroutineBlock?.let { block ->
block()
}
}
}
MainScope().launch {
println("执行在一个协程中...")
val result = saveLocal {
async(Dispatchers.IO) {
"123456"
}.await()
}
println("一个协程执行完毕... result:$result")
}
例如,假设我们定义一个用于异步获取两个文档的 coroutineScope。通过对每个延迟引用调用 await(),我们可以保证这两项 async 操作在返回值之前完成:
suspend fun fetchTwoDocs() = coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
假如像上面这样直接使用coroutineScope,那么async执行完成,coroutineScope中排在async之后的代码有可能被调度到某个子线程中执行,即上面的红色部分执行完后,蓝色部分可能运行在某个子线程中。如下图:
所以在Android中,最好是在lifecycleScope或viewModelScope中去使用async, 这样能保证async之后的代码仍然执行在主线程上。但是此时在lifecycleScope或viewModelScope中调用的async中的代码也会执行在主线程(虽然是异步的,但既然是主线程就会有IO太长阻塞主线程的风险),也就是说async默认跟父协程的调度器是一样的,因此,如果有需要,此时可以为async指定线程调度器。如下:
除了单独调用每个await方法,还可以对集合使用 awaitAll(),如以下示例所示:
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
)
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
虽然 fetchTwoDocs() 使用 async 启动新协程,但该函数使用 awaitAll() 等待启动的协程完成后才会返回结果。 此外,coroutineScope 会捕获协程抛出的所有异常,并将其传送回调用方。
写法上需要注意的点:
suspend fun main() = runBlocking {
val times = measureTimeMillis {
// 这样写是串行执行,总耗时2s
val one = doOne()
val two = doTwo()
println("The result is ${one + two}")
}
println("cost time: ${times}ms")
}
suspend fun main() = runBlocking {
val times = measureTimeMillis {
// 这样写是并行执行,总耗时1s
val one = async { doOne() }
val two = async { doTwo() }
println("The result is ${one.await() + two.await()}")
}
println("cost time: ${times}ms")
}
suspend fun main() = runBlocking {
val times = measureTimeMillis {
// 但是这样写不是并行执行,总耗时2s
val one = async { doOne() }.await()
val two = async { doTwo() }.await()
println("The result is ${one + two}")
}
println("cost time: ${times}ms")
}
private suspend fun doOne(): Int {
delay(1000L)
return 13
}
private suspend fun doTwo(): Int {
delay(1000L)
return 14
}
public object GlobalScope : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
var mainScope= MainScope()
mainScope.launch {
println("执行在一个协程中...")
val result = saveLocal {
async(Dispatchers.IO) {
"123456"
}.await()
}
println("一个协程执行完毕... result:$result")
}
...
override fun onDestroy() {
super.onDestroy()
mainScope.cancel()
}
private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(JOB_KEY, CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
}
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
get() {
while (true) {
val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
if (existing != null) {
return existing
}
val newScope = LifecycleCoroutineScopeImpl(
this,
SupervisorJob() + Dispatchers.Main.immediate
)
if (mInternalScopeRef.compareAndSet(null, newScope)) {
newScope.register()
return newScope
}
}
}
public fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {
lifecycle.whenCreated(block)
}
public fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {
lifecycle.whenStarted(block)
}
public fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
lifecycle.whenResumed(block)
}
class ExampleClass {
// Job and Dispatcher are combined into a CoroutineContext which will be discussed shortly
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine within the scope
scope.launch {
fetchDocs() // New coroutine that can call suspend functions
}
}
fun cleanUp() {
// Cancel the scope to cancel ongoing coroutines work
scope.cancel()
}
}
class CancelJobDialog() : DialogFragment(), CoroutineScope by MainScope() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
dialog?.requestWindowFeature(Window.FEATURE_NO_TITLE)
dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
return inflater.inflate(R.layout.dialog_cancel_job, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val mNoTv = view.findViewById(R.id.btn_n)
val mYesTv = view.findViewById(R.id.btn_p)
mNoTv.click { dismiss() }
mYesTv.click { doSth() }
}
private fun doSth() {
launch{
println("执行在另一个协程中...")
delay(1000L)
println("另一个协程执行完毕...")
}
dismiss()
}
override fun onDismiss(dialog: DialogInterface) {
cancel()
super.onDismiss(dialog)
}
}
/**
* 自定义带协程作用域的弹窗
*/
abstract class CoroutineScopeCenterPopup(activity: FragmentActivity) : CenterPopupView(activity), CoroutineScope {
private lateinit var job: Job
private val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
println(throwable.message ?: "Unkown Error")
}
// 此协程作用域的自定义 CoroutineContext
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job + CoroutineName("CenterPopupScope") + exceptionHandler
override fun onCreate() {
job = Job()
super.onCreate()
}
override fun onDismiss() {
job.cancel() // 关闭弹窗后,结束所有协程任务
println("关闭弹窗后,结束所有协程任务")
super.onDismiss()
}
}
class InterviewAcceptPopup(private val mActivity: FragmentActivity) : CoroutineScopeCenterPopup(mActivity) {
override fun getImplLayoutId(): Int {
return R.layout.dialog_interview_accept
}
override fun onCreate() {
super.onCreate()
val btnYes = findViewById(R.id.btn_y)
btnYes.click {
doSth()
}
}
private fun doSth() {
launch {
println("执行在协程中...")
delay(1000L)
println("执行完毕...")
dismiss()
}
}
}
fun main() {
runBlocking {
println("coroutine1 start")
delay(1000) //模拟耗时
println("coroutine1 end")
}
runBlocking {
println("coroutine2 start")
delay(2000) //模拟耗时
println("coroutine2 end")
}
println("process end")
}
suspend fun main() {
GlobalScope.launch {
println("coroutine1 start")
delay(1000) //模拟耗时
println("coroutine1 end")
}
GlobalScope.launch {
println("coroutine2 start")
delay(2000) //模拟耗时
println("coroutine2 end")
}
println("process end")
}
在suspend的main函数中执行结果如下:
不过自己定义调度器的情况不多见,更常见的是将我们自己定义好的线程池转成调度器 ,如代码清单 6-4 所示。
使用扩展函数 asCoroutineDispatcher 就可以将 Executor 转为调度器,不过这个调度器需要在使用完毕后主动关闭,以免造成线程泄露。本例中,我们使用 use 在协程执行完成后主动关闭这个调度器。
private var mHandlerThread: HandlerThread? = HandlerThread("handle_thread")
private var mHandler: Handler? = mHandlerThread?.run {
start()
Handler(this.looper)
}
...
GlobalScope.launch(mHandler.asCoroutineDispatcher("handle_thread")) {
println("执行在协程中...")
delay(1000L)
println("执行完毕...")
}
class LoginRepository(...) {
...
suspend fun makeLoginRequest(
jsonBody: String
): Result {
// Move the execution of the coroutine to the I/O dispatcher
return withContext(Dispatchers.IO) {
// 网络请求阻塞代码
}
}
}
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine on the UI thread
viewModelScope.launch {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
// 调用挂起函数请求网络接口
val result = loginRepository.makeLoginRequest(jsonBody)
// 显示请求结果
when (result) {
is Result.Success -> // Happy path
else -> // Show error in UI
}
}
}
}
class LoginViewModel(
private val loginRepository: LoginRepository
) : ViewModel() {
fun login(username: String, token: String) {
viewModelScope.launch {
try {
loginRepository.login(username, token)
// Notify view user logged in successfully
} catch (exception: IOException) {
// Notify view login attempt failed
}
}
}
}
通过launch方式启动的协程,异常应当在协程体内进行捕获,而通过async方式启动的协程,需要对await调用处进行捕获异常:
对于async创建的非根协程,仍然会将异常向上抛出,如下代码会导致应用Crash:
suspend fun main() {
val exceptionHandler = CoroutineExceptionHandler { context, throwable ->
println("exceptionHandler: $throwable")
}
val job = GlobalScope.launch(exceptionHandler) {
throw ArithmeticException()
}
val deferred = GlobalScope.async(exceptionHandler) {
throw IllegalStateException()
}
job.join()
deferred.await()
}
需要注意的是异常处理Handler需要挂在最外部的根协程上,如果挂在子协程上,可能无法捕获异常:
val exceptionHandler = CoroutineExceptionHandler { context, throwable ->
println("exceptionHandler: $throwable")
}
findViewById
exception.supressed是一个数组,里面保存了所有被抑制的异常,如果想获知所有的异常类型可以通过此方法获取。
com.my.kotlin.coroutine.exceptionhandler.GlobalCoroutineExceptionHandler
如果大家在 Android 设备上尝试运行该程序,部分机型可能只能看到全局异常处理器输出的异常信息。换言之,如果我们不配置全局异常处理器,在 Default 或者 IO 调度器上遇到未捕获的异常时极有可能发生程序闪退却没有任何异常信息的情况,此时全局异常处理器的配置就显得格外有用了。(提示:全局异常处理器不适用于 JavaScript 和 Native 平台)
注意:发生异常时,全局Handler会打印异常信息,但是并不会阻止App崩溃的发生。
这是标准库提供的扩展函数,可以实现流复制。
如果 job 为空 ,那么说明所在的协程是个简单协程,这种情况不存在取消逻辑;job 不为空时,如果 isActive 也不为 true, 则说明当前协程被取消了,抛出它对应的取消异常即可。
yield 函数的作用主要是检查所在协程状态, 如果已经取消,则抛出取消异常予以响应。此外,它还会尝试出让线程的执行权,给其他协程提供执行机会。
这看上去没什么问题,只是不够简洁,甚至还有些令人迷惑。幸运的是,官方框架提供了一个可以设定超时的 API withTimeout ,我们可以用这个 API 来优化上面的代码,如代码清单 6-12 所示。
withTimeout 这个 AP 可以设定一个超时,如果它的第二个参数 block 运行超时,那么就会被取消,取消后 withTimeout 直接抛出取消异常。如果不希望在超时的情况下抛出取消异常,也可以使用 withTimeoutOrNull, 它的效果是在超时的情况下返回 null。
根据前面协程作用域异常传播的结论,子协程产生的未捕获异常会传播给它的父协程,然后父协程会按照如下顺序进行处理:取消所有的子协程、取消自己、将异常继续向上传递。如下图:
但这种情况有时并不是我们想要的,我们更希望一个协程在产生异常时,不影响其他协程的执行。
private fun testSupervisorJob() {
val context = Job() + Dispatchers.IO
lifecycleScope.launch {
launch(context) {
delay(1000)
println("子协程1")
}
launch(context) {
delay(2000)
println("子协程2")
9 / 0 // 此处会抛出ArithmeticException异常
}
launch(context) {
delay(3000)
println("子协程3")
}
launch(context) {
delay(4000)
println("子协程4")
}
delay(5000)
println("父协程")
}
}
}
private fun testSupervisorJob() {
val context = Job() + Dispatchers.IO + CoroutineExceptionHandler { context, throwable ->
println("${context[CoroutineName]} 发生了异常: $throwable")
}
lifecycleScope.launch {
launch(context + CoroutineName("子协程1")) {
delay(1000)
println("子协程1")
}
launch(context + CoroutineName("子协程2")) {
delay(2000)
println("子协程2")
9 / 0 // 此处会抛出ArithmeticException异常
}
launch(context + CoroutineName("子协程3")) {
delay(3000)
println("子协程3")
}
launch(context + CoroutineName("子协程4")) {
delay(4000)
println("子协程4")
}
delay(5000)
println("父协程")
}
}
此时主线程不会崩溃了,但是由于第二个协程发生了异常,传递给了父协程,导致父协程取消了其他子协程,因此看不到子协程3和4的输出。此时我们将Job替换成SupervisorJob:
private fun testSupervisorJob() {
val context = SupervisorJob() + Dispatchers.IO + CoroutineExceptionHandler { context, throwable ->
println("${context[CoroutineName]} 发生了异常: $throwable")
}
lifecycleScope.launch {
launch(context + CoroutineName("子协程1")) {
delay(1000)
println("子协程1")
}
launch(context + CoroutineName("子协程2")) {
delay(2000)
println("子协程2")
9 / 0 // 此处会抛出ArithmeticException异常
}
launch(context + CoroutineName("子协程3")) {
delay(3000)
println("子协程3")
}
launch(context + CoroutineName("子协程4")) {
delay(4000)
println("子协程4")
}
delay(5000)
println("父协程")
}
}
运行之后输出:
可以看到子协程3和4这两个子协程不会因为子协程2发生了异常而被取消了,也就是说4个子协程都可以独立运行,互不影响。
private fun testSupervisorJob() {
val exceptionHandler = CoroutineExceptionHandler { context, throwable ->
println("${context[CoroutineName]} 发生了异常: $throwable")
}
lifecycleScope.launch {
lifecycleScope.launch {
delay(1000)
println("子协程1")
}
lifecycleScope.launch(exceptionHandler) {
delay(2000)
println("子协程2")
9 / 0 // 此处会抛出ArithmeticException异常
}
lifecycleScope.launch {
delay(3000)
println("子协程3")
}
lifecycleScope.launch {
delay(4000)
println("子协程4")
}
delay(5000)
println("父协程")
}
}
这样同样可以使得子协程之间的异常互不影响。
private fun testSupervisorJob() {
val exceptionHandler = CoroutineExceptionHandler { context, throwable ->
println("${context[CoroutineName]} 发生了异常: $throwable")
}
lifecycleScope.launch {
supervisorScope {
launch {
delay(1000)
println("子协程1")
}
launch(exceptionHandler) {
delay(2000)
println("子协程2")
9 / 0 // 此处会抛出ArithmeticException异常
}
launch {
delay(3000)
println("子协程3")
}
launch {
delay(4000)
println("子协程4")
}
}
delay(5000)
println("父协程")
}
}
相比之下,SupervisorJob可能更适合用于自定义Scope的场景中(例如在ViewModel或Service中),除了官方库自带的几种Scope,我们有时可以通过继承或组合的方式来使用CoroutineScope来创建自己的Scope对象,例如:
class MyViewModel : ViewModel() {
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
println("发生了异常: $throwable")
}
private val scopeWithNoEffect = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)
fun doBusiness() {
viewModelScope.launch {
scopeWithNoEffect.launch {
// ... 业务1
}
launch {
// ... 业务2
}
}
}
override fun onCleared() {
super.onCleared()
scopeWithNoEffect.cancel()
}
}
这样业务1挂掉了不会影响业务2的执行。
private fun testSupervisorJob() {
lifecycleScope.launch {
launch {
throw Exception()
}
launch { }
}
}
而实际上这个代码中的子协程的异常依然传递给了父协程导致崩溃,所以说子协程并没有从父协程继承SupervisorJob。
// DO inject Dispatchers
class NewsRepository(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }
}
// DO NOT hardcode Dispatchers
class NewsRepository {
// DO NOT use Dispatchers.Default directly, inject it instead
suspend fun loadNews() = withContext(Dispatchers.Default) { /* ... */ }
}
这种依赖项注入模式可以降低测试难度,因为您可以使用测试调度程序替换单元测试和插桩测试中的这些调度程序,以提高测试的确定性。
class NewsRepository(private val ioDispatcher: CoroutineDispatcher) {
// 因为该操作是从服务器手动获取新闻,使用阻塞Http请求,它需要将代码执行到IO调度器上,以使主线程安全
suspend fun fetchLatestNews(): List {
withContext(ioDispatcher) { /* ... implementation ... */ }
}
}
// 这个用例获取最新的新闻和相关的作者。
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository
) {
// 该方法不需要移动协程到不同的线程,因为newsRepository是主线程安全的,
// 协程中完成的工作是轻量级的,因为它只创建一个列表,并向其中添加元素
suspend operator fun invoke(): List {
val news = newsRepository.fetchLatestNews()
val response: List = mutableEmptyList()
for (article in news) {
val author = authorsRepository.getAuthor(article.author)
response.add(ArticleWithAuthor(article, author))
}
return Result.Success(response)
}
}
此模式可以提高应用的可伸缩性,因为调用挂起函数的类无需担心使用哪个 Dispatcher 来处理哪种类型的工作。该责任将由执行相关工作的类承担。
// DO create coroutines in the ViewModel
class LatestNewsViewModel(
private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow(LatestNewsUiState.Loading)
val uiState: StateFlow = _uiState
fun loadNews() {
viewModelScope.launch {
val latestNewsWithAuthors = getLatestNewsWithAuthors()
_uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors)
}
}
}
// Prefer observable state rather than suspend functions from the ViewModel
class LatestNewsViewModel(
private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {
// DO NOT do this. News would probably need to be refreshed as well.
// Instead of exposing a single value with a suspend function, news should
// be exposed using a stream of data as in the code snippet above.
suspend fun loadNews() = getLatestNewsWithAuthors()
}
视图不应直接触发任何协程来执行业务逻辑,而应将这项工作委托给 ViewModel。这样一来,业务逻辑就会变得更易于测试,因为可以对 ViewModel 对象进行单元测试,而不必使用测试视图所必需的插桩测试。
// DO expose immutable types
class LatestNewsViewModel : ViewModel() {
private val _uiState = MutableStateFlow(LatestNewsUiState.Loading)
val uiState: StateFlow = _uiState
/* ... */
}
class LatestNewsViewModel : ViewModel() {
// DO NOT expose mutable types
val uiState = MutableStateFlow(LatestNewsUiState.Loading)
/* ... */
}
// Classes in the data and business layer expose either suspend functions or Flows
class ExampleRepository {
suspend fun makeNetworkRequest() { /* ... */ }
fun getExamples(): Flow { /* ... */ }
}
采用该最佳实践后,调用方(通常是演示层)能够控制这些层中发生的工作的执行和生命周期,并在需要时取消相应工作。
class GetAllBooksAndAuthorsUseCase(
private val booksRepository: BooksRepository,
private val authorsRepository: AuthorsRepository,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend fun getBookAndAuthors(): BookAndAuthors {
// 并行地获取书籍和作者,并在两者请求时返回完成,数据准备好
return coroutineScope {
val books = async(defaultDispatcher) {
booksRepository.getAllBooks()
}
val authors = async(defaultDispatcher) {
authorsRepository.getAllAuthors()
}
BookAndAuthors(books.await(), authors.await())
}
}
}
如果只要应用处于打开状态,要完成的工作就具有相关性,并且此工作不限于特定屏幕,那么此工作的存在时间应该比调用方的生命周期更长。对于这种情况,您应使用外部 CoroutineScope。
class ArticlesRepository(
private val articlesDataSource: ArticlesDataSource,
private val externalScope: CoroutineScope,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
// As we want to complete bookmarking the article even if the user moves
// away from the screen, the work is done creating a new coroutine
// from an external scope
suspend fun bookmarkArticle(article: Article) {
externalScope.launch(defaultDispatcher) {
articlesDataSource.bookmarkArticle(article)
}
.join() // Wait for the coroutine to complete
}
}
class ArticlesRepositoryTest {
@Test
fun testBookmarkArticle() = runTest {
// Pass the testScheduler provided by runTest's coroutine scope to the test dispatcher
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
val articlesDataSource = FakeArticlesDataSource()
val repository = ArticlesRepository(
articlesDataSource,
testDispatcher
)
val article = Article()
repository.bookmarkArticle(article)
assertThat(articlesDataSource.isBookmarked(article)).isTrue()
}
}
所有 TestDispatchers 都应共用同一调度器。这样可让您在单个测试线程上运行所有协程代码,从而使测试具有确定性。runTest 会等待同一调度器上的所有协程或测试协程的所有子协程完成运行后再返回。
参考:
《深入理解Kotlin协程》- 2020年-机械工业出版社-霍丙乾
KOTLIN COROUTINES