Android StudioにDatabase Inspector
という便利な機能が入りました。
Databaseに関わるデバッグが効率良く行えるとても優れた機能ですが、実現方法が気になったので、Android Open Sourceの該当処理を探して、コードリーディングした内容をメモしてみます。
Database Inspectorとは
Android Studio上のUIで、起動中アプリのSQLiteDBの中身を表示・変更できるデバッグツールです。公式記事が詳しいです。
以前だと、例えばDB Browser for SQLiteのようなDBビューアーを使ったりしていましたが、アプリ起動してそのままAndroidStudio上でデバッグが出来るDatabase Inspectorは大変便利です。
疑問に思ったこと
例えば、Room
でKotlin CoroutinesのFlow
を使ってテーブルを監視している場合のことを考えます。アプリ起動中にDatabase Inspector
でテーブルの内容を更新すると、Flow
に新たなデータが流れてきます。
詳細は割愛しますが、Room
でのテーブル監視にはInvalidationTracker
というクラスが利用されいます。テーブルデータの変更をアプリが検出するためにはこのInvalidationTracker
(またはRoomの他のコンポーネント)が利用されるはずですが、AndroidSudio上のDatabase Inspectorと起動中アプリがどうやってやり取りを行うのかが気になりました。
結論
最初に結論を書いておくと、Database InspectorはART TI
という仕組みを利用しているようです。
Android 8.0 以降では、ART Tooling Interface(ART TI)により特定のランタイムの内部を公開して、プロファイラとデバッガでアプリの実行時の動作に影響を与えられるようにすることが可能です。
ART TI
の詳細には立ち入りませんが、どうやら起動中アプリのプロセスとやり取りすることが可能な技術のようです。
コードリーディング
ART TI
が使われていることは分かりましたが、実装がどうなっているかコードを読みます。AndroidOpenSourceでなんとなくアタリをつけて検索するとSqliteInspector
というズバリなクラスが見つかります。
Inspector
というクラスを継承していること、コンストラクタでConnection
とInspectorEnvironment
という引数を受け取っていることがわかります。
Inspector
メソッドのコメントからAndroidStudioからのコマンドを受信する責務をもつことが読み取れます(onReceiveCommand
)。Connection
のインスタンスを持ちます。
Connection
メソッドが1つのみの小さな抽象クラスです。sendEvent
がAndroidStudioに対してコマンドを送信するメソッドになるようです。
InspectorEnvironment
Inspector機能に必要なものを提供するinterfaceのようです。ArtTooling
がART TI
のインターフェースのように思われます。
SqliteInspector
の処理を見ると、
mEnvironment.artTooling().findInstances(SQLiteDatabase.class)
というようにSQLiteDatabase
のインスタンスを取得していることからもそう想像できるのですが、残念ながらArtTooling
の実装クラスを見つけることはできませんでした。
Roomの変更通知までの流れ
ART TI
でSQLiteDatabase
を参照できそうなことはわかったので、テーブル変更通知周りの処理の流れを追ってみます。
前述しましたが、この挙動を実現するためにはInvalidationTracker
にアクセスする必要があります。SqliteInspector
でRoomInvalidationRegistry
というそれっぽいクラスを参照しているので、これを見てみます。
- 予め
androidx.room.InvalidationTracker::refreshVersionsAsync
のMethodインスタンスをリフレクションで取得しておく triggerInvalidations
が呼ばれるとArtTooling
経由で取得したInvalidationTracker
のrefreshVersionsAsync
を呼びしてあげる。
...というような実装になっています。
InvalidationTrackerについて
Roomのテーブル監視は、DBのトリガーと監視用テーブルで実現しています。「データ変更があると監視用テーブルの該当レコードにフラグを立てる」ようなトリガーが監視対象のテーブルに設定されます。
そして、然るべきタイミングで監視用テーブルを参照し、フラグが立っているテーブル一覧を取得し、それらのテーブルを監視している処理(クエリ)をinvokeする、というような実装になっています。
上に書いたInvalidationTracker::refreshVersionsAsync
が「監視用テーブルを参照し、フラグが立っているテーブル一覧を取得し、それらのテーブルを監視している処理(クエリ)をinvokeする」メソッドになります。
最後に、SqliteInspector
内でRoomInvalidationRegistry::triggerInvalidations
が実行されるタイミングを見ておきます。
triggerInvalidationsのタイミング
直接の呼び出しはtriggerInvalidation
メソッドからで、Database Inspectorで実行されたクエリがSELECT
でないことを確認しています。これがどこから呼ばれるのか確認すると、onReceiveCommand
から呼ばれるhandleQuery
(case QUERY)が呼び出し元であることが分かります。
以上をすべてまとめると、以下のようになっているんじゃないかと想像されます。
- Database Incpector上でのデータ変更はコマンドタイプ: QUERY として
onReceiveCommand
に通知される - 実行されたクエリが
SELECT
でない場合にRoomInvalidationRegistry::triggerInvalidations
を呼ぶ - ArtToolingで取得した起動中アプリの
InvalidationTracker
インスタンスのrefreshVersionsAsync
を呼ぶことで、アプリにデータ変更を通知する
さいごに
AndroidStudioとInspector
との通信はgRPC
ぽいですが、詳しく追ってません。
ArtTooling
の実装は見られなかったので一部想像で書いています。間違いなどあればコメントください。