源碼解析:在Room和Retrofit中使用協程

今天我們就來從源碼的角度上分析Room ORM框架以及Retrofit是如何實現對於Kotlin協程的支持的。

  1. 前提知識:對於Kotlin協程有所瞭解,不瞭解的同學可以參考下面系列文章。

    Kotlin學習系列之:協程的創建(一)

    Kotlin學習系列之:協程的創建(二)

    Kotlin學習系列之:協程的創建(三)

    Kotlin學習系列之:協程的取消和超時

    Kotlin學習系列之:使用async和await實現協程高效併發

    Kotlin學習系列之:協程上下文與分發器

  2. 核心知識點:掛起函數(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:可通過這個方法回調掛起點延續的結果

    搞清楚掛起函數的本質後,我們就可以繼續下面的分析。

  3. 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裏的掛起函數。

  4. 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接口方法。

  5. 縱觀Room和Retrofit,對於協程的支持,都是基於Continuation接口進行相關代碼實現,對於我們的啓發有兩點:

    • 理解suspend function的本質
    • 如果編寫自己的第三方框架中也需要提供Kotlin協程的支持,可以仿照Room和Retrofit的實現方式。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章