Roomはどのようにsuspend関数を実現しているのか
Roomに2.1.0-alpha03
からsuspend関数(コルーチン)のサポートが入りましたね。ちょっと気になったのでいくらか触ってみました。
Architecture Components Release Notes | Android Developers
何ができるようになるのか
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関数の橋渡しのアイデアはすごいと思った。わいわい。