今天我們就來從源碼的角度上分析Room ORM框架以及Retrofit是如何實現對於Kotlin協程的支持的。
-
前提知識:對於Kotlin協程有所瞭解,不瞭解的同學可以參考下面系列文章。
-
核心知識點:掛起函數(suspend function)的本質
假設我們有如下一個掛起函數(定義在HelloKotlin18.kt文件中):
suspend fun call(){ }
如果我們想在Java代碼中調用這個call方法:
public class CallSuspendInJava { public static void main(String[] args) { HelloKotlin18Kt.call() } }
這個時候,編譯器會給我們錯誤警示:
編譯器的意思是,我們在這個地方需要傳入一個Continuation類型的參數。顯而易見,我們在聲明call()方法時,並沒有要求傳入任何參數,那麼這個地方的Continuation類型的參數從何而來呢?別急,我們通過反編譯來揭曉答案。
反編譯結果如下:
可見,所謂的掛起函數,kotlin編譯器最終會將其編譯成接收Continuation類型參數的普通函數。那麼Continuation到底是個啥?
/** * Interface representing a continuation after a suspension point that returns a value of type `T`. */ @SinceKotlin("1.3") public interface Continuation<in T> { /** * The context of the coroutine that corresponds to this continuation. */ public val context: CoroutineContext /** * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the * return value of the last suspension point. */ public fun resumeWith(result: Result<T>) }
代表一個返回值爲T類型的掛起點延續的接口。
context代表協程上下文
resumeWith:可通過這個方法回調掛起點延續的結果
搞清楚掛起函數的本質後,我們就可以繼續下面的分析。
-
Room框架
假定我們有這樣一個Dao:
@Dao interface CachedOrderDao { @Insert suspend fun insert(order: CachedOrder) }
build完之後,會生成CachedOrderDao_Impl的中間類,我們來看其insert方法的實現:
@Override public Object insert(final CachedOrder order, final Continuation<? super Unit> p1) { return CoroutinesRoom.execute(__db, true, new Callable<Unit>() { @Override public Unit call() throws Exception { __db.beginTransaction(); try { __insertionAdapterOfCachedOrder.insert(order); __db.setTransactionSuccessful(); return Unit.INSTANCE; } finally { __db.endTransaction(); } } }, p1); }
我們在這裏可以看到前面剛剛分析過的Continuation,我們再看CoroutinesRoom.execute()的實現:
class CoroutinesRoom private constructor() { companion object { @JvmStatic suspend fun <R> execute( db: RoomDatabase, inTransaction: Boolean, callable: Callable<R> ): R { if (db.isOpen && db.inTransaction()) { return callable.call() } // Use the transaction dispatcher if we are on a transaction coroutine, otherwise // use the database dispatchers. val context = coroutineContext[TransactionElement]?.transactionDispatcher ?: if (inTransaction) db.transactionDispatcher else db.queryDispatcher return withContext(context) { callable.call() } } } }
這裏會接觸到一個屬性:coroutineContext
/** * Returns the context of the current coroutine. */ @SinceKotlin("1.3") @Suppress("WRONG_MODIFIER_TARGET") @InlineOnly public suspend inline val coroutineContext: CoroutineContext get() { throw NotImplementedError("Implemented as intrinsic") }
它的作用就是使得在掛起函數中獲取當前協程上下文。
再回到execute的執行邏輯,會根據事務來決定使用哪個Dispatcher:TransactionDispatcher和QueryDispatcher。
/** * Gets the query coroutine dispatcher. * * @hide */ internal val RoomDatabase.queryDispatcher: CoroutineDispatcher get() = backingFieldMap.getOrPut("QueryDispatcher") { queryExecutor.asCoroutineDispatcher() } as CoroutineDispatcher /** * Gets the transaction coroutine dispatcher. * * @hide */ internal val RoomDatabase.transactionDispatcher: CoroutineDispatcher get() = backingFieldMap.getOrPut("TransactionDispatcher") { transactionExecutor.asCoroutineDispatcher() } as CoroutineDispatcher
public abstract class RoomDatabase { private Executor mQueryExecutor; private Executor mTransactionExecutor; private final Map<String, Object> mBackingFieldMap = new ConcurrentHashMap<>(); ...... }
這裏的asCoroutineDispatcher()我們在Kotlin學習系列之:協程上下文與分發器一篇中介紹過,它可以將jdk中的線程池轉換成對應的協程分發器。也就是說,我們在Dao裏面定義的相關掛起函數,都是在我們指定的協程分發器中運行的。
public T build() { //noinspection ConstantConditions if (mContext == null) { throw new IllegalArgumentException("Cannot provide null context for the database."); } //noinspection ConstantConditions if (mDatabaseClass == null) { throw new IllegalArgumentException("Must provide an abstract class that" + " extends RoomDatabase"); } if (mQueryExecutor == null && mTransactionExecutor == null) { mQueryExecutor = mTransactionExecutor = ArchTaskExecutor.getIOThreadExecutor(); } else if (mQueryExecutor != null && mTransactionExecutor == null) { mTransactionExecutor = mQueryExecutor; } else if (mQueryExecutor == null && mTransactionExecutor != null) { mQueryExecutor = mTransactionExecutor; } ..... }
我們可以在RoomDatabase.Builder.build()方法裏,找到這倆線程池的初始化之處。如果你感興趣,還可以繼續往ArchTaskExecutor類裏去追溯。但是到這裏,我們知道Dao裏定義的掛起函數都是運行在子線程中。那麼也就意味着,我們可以在主線程中直接調用Dao裏的掛起函數。
-
Retrofit.
從Retrofit2.6.0開始,也內置了對於協程的支持,從而我們可以將請求方法定義成掛起函數:
@GET("xxx/xxx") suspend fun getXXX(): CommonResponse<XXX>
由於Retrofit是基於動態代理實現的,並沒有像Room中對Dao接口生成直接的Impl類。所以我們需要進入Retrofit的源碼一探究竟,從Retrofit的create方法開始:
到這裏,我們又看到了Continuation接口。如果參數是Continuation類型,isKotlinSuspendFunction = true。那麼我們現在可以跳出RequestFactory.parseAnnotations()方法了:
KotlinExtensions.awaitResponse(call, continuation)
實際上這行代碼同時涉及到兩個知識點:擴展方法和掛起函數的本質。並且awaitResponse()中對Call調用的是enqueue()的異步方法,所以我們可以在main線程中調用Retrofit接口方法。
-
縱觀Room和Retrofit,對於協程的支持,都是基於Continuation接口進行相關代碼實現,對於我們的啓發有兩點:
- 理解suspend function的本質
- 如果編寫自己的第三方框架中也需要提供Kotlin協程的支持,可以仿照Room和Retrofit的實現方式。