前段時間面試聊到了協程 , 但自己又很久沒去閱讀協程相關的源碼 ,所以回答的并不是很好。
過程:
面試官 : 項目中怎么處理多線程相關的
xxx: 使用Kolint協程來處理的
面試官:那你說說協程是怎么使用的
xxx : Activity 和 ViewModel 都有協程域,直接調用launch方法使用就行
面試官 :那在Application 或者 service 中如何使用呢
xxx:可以使用GlobalScope
面試官 : 那你說說GlobalScope 有什么注意事項?
xxx : emm。。。
想要回答好這個問題,那肯定是對協程必須要知根知底。 正好這幾天看了些相關文章,所以想寫篇文章總結一下,希望能幫到大家。
協程是什么?
用輕量級線程來回答其實并不準確,因為協程并不是繼承自線程,而是運行在線程之上的。每一個協程都實現了Continuation接口 , 這個接口里面有個resumeWith 方法和context (這個context不是android 中activity繼承那個context)。Continuation有很多子類,最低級的子類是SuspendLambda。協程域里 launch/async/withContext等 里面的代碼經過編譯器編譯之后都存在SuspendLambda中。說了這么多,所以協程也可以理解成若干個Continuation協作構成的程序。
協程方法必須使用suspend 標記,suspend標記的方法經過kt編譯器CPS轉換后,會在方法末尾添加一個參數Continuation,和將方法的返回值修改為Object 。
SuspendLambda
SuspendLambda會實現Function2接口, 因為 suspend CoroutineScope.() -> Unit 經過kt編譯器在java對應著的就是Function2<CoroutineScope, Continuation<? super Unit>, Object> ,所以CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
)方法最后的那個傳入block,在java就是 MainActivityonCreateonCreateonCreate1,如下:
final class MainActivity$onCreate$1 extends SuspendLambda implements Function2<CoroutineScope, Continuation<? super Unit>, Object> {
int label;
MainActivity$onCreate$1(Continuation<? super MainActivity$onCreate$1> continuation) {
super(2, continuation);
}
public final Continuation<Unit> create(Object obj, Continuation<?> continuation) {
return new MainActivity$onCreate$1<>(continuation);
}
public final Object invoke(CoroutineScope coroutineScope, Continuation<? super Unit> continuation) {
return ((MainActivity$onCreate$1) create(coroutineScope, continuation)).invokeSuspend(Unit.INSTANCE);
}
public final Object invokeSuspend(Object $result) {
MainActivity$onCreate$1 mainActivity$onCreate$1;
Object coroutine_suspended = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch (this.label) {
case 0:
ResultKt.throwOnFailure($result);
mainActivity$onCreate$1 = this;
long r2 = LiveLiterals$MainActivityKt.INSTANCE.m135xec86fb79();
Continuation continuation = mainActivity$onCreate$1;
mainActivity$onCreate$1.label = 1;
if (DelayKt.delay(r2, continuation) == coroutine_suspended) {
return coroutine_suspended;
}
break;
case 1:
mainActivity$onCreate$1 = this;
ResultKt.throwOnFailure($result);
break;
case 2:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
System.out.println(LiveLiterals$MainActivityKt.INSTANCE.m139x894324b7() + Thread.currentThread().getName());
long r22 = LiveLiterals$MainActivityKt.INSTANCE.m136x7689ba5d();
Continuation continuation2 = mainActivity$onCreate$1;
mainActivity$onCreate$1.label = 2;
if (DelayKt.delay(r22, continuation2) == coroutine_suspended) {
return coroutine_suspended;
}
MainActivity$onCreate$1 mainActivity$onCreate$12 = mainActivity$onCreate$1;
return Unit.INSTANCE;
}
}
如果需要切換線程則使用DispatchedContinuation , 這個也是Continuation的子類,而且這個類是繼承自Runnable的,如果需要切換線程則把SuspendLambda包裝在這個類里面。
如果在當前線程執行則直接調用SuspendLambda#resumeWith方法就行了,resumeWith方法實現在BaseContinuationImpl中。好了暫時先粗略了解這兩個子類,關于協程是什么就先說到這里了。
協程作用域
GlobalScope是不支持cancel的,但是GlobalScope.launch會返回一個job,這個job是支持取消的。因為GlobalScope 的EmptyCoroutineContext里是沒有Job的。所以更推薦使用 CoroutineScope(Dispatchers.Default) ,這個會在context上加上Job 。
MainScope 是在主線程使用的協程作用域,因此在這個域里不能執行耗時操作的,如果要執行耗時操作必須要啟動子協程并且指定調度器。
協程的啟動
協程必須在協程域里面啟動,有四種啟動方式DEFAULT 、 ATOMIC、UNDISPATCHED、LAZY ,DEFAULT 啟動 是支持取消的。協程啟動之前先要經過協程調度器去調度到對應的線程,之后才執行協程體內的代碼。
launch啟動協程不是lazy情況,每次都會新建StandaloneCoroutine,StandaloneCoroutine繼承AbstractCoroutine,AbstractCoroutine繼承自JobSupport和實現Continuation,這個可以理解為頂級協程,這個協程支持cancel或者其他job支持的操作。
withContext 啟動協程,會掛起當前協程直到獲取到返回值,才恢復當前協程執行。
async 啟動協程不會掛起當前協程 ,會返回一個Deferred,調用Deferred#await方法如果返回值還沒準備好會掛起當前協程。
所以總結下: 這么多啟動子協程無非就兩種方式,一種掛起當前協程啟動,另一種是不掛起當前協程啟動。
協程調度器
DEFAULT 調度器 ,通過CoroutineScope.launch啟動的時候會先構建出協程上下文,調度器為 Dispatchers.Default 即默認調度器 ,Dispatchers.Default 是一個單例 ,里面的線程數量和當前手機的cpu核數相等。如果是雙核的話,調度器為默認調度器的情況,協程里面的代碼只能在兩個線程跑(不信可以通過Thread.sleep去測試),所以請求網絡只用這個調度器肯定不行,兩個線程不夠跑。
IO調度器,里面最少有64個線程,網絡請求、IO操作都可以使用這個調度器,并且這個調度器也會用到默認調度器中的線程(資源利用最大化)。
調度器中的Worker數量即線程數量,每個Worker有它自己的本地隊列,這個隊列是一個生產者消費者隊列,最大的緩沖閾值為128。
協程執行流程
回到最開始那個代碼片段,無論通過什么scope.launch啟動協程 ,其實都是調用CoroutineScope的擴展方法launch。通過withContext開始協程會調用到suspendCoroutineUninterceptedOrReturn會掛起當前協程。
以哪種方式啟動協程最終都會執行代碼(以默認啟動為例), block.startCoroutineCancellable(completion) ,這個又會執行createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit))
。 createCoroutineUnintercepted 這個會執行上面那個代碼的create方法獲取到MainActivityonCreateonCreateonCreate1,completion就是頂級協程,在當前協程執行后,即invokeSuspend方法執行完,會調用頂級協程的resumeWith方法。頂級協程的invokeSuspend方法執行完當前協程域的所有協程就結束了。
intercepted()方法如果需要調度線程則會將協程包裝成DispatchedContinuation,所有前提都準備好了會調用當前協程的resumeWith方法。
internal abstract class BaseContinuationImpl(
public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
public final override fun resumeWith(result: Result<Any?>) {
var current = this
var param = result
while (true) {
probeCoroutineResumed(current)
with(current) {
val completion = completion!! // fail fast when trying to resume continuation without completion
val outcome: Result<Any?> =
try {
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
Result.failure(exception)
}
releaseIntercepted() // this state machine instance is terminating
if (completion is BaseContinuationImpl) {
// unrolling recursion via loop
current = completion
param = outcome
} else {
// 調用父協程的resumeWith的方法
completion.resumeWith(outcome)
return
}
}
}
}
//要執行的協程代碼體
protected abstract fun invokeSuspend(result: Result<Any?>): Any?
resumeWith里面有個死循環,執行完invokeSuspend 方法,返回為COROUTINE_SUSPENDED則需要掛起,掛起則直接調用return 退出當前方法,所以協程掛起也沒多少神秘就是return結束當前方法去執行子協程,并把當前協程傳給子協程,子協程resumeWith方法中,因為是while(true),在執行完自身invokeSuspend方法后把 current = completion ,又恢復到當前協程執行當前協程的invokeSuspend方法。completion 如果不是BaseContinuationImpl則是頂級協程,頂級協程繼承自AbstractCoroutine,所有子協程都是繼承自SuspendLambda, SuspendLambda又是繼承自BaseContinuationImpl。所以調用完頂級協程的completion.resumeWith(outcome),return當前協程域的協程就執行完了。
總結一下:resumeWith這個死循環要跳出只能是掛起當前協程或者是執行完頂級協程的resumeWith()方法。
再來說一下其他的幾個協程中常用的api
delay 方法
這個方法就是用來掛起當前的協程的,并且支持取消掛起。但是delay方法掛起并不會阻塞主線程,因為這個內部通過另開一個線程配合DelayedTaskQueue隊列來實現的,并不會影響主線程。
delay內部也是通過suspendCancellableCoroutine實現。
suspendCancellableCoroutine、suspendCoroutine
這兩個方法會掛起當前協程,去執行耗時操作,當耗時操作執行完恢復當前協程執行的時候就可以獲取到suspendCancellableCoroutine、suspendCoroutine的返回值,所以一般用于和其他庫做適配比如retrofit,注意這兩個方法內部并不會開啟子協程 。
retrofit 中使用如下
@JvmName("awaitNullable")
suspend fun <T : Any> Call<T?>.await(): T? {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T?> {
override fun onResponse(call: Call<T?>, response: Response<T?>) {
if (response.isSuccessful) {
continuation.resume(response.body())
} else {
continuation.resumeWithException(HttpException(response))
}
}
override fun onFailure(call: Call<T?>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
enqueue 是異步方法,這里把異步處理完請求后通過continuation.resume 系列方法回到當前協程,執行當前協程的invokeSuspend方法。
GlobalScope正確使用
如果在很多處通過GlobalScope.launch啟動協程,這樣會造成協程非常難管理,因為不能通過頂級域GlobalScope去取消協程,而且這種方式啟動的生命周期跟隨應用的生命周期,非常容易造成內存泄漏。
如果真要使用GlobalScope的話,可以把GlobalScope.launch啟動協程的返回值job都保存在map中,自己管理這些job的狀態,在協程需要取消的時候從map移除job并調用其cancel方法。