Android协程使用Dispatchers.Main.immediate时,如何保障代码同步执行的?

1,871 阅读2分钟

通常情况下,使用协程启动job时,job的调度依赖协程上下文指定的派发器,简单说job是调度器异步执行的,协程只是保障启动协程的线程不阻塞

但是!当指定CoroutineContext.Dispatcher为Dispatchers.Main.immediate时,你会发现launch的代码在主线程顺序进行

问题来了

异步调度能理解,把job丢给线程池执行,执行完毕resume即可(需要一点协程原理基础)

同步调度是如何实现的?(其实这是个伪命题,下文剖析)

场景复现

Dispatchers.Default
Log.e(TAG, "first")
GlobalScope.launch {
  Log.e(TAG, Thread.currentThread().name + " second")
}
Log.e(TAG, "third")
​
// 输出
first
third
DefaultDispatcher-worker-1 second
Dispatchers.Main.immediate(以下Dispatchers.Main代指Dispatchers.Main.immediate)
// 继承CoroutineScope, 覆写coroutineContext即可
val coroutineContext: CoroutineContext = Dispatchers.Main.immediate + job + exceptionHandler
Log.e(TAG, "first")
launch {
  Log.e(TAG, Thread.currentThread().name + " second")
}
Log.e(TAG, "third")
​
// 输出
first
main second
third

有点意思,看样子如果Dispatchers.Main执行耗时任务,会阻塞主线程(ANR maybe)?试试

Log.e(TAG, "first")
launch {
  Thread.sleep(TimeUnit.SECONDS.toMillis(10))
  Log.e(TAG, Thread.currentThread().name + " second")
}
Log.e(TAG, "third")
​
// 输出
first
// 约10s后输出
main second
third

Decompile一下,invokeSuspend函数并没有什么差异,协程代码生成时没有对Dispatchers.Main进行特殊处理,芭比Q了只能看源码了

Debug看下调度堆栈,定位下源码类(精通协程原理的大佬请忽略)

Dispatchers.Default调用堆栈

1.png

Dispatchers.Main调用堆栈

2.png

看堆栈已经很清晰了,Dispatchers.Main都没启动Worker(没有调度job的说法),而是同步调用一堆协程生成的代码,所以没必要时不要随便使用MainScope跑主线程代码...

基于堆栈逐步跟进,发现DispatchedContinuation类有个判断

DispatchedContinuation
inline fun resumeCancellableWith(
  result: Result<T>,
  noinline onCancellation: ((cause: Throwable) -> Unit)?
) {
  val state = result.toState(onCancellation)
  if (dispatcher.isDispatchNeeded(context)) {
    _state = state
    resumeMode = MODE_CANCELLABLE
    dispatcher.dispatch(context, this)
  } else {
      // ...
  }
}

可以看到,job进入dispatcher执行的前提是dispatcher.isDispatchNeeded(context),Dispatchers.Main中context类型为HandlerContext

HandlerContext
override fun isDispatchNeeded(context: CoroutineContext): Boolean {
  return !invokeImmediately || Looper.myLooper() != handler.looper
}

isDispatchNeeded方法返回了false,真相大白

Questions

通过Dispatchers.Main构建的CoroutineScope有什么用?

一般指定为Dispatchers.Main的CoroutineScope又称UiScope/MainScope,我理解主要就是用来在主线程调用suspend函数的,suspend函数里再进行线程切换,resume后切回主线程,适用Init UI -> Request Data -> Refresh UI的场景