visible true

技術的なメモを書く

Roomはどのようにsuspend関数を実現しているのか

Roomに2.1.0-alpha03からsuspend関数(コルーチン)のサポートが入りましたね。ちょっと気になったのでいくらか触ってみました。

Architecture Components Release Notes  |  Android Developers

f:id:sys1yagi:20181209155920p:plain

何ができるようになるのか

DAOでsuspend関数を宣言できるようになりました。具体的には次です。

@Dao
interface UserDao {
    @Insert
    suspend fun insert(user: User)

    @Query("SELECT * FROM user")
    suspend fun getAllUsers(): List<User>

    @Delete
    suspend fun delete(user: User)
}

CoroutineScopeのなかでこれらの関数を呼ぶとノンブロッキングで各種操作ができるようになります。

val job = Job()
val scope = CoroutineScope(Dispatchers.Main + job)
scope.launch {
    try {
        val dao = db.userDao()
        0.until(100).forEach {
            dao.insert(User())
        }
        val size = dao.getAllUsers().size // 100
        // do something
    } catch (e: java.lang.Exception) {
        // error
    }
}

便利ですね。

RoomはJavaコードを生成するライブラリ

さて、そこで気になるのは、Kotlinオンリーの機能であるはずのsuspend関数をRoomはどのように生成しているのだろう?ということです。実際に生成されたコードを読んでみました。

Continuationを明示的に受け取る関数をJavaで実装している

例えば、suspend fun insert(user: User)の生成コードは次のようになっています。

@Override
public Object insert(final User user, final Continuation<? super Unit> p1) {
    return CoroutinesRoom.execute(__db, new Callable<Unit>() {
        @Override
        public Unit call() throws Exception {
        __db.beginTransaction();
            try {
                __insertionAdapterOfUser.insert(user);
                __db.setTransactionSuccessful();
                return kotlin.Unit.INSTANCE;
            } finally {
                __db.endTransaction();
            }
        }
    }, p1);
}

第二引数にfinal Continuation<? super Unit> p1 !! なるほどCPSで渡されるContinuationを明示的に受け取る関数を宣言しています。これでsuspend関数として呼び出される関数をJavaでも宣言できるんですね〜すごい。

CoroutinesRoom objectによるブリッジ

次に注目なのはinsert関数の冒頭で呼び出しているCoroutinesRoom.execute関数です。この中身を覗くと、次のようになっています。

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class CoroutinesRoom {
    companion object {
        @JvmStatic
        suspend fun <R> execute(db: RoomDatabase, callable: Callable<R>): R {
            return withContext(db.queryExecutor.asCoroutineDispatcher()) {
                callable.call()
            }
        }
    }
}

@JvmStaticを宣言してJavaからstatic関数として呼び出せるようにしています。CoroutinesRoomはKotlinなのでsuspendキーワードが使えます!そこでwithContext関数を呼び出してqueryExecutorを使ってクエリを実行しています。suspend関数をJavaで実装して、さらにKotlinに引き渡してwithContext関数を使って実行を別のスレッドに切り替えつつ中断/再開を実現しています。

呼び出し元を見直すと次のようになっています。

CoroutinesRoom.execute(__db, new Callable<Unit>() {...省略...}, p1)

p1はContinuationです。JavaからKotlinのsuspend関数を呼び出すには末尾にContinuationを明示的に渡せばいいんですね〜すごい。

withContextとExecutor

withContext - kotlinx-coroutines-core

withContext関数は指定したContextで渡したラムダ式の中断/再開を行います。Roomではdb.queryExecutorをCoroutineDispatcherに変換して渡してます。デフォルトだと自動的にRoomが用意したExecutorが使われてしまうのですが、queryExecutorはRoomDatabase.Builderでセットできます。

次の例はDispatchers.IOをRoomのqueryExecutorにセットする例です。

val db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java,
            "database-name"
        ).apply {
            // ※良い方法ではない気がするのでマネはしないほうがいいかも
            val dispatcher = Dispatchers.IO
            if (dispatcher is ExecutorCoroutineDispatcher) {
                setQueryExecutor(dispatcher.executor)
            }
        }.build()

すごいぞRoom

コルーチンを主に使っているのでこういったサポートはとてもたすかります。 あとJavaによるsuspend関数の橋渡しのアイデアはすごいと思った。わいわい。