Realm Advent Calendar 2016 2日目の記事です。
Realm Javaはパフォーマンス、省メモリー、使いやすさを実現するために、Annotation Processorによるコード生成と、コンパイルにより生成されたバイトコードに対する書き換えを行っています。
これらの処理をいきなりコードから追って理解するのは大変なので、それぞれの処理がどういう意図のもとに何を行っているかを2016年12月2日時点での最新版であるRealm-Java 2.2.1 のソースコードを元に解説します。
Realm Java自体を開発する上で必須の知識であることは言うまでもないですが、Realm Javaを利用するだけの人にとっても裏でどのようなことがおこなわれているかを理解することで安心して使うことができるようになると思います。
処理の流れ
ユーザーが記述したソースコードに対して、Realmが行う処理の流れは以下のとおりです。
- Annotation Processorにより、モデルクラスのサブクラスの生成、デフォルトモジュールクラスである
io.realm.RealmDefaultModule.java
の生成、全モジュールクラスに対するio.realm.<ModuleName>Mediator.java
の生成 - Android Gradle Plugin が提供するTransformerの仕組みを使い、コンパイル後のバイトコードの書き換え
ここで重要なのは、1と2の間にソースコード(*.java
)からバイトコード(*.class
)へのコンパイルがおこなわれるということです。そのため、1の段階でJavaとしてコンパイルエラーの起こらないコードであることが要求されます (1で中途半端なコードを生成して2で辻褄を合わせるということはできない)。
では、それぞれについて詳細に見ていきます。
Annotation Processor
ソースコード
まず、RealmのAnnotation Processorのソースコードは、/realm/realm-annotations-processorにプロジェクトとしてまとめられています。コードを読む場合は、io.realm.processor.RealmProcessor.javaから読み始めることをおすすめします。
依存ライブラリ
依存関係は以下のとおりです。
compile - Dependencies for source set 'main'.
+--- com.squareup:javawriter:2.5.0
\--- io.realm:realm-annotations:2.2.1
これを見るとわかるように、依存はとてもシンプルです。
realm-annotations
は、 @RealmClass
など、Realmが定義するアノテーションを含むライブラリです。これらのアノテーションはライブラリとしてのRealm本体(realm-library
)と、アノテーションプロセッサの両方から同じものが必要となるため独立したライブラリとして提供しています。
JavaWriterは、Javaのソースコードを生成するためのライブラリです。
JavaPoetという後継版のライブラリがあるのですが、RealmではまだJavaWriter
を使用しています。JavaPoet
に移行したいよねという話が出たこともあるのですが、開発の優先順位の都合で未だに実現していません。
モデルクラスごとの処理
それぞれのモデルクラスに対して、以下の処理を行います。
- RealmProxyクラス用のインタフェースの生成
- RealmProxyクラスの生成
これらの処理の結果、モデルクラモデルクラスに対して、以下の様なio.realm.FooRealmProxy
クラスが作られることになります。
package io.realm;
public class FooRealmProxy extends Foo
implements RealmObjectProxy,
FooRealmProxyInterface {
// 略
}
実際に生成されるクラスは、ユニットテスト内のSimpleRealmProxy.javaを見てください。
RealmProxyInterfaceインターフェースの生成
それぞれのモデルクラスに対して、io.realm.<モデルクラス名>RealmProxyInterface
というインターフェースを作成します。
このインターフェースでは、モデルクラスに定義されたフィールドに対して、Realmが内部的に使用する特別なgetterとsetterを宣言します。たとえばname
というString
型のフィールドを持つHuman
クラスがモデルクラスとして定義されていたとすると、以下のようなインターフェースが定義されます。
package io.realm;
public interface HumanRealmProxyInterface {
public String realmGet$name();
public void realmSet$name(String value);
}
このインターフェースは、RealmProxyクラスとユーザーが定義したモデルクラスの両方が実装させ、内部用の特別なgetter/setterをモデル定義クラスとそのサブクラスであるRealmProxyクラスのどちらに対しても呼び出せるようにする役割を持っています。ユーザーがモデル定義クラスを作成する段階ではこのインタフェースを実装する必要はなく、後で出てくるバイトコード書き換えによって付与します。
このインタフェースが重要なのは、Realmが定義するこれらのgetter/setterをモデル定義クラスのインスタンスに対しても呼び出すことが必要なAPIがRealmに存在するからです。具体的にはRealm.copyFromRealm(RealmModel)
などがこれに該当します。
はじめに述べたように、バイトコード書き換えの前におこなわれるJavaのコンパイルにおいてコンパイルエラーがないコードになっていることが必要です。そのため、単純にモデルクラス自体に対してrealmSet$name(String)
を呼ぶことはできません(定義されていないためコンパイルエラーになる)。型としてモデルクラスであっても実際のインスタンスがそのRealmProxyクラスになっていれば単にRealmProxyクラスにキャストを行うコードで済みます。ところが実際にそれがモデルクラス自体のインスタンスであった場合、そのようなコードはClassCastException
となってしまいます。この問題を回避するため、実際のインスタンスがモデルクラス自体の可能性がある場所では、RealmProxyInterface
にキャストした上でgetter/setterを呼び出すようになっています。例えばSimpleRealmProxy.copy(…)がこのケースに該当します。
Human human = new Human();
// 🙅 Humanには(まだ)このsetterが存在しないのでコンパイルエラー
human.realmSet$name("taro");
// 🙅 コンパイルは通るが、実際にはHumanRealmProxyクラスのインスタンスではないので実行時エラー
((HumanRealmProxy) human).realmSet$name("taro");
// 🙆 コンパイル問題なし。このままだと実行時エラーとなるがバイトコード書き換えで辻褄をあわせる
((HumanRealmProxyInterface) human).realmSet$name("taro");
RealmProxyクラスの生成
それぞれのモデルクラスに対して、io.realm.<モデルクラス名>RealmProxy
というクラスを作成します。
このクラスの主な役割は、モデル定義された各フィールドに対する読み書きに対応して、データベースファイルに対する読み書き操作を行うメソッドの提供と、モデルクラス固有の各種staticメソッドの提供にあります。
前者の読み書き操作のためのメソッド(getter/setter)は、RealmProxyクラス自身が使用するだけでなく、後述するバイトコード書き換えのによってユーザーのコードからも使用されます。getter/setterの名前は、RealmProxyInterfaceで定義したものに合わせます。
後者のモデルクラス固有の各種staticメソッドは、このあと生成するModule Mediatorクラスから使用されます。これらの具体的な例としては以下のものがあります。
- モデルクラスに対応するテーブル作成処理(
initTable()
) - データベース上のテーブル定義とモデルクラス定義が一致しているかのチェック(
validateTable()
) - モデルクラスに対応するテーブル名を返す(
getTableName()
) - Jsonインポート処理(
createUsingJsonStream()
等) -
copyToRealm()
系のAPIでmanaged objectを作成する処理(copy()
) -
insert()
系のAPIでデータを取り込む処理(insert()
等) -
copyFromRealm()
でunmanaged objectを作成する処理(createDetachedCopy()
)
モデルクラス非依存の処理
モデルクラス非依存の処理には以下のものがあります。
- RealmDefaultModuleクラス生成
- 各Moduleクラスに対する
io.realm.<モジュール名>Mediator
クラスの生成
モジュールはモデルクラスの集合を意味し、Realmデータベースファイル毎にどのモデルクラスが使われるかを決めるために使われます。
RealmDefaultModuleクラス
このクラスは、ビルド対象のプロジェクトがAndroid applicationプロジェクトの場合のみに生成されるクラスで、そのプロジェクト内に定義されているすべてのモデルクラスを含むモジュールです。ライブラリプロジェクトで定義されたモデルクラスはここには含まれません。
実際に生成されるクラスは以下のとおりです。
package io.realm;
@io.realm.annotations.RealmModule(allClasses = true)
class DefaultRealmModule {
}
モジュールに対するMediatorクラス
RealmDefaultModule
を含む全Moduleクラスに対して、io.realm.<モジュール名>Mediator
クラスを生成します。
このクラスの役割は、Realm Javaのライブラリ本体から依頼された処理を適切なRealmProxyクラスのstaticメソッドに割り振ることにあります。そのため、RealmDefaultModuleMediator.java#L234の例のように、ほとんどのメソッドは実際のオブジェクトのクラスによる分岐処理を持ち、その先の処理はそれぞれのRealmProxyクラスの対応するstaticメソッドへの移譲の形になっています。
Annotation Processorまとめ
これでRealmのAnnotation Processorの処理をすべて説明しました。ここで生成したクラスは、javacによりクラスファイルにコンパイルされます。ここで生成されたコードはJava言語的にはコンパイルエラーが発生しないコードになっています。ただし、RealmProxyInterfaceインターフェースの生成の節に書いたように、一部のキャストはこのままでは正しくありません。このあと、その辻褄合わせを含む各種バイトコード書き換えを行っています、
Transformer
ソースコード
まず、バイトコード書き換えを行うRealm Transformerのソースコードは、/realm-transformer/にプロジェクトとしてまとめられています。コードを読む場合は、io.realm.transformer.RealmTransformer.groovyから読み始めることをおすすめします。
Realm JavaのほとんどのコードはJavaで記述されていますが、Realm Transformerについては大部分がGroovyで記述されています。
依存ライブラリ
依存関係は以下のとおりです。
compile - Dependencies for source set 'main'.
+--- io.realm:realm-annotations:2.2.1
+--- org.javassist:javassist:3.20.0-GA
\--- com.android.tools.build:gradle:2.1.0
\--- com.android.tools.build:gradle-core:2.1.0
+--- 略
Realm以外のライブラリとしては、JavassistとAndroid Gradle Pluginに依存しています。
Android Gradle Pluginに依存している理由は、アプリのビルドの適切なタイミングでバイトコード書き換えの処理を呼び出してもらうためです。
Javassistは、クラスファイルの書き換えを行うためのライブラリです。
Annotation Processorでは、存在するクラスの情報から新たなクラスを作り出すことしかできませんが、バイトコード書き換えではバイトコードで表現できることであればほとんどなんでもできます。
モデルクラスごとの処理
Annotation Processorの場合と同様、Transtormerにもモデルクラスごとの処理と、モデルクラス非依存の処理があります。まずはモデルクラスごとの処理を見ていきましょう。
モデルクラスへのgetter/setterの追加
それぞれのモデルクラスに対して、フィールドの読み書きを行うgetter/setterを定義します。
処理の呼び出しはRealmTransformer.groovy#L132です。
getter/setterの名前はRealmProxyInterfaceインターフェース生成の節のものと合わせます。
これにより、モデルクラス自身のインスタンスに対してgetter/setterを呼ぶと自身のフィールドの値への読み書きとなります。モデルクラス自身ではなく、対応するRealmProxyクラスのインスタンスに対してgetter/setterを呼ぶとメソッドがオーバーライドされた形になるためその呼び出しはデータベースへの読み書きとなります。
前出のモデルクラスHuman
に対してgetter/setterを追加すると以下のようになります(実際にはこれに対応するバイトコードができるだけで、Javaのコードは作られません)。
public class Human {
public String name;
// バイトコード書き換えにより追加されたgetter/setter
// これらは HumanRealmProxyクラスでオーバーライドさ���ます
public String realmGet$name() {
return name;
}
public void realmSet$name(String value) {
return this.name = value;
}
}
モデルクラスへRealmProxyInterfaceインターフェースの追加
RealmProxyInterfaceインターフェースの生成の節で書いたように、コンパイルされたコードにはモデルクラスを対応するRealmProxyInterfaceにキャストする処理が含まれています。そのままでは実行時エラーになってしまうので、このインターフェースをモデルクラスのインターフェースリストに追加する書き換えを行います。
処理の呼び出しはRealmTransformer.groovy#L133です。
前出のモデルクラスHuman
に対してこのインターフェース追加の書き換えを行うと以下のようになります(実際にはこれに対応するバイトコードができるだけで、Javaのコードは作られません)。
public class Human implements HumanRealmProxyInterface {
public String name;
// バイトコード書き換えにより追加されたgetter/setter
// これらは HumanRealmProxyクラスでオーバーライドされます
public String realmGet$name() {
return name;
}
public void realmSet$name(String value) {
return this.name = value;
}
}
モデルクラス非依存の処理
モデルクラスに依存しない書き換えも行います。
Transformer適用の目印を付与
処理の呼び出しはRealmTransformer.groovy#L108です。
もし何らかの事情によりRealmのTransformerが適用されて自体が発生すると、様々な実行時エラーの原因となります。そうなると、それぞれの実行時エラーに対してバグレポートが寄せられることになり、原因調査にも時間を要してしまうことが予想されました。そのため、万が一Transformerが実行されなかった場合に備え、Realmのライブラリ本体側でチェックを行い例外をスローするようになっています。
このチェックは、RealmConfigurationのstaticイニシャライザーで行っていて、デフォルトモジュールのtransformerApplied()
がfalse
を返すと例外がスローされるようになっています。
transformerApplied()
はio.realm.internal.RealmProxyMediatorでfalse
にハードコードされているため、RealmのTransformerが実行されなければ必ずfalse
が返ります。
この施策のおかげなのかそもそもTransformerが適用されないケースというのが世界で1度も発生していないからのかはわかりませんが、Transformer未適用に起因するissueは一度も報告されたことはありません。
モデルクラスの各フィールドへの読み書きをgetter/setter呼び出しに置き換え
処理の呼び出しはRealmTransformer.groovy#L140です。
すべてのクラスを対象に、モデルクラスのフィールドへのアクセスを探し、それをRealmProxyInterfaceインターフェースのgetter/setter呼び出しに置き換えます。ただし、モデルクラスとモデルクラスに対応するRealmProxyクラスがもつgetter/setter内からの読み書きについては除外します(無限再起呼び出しになってしまうため)。
こうすることで、モデルクラスのフィールドに対してユーザーが記述した読み書きの処理は、Realmが定義したgetter/setterの呼び出しに変わります。これらのgetter/setterはモデルクラスで定義されたものが対応するRealmProxyクラスでオーバーライドされているため、インスタンスがどちらのものなのかによって適切にフィールドの読み書きかデータベースの読み書きに置き換えられることになります。
Human human = ...;
String name = human.name;
// ✍上のフィールドアクセスのコードは、下のメソッド呼び出しに書き換えられます
String name = human.realmGet$name();
Transformerまとめ
Transformerによるバイトコード書き換えにより、すべての辻褄合わせを行うとともにフィールドアクセスをメソッド呼び出しに置き換えることがお分かりいただけたでしょうか。
さいごに
これらのすべての処理の結果モデルクラスのフィールドアクセスを含む様々な処理に対して遅延ロードが効くようになり、Realm Javaのパフォーマンス、省メモリー、使いやすさの一つの理由となっています。
ぜひRealm Javaを使って素晴らしいモバイルアプリケーションを作っていただければと思います。
About the content
This content has been published here with the express permission of the author.