Database Inspectorがどのように実装されているかメモ

Android StudioDatabase Inspectorという便利な機能が入りました。 Databaseに関わるデバッグが効率良く行えるとても優れた機能ですが、実現方法が気になったので、Android Open Sourceの該当処理を探して、コードリーディングした内容をメモしてみます。

Database Inspectorとは

Android Studio上のUIで、起動中アプリのSQLiteDBの中身を表示・変更できるデバッグツールです。公式記事が詳しいです。

developer.android.com

以前だと、例えば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という仕組みを利用しているようです。

source.android.com

Android 8.0 以降では、ART Tooling Interface(ART TI)により特定のランタイムの内部を公開して、プロファイラとデバッガでアプリの実行時の動作に影響を与えられるようにすることが可能です。

ART TIの詳細には立ち入りませんが、どうやら起動中アプリのプロセスとやり取りすることが可能な技術のようです。

コードリーディング

ART TIが使われていることは分かりましたが、実装がどうなっているかコードを読みます。AndroidOpenSourceでなんとなくアタリをつけて検索するとSqliteInspectorというズバリなクラスが見つかります。

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspector.java

Inspectorというクラスを継承していること、コンストラクタでConnectionInspectorEnvironmentという引数を受け取っていることがわかります。

Inspector

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:inspection/inspection/src/main/java/androidx/inspection/Inspector.java

メソッドのコメントからAndroidStudioからのコマンドを受信する責務をもつことが読み取れます(onReceiveCommand)。Connectionインスタンスを持ちます。

Connection

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:inspection/inspection/src/main/java/androidx/inspection/Connection.java

メソッドが1つのみの小さな抽象クラスです。sendEventがAndroidStudioに対してコマンドを送信するメソッドになるようです。

InspectorEnvironment

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:inspection/inspection/src/main/java/androidx/inspection/InspectorEnvironment.java

Inspector機能に必要なものを提供するinterfaceのようです。ArtToolingART TIのインターフェースのように思われます。 SqliteInspectorの処理を見ると、

mEnvironment.artTooling().findInstances(SQLiteDatabase.class)

というようにSQLiteDatabaseインスタンスを取得していることからもそう想像できるのですが、残念ながらArtToolingの実装クラスを見つけることはできませんでした。

Roomの変更通知までの流れ

ART TISQLiteDatabaseを参照できそうなことはわかったので、テーブル変更通知周りの処理の流れを追ってみます。

前述しましたが、この挙動を実現するためにはInvalidationTrackerにアクセスする必要があります。SqliteInspectorRoomInvalidationRegistryというそれっぽいクラスを参照しているので、これを見てみます。

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/RoomInvalidationRegistry.java;l=38?q=RoomInvalidationRegistry&sq=&ss=androidx%2Fplatform%2Fframeworks%2Fsupport

  • 予めandroidx.room.InvalidationTracker::refreshVersionsAsyncのMethodインスタンスをリフレクションで取得しておく
  • triggerInvalidationsが呼ばれるとArtTooling経由で取得したInvalidationTrackerrefreshVersionsAsyncを呼びしてあげる。

...というような実装になっています。

InvalidationTrackerについて

Roomのテーブル監視は、DBのトリガーと監視用テーブルで実現しています。「データ変更があると監視用テーブルの該当レコードにフラグを立てる」ようなトリガーが監視対象のテーブルに設定されます。

そして、然るべきタイミングで監視用テーブルを参照し、フラグが立っているテーブル一覧を取得し、それらのテーブルを監視している処理(クエリ)をinvokeする、というような実装になっています。

上に書いたInvalidationTracker::refreshVersionsAsyncが「監視用テーブルを参照し、フラグが立っているテーブル一覧を取得し、それらのテーブルを監視している処理(クエリ)をinvokeする」メソッドになります。

最後に、SqliteInspector内でRoomInvalidationRegistry::triggerInvalidationsが実行されるタイミングを見ておきます。

triggerInvalidationsのタイミング

直接の呼び出しはtriggerInvalidationメソッドからで、Database Inspectorで実行されたクエリがSELECTでないことを確認しています。これがどこから呼ばれるのか確認すると、onReceiveCommandから呼ばれるhandleQuery(case QUERY)が呼び出し元であることが分かります。

以上をすべてまとめると、以下のようになっているんじゃないかと想像されます。

  1. Database Incpector上でのデータ変更はコマンドタイプ: QUERY としてonReceiveCommandに通知される
  2. 実行されたクエリがSELECTでない場合にRoomInvalidationRegistry::triggerInvalidationsを呼ぶ
  3. ArtToolingで取得した起動中アプリのInvalidationTrackerインスタンスrefreshVersionsAsyncを呼ぶことで、アプリにデータ変更を通知する

さいごに

AndroidStudioとInspectorとの通信はgRPCぽいですが、詳しく追ってません。 ArtToolingの実装は見られなかったので一部想像で書いています。間違いなどあればコメントください。