本文记录了kotlin协程(Android)中delay方法的字节码实现,并解析了delay方法如何实现挂起操作。
一、delay方法介绍
1.1、delay方法使用举例
class TestDelay {
suspend fun testDelay() {
Log.d("TestDelay", "before delay")
delay(1000)
Log.d("TestDelay", "after delay")
}
}
上述方法将协程挂起1s,然后再恢复执行。
1.2、delay方法如何实现挂起操作
delay的挂起操作有别于Thread.sleep操作,delay挂起不会导致线程暂停执行,sleep会导致线程暂停执行。
查看上述代码的字节码反义结果如下:
delay方法正常执行情况下, 会返回一个COROUTINE_SUSPENDED对象,此对象与var4对象相同,会使当前的方法直接返回,从而起到挂起作用。
二、delay方法实现代码
接下查看delay是如何返回COROUTINE_SUSPENDED对象的
2.1、kotlin层代码
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
// if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
if (timeMillis < Long.MAX_VALUE) {
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}
public interface Delay {
/** @suppress **/
@Deprecated(
message = "Deprecated without replacement as an internal method never intended for public use",
level = DeprecationLevel.ERROR
) // Error since 1.6.0
public suspend fun delay(time: Long) {
if (time <= 0) return // don't delay
return suspendCancellableCoroutine { scheduleResumeAfterDelay(time, it) }
}
public fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>)
public fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
DefaultDelay.invokeOnTimeout(timeMillis, block, context)
}
查看协程代码(1.7.3版本)发现,delay方法会调用协程context中的scheduleResumeAfterDelay方法,此方法会等待一段时间,然后再resume协程。
这里出现一个问题----delay方法没有返回值,这与1.2中的方法好像有些冲突,在下一节查看delay方法的字节码,看下具体实现是如何,来确认是否真的有冲突。
这里先看下suspendCancellableCoroutine方法,其实现如下:
public suspend inline fun <T> suspendCancellableCoroutine(
crossinline block: (CancellableContinuation<T>) -> Unit
): T =
suspendCoroutineUninterceptedOrReturn { uCont ->
val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
/*
* For non-atomic cancellation we setup parent-child relationship immediately
* in case when `block` blocks the current thread (e.g. Rx2 with trampoline scheduler), but
* properly supports cancellation.
*/
cancellable.initCancellability()
block(cancellable)
cancellable.getResult()
}
@SinceKotlin("1.3")
@InlineOnly
@Suppress("UNUSED_PARAMETER", "RedundantSuspendModifier")
public suspend inline fun <T> suspendCoroutineUninterceptedOrReturn(crossinline block: (Continuation<T>) -> Any?): T {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
throw NotImplementedError("Implementation of suspendCoroutineUninterceptedOrReturn is intrinsic")
}
上图中的2个方法都用inline标注了,是内敛方法,所以在编译时会直接编译到delay方法中(注意:因为直接编译到delay方法中,所以内敛方法中最后一行的结果会被delay方法中的return使用)。同时看到在方法的最后调用了cancellable.getResult()方法,经查看代码发现,此方法内会返回COROUTINE_SUSPENDED对象。在此我们猜测此对象会作为delay方法的返回值,在下一节也会印证此猜测。
这样就了解了delay方法(编译后的字节码方法)的返回值对象是由何而来。针对kotlin源码中delay方法并没有返回值的问题,猜测在编译时delay方法会被编译成一个有返回值的方法,在下一节也会印证此猜测。
2.2、字节码
delay方法的字节码反义如下
@Nullable
public static final Object delay(long timeMillis, @NotNull Continuation $completion) {
if (timeMillis <= 0L)
return Unit.INSTANCE;
int $i$f$suspendCancellableCoroutine = 0;
Continuation uCont$iv = $completion;
int $i$a$-suspendCoroutineUninterceptedOrReturn-CancellableContinuationKt$suspendCancellableCoroutine$2$iv = 0;
CancellableContinuationImpl<? super Unit> cancellable$iv = new CancellableContinuationImpl(IntrinsicsKt.intercepted(uCont$iv), 1);
cancellable$iv.initCancellability();
CancellableContinuation<? super Unit> cont = cancellable$iv;
int $i$a$-suspendCancellableCoroutine-DelayKt$delay$2 = 0;
if (timeMillis < Long.MAX_VALUE)
getDelay(cont.getContext()).scheduleResumeAfterDelay(timeMillis, cont);
if (cancellable$iv.getResult() == IntrinsicsKt.getCOROUTINE_SUSPENDED())
DebugProbesKt.probeCoroutineSuspended($completion);
if (cancellable$iv.getResult() ==
IntrinsicsKt.getCOROUTINE_SUSPENDED())
return cancellable$iv.getResult();
cancellable$iv.getResult();
return Unit.INSTANCE;
}
可以看出delay方法在经过编译之后,会变成一个返回值为object的方法,同时在正常执行情况下,会将cancellable$iv.getResult()作为返回值,而这个方法也就是kotlin中的cancellable.getResult()方法。
三、总结
本文介绍了delay方法会被编译成一个有返回值的方法,并通过返回COROUTINE_SUSPENDED对象的方式,实现挂起。在挂起后,会等待一段时间,然后再调用协程的resume方法,恢复协程执行。此恢复操作,如果是在主线程会通过handler.postDelay方式实现;如果是在子线程,会通过一个DelayedTask实现,在此不进行具体分析。