Twitterクライアントのようなそれなりに大規模なアプリの内部DBをSQLiteからRealmに置き換えるといういわゆる「飛行中の航空機のエンジンを取り替える」系の修正をやってるので現実的で苦々しい知見ばかり貯まっていく
— 竹内裕昭 (@takke) August 4, 2015
拙作のAndroid用TwitterクライアントTwitPane の内部DBをSQLiteからRealmに変更しました。
この移行作業にあたって色々とノウハウがあったので備忘録代わりにメモしておきます。
Realmの日本語の情報はまだまだ不足していますので普及の一助になれば。
とはいえ、、開発中に自分がTwitterでつぶやいていたものから拾っていますので真偽不明のものが多いです。以下に書かれていることを鵜呑みにせず、必ずご自分で検証してから導入しましょう。
初めて Realm を使うという方はまず Realm for Android - Qiita を読むといいと思います。
下記はいずれも Realm 0.81.1 と 0.82.0 を対象にしています。
Query性能は3~4割程度速くなる
SQLiteに入れてた20件くらいのクエリーをRealmに置き換えると 150ms -> 100ms くらいになった。この数値はデータ整形も含むので純粋なデータ取得だけならもっと劇的に速いかも(計測するの面倒なので省略)。
— 竹内裕昭 (@takke) July 9, 2015
4件のクエリーで 14ms -> 8ms とか 17ms -> 10ms とか。Realm に置き換えることで確かに速くなる。スクロール時に発生するクエリーなので数msでも実は体感レベルで改善する可能性あり。。
— 竹内裕昭 (@takke) July 9, 2015
17件(計62KBくらい)のinsertでSQLite -> Realmに変更すると97ms -> 38msでした。
— 竹内裕昭 (@takke) July 9, 2015
SQLiteからRealmに変更すると結局Queryが3~4割程度速くなるようなので試して良かった。どうせ実アプリに組み込んだら数%程度だろうとか思ってたしそれくらいなら移行するつもりなかったので。
— 竹内裕昭 (@takke) July 9, 2015
APKが肥大化する(Split APKで肥大化を防ぐ)
Realm公式の解説通りに gradle に記述して Realm を導入すると APK サイズが数MBほど大きくなります。
これは Realm の Core 部分が NDK で書かれており、各アーキテクチャ用の .so ファイルが含まれているためです。
そこでアーキテクチャ別に APK を用意する(分割する)ことで APK サイズの増加量を 700 KB 程度(だったかな?)まで抑えることができました。
詳細は Realm導入によるAPK肥大化を防ぐ(Split APK) - Qiita に書きました。
proguard を通すとスキーマが変わる
proguard の有無で Realm のテーブルのスキーマが別物と認識されてしまう罠がありました。
8/5 14:00頃追記
結論としては 0.81.0 のドキュメントが間違っていました。
正しくは最新のドキュメントに記載の通り、下記のように記述することで proguard の有無によらず Realm のスキーマを共用できました。
# Realm
-keep class io.realm.annotations.RealmModule
-keep @io.realm.annotations.RealmModule class *
-dontwarn javax.**
-dontwarn io.realm.**
以下、検証内容の詳細です。
検証内容ここから
改めて検証したところ、-keep class io.realm.annotations.RealmModule
を記述することで proguard の有無によらず Realm のスキーマを共用できました。逆にこの1行を削除すると proguard なし版の DB を proguard あり版のアプリから読み込んだときに下記のエラーが発生しました。
08-05 13:55:26.810 32464-32464/jp.takke.realmsample E/AndroidRuntime﹕ FATAL EXCEPTION: main
Process: jp.takke.realmsample, PID: 32464
java.lang.IllegalArgumentException: g is not part of the schema for this Realm
at io.realm.internal.c.a.f(Unknown Source)
at io.realm.internal.c.a.a(Unknown Source)
at io.realm.d.a(Unknown Source)
at io.realm.n.<init>(Unknown Source)
at io.realm.d.c(Unknown Source)
at jp.takke.realmsample.d.a(Unknown Source)
at jp.takke.realmsample.d.b(Unknown Source)
at jp.takke.realmsample.e.a(Unknown Source)
at jp.takke.realmsample.MainActivity.k(Unknown Source)
at jp.takke.realmsample.MainActivity.b(Unknown Source)
at jp.takke.realmsample.b.onClick(Unknown Source)
at android.view.View.performClick(View.java:4780)
at android.view.View$PerformClick.run(View.java:19866)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5254)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
確かに 0.81.0 の公式ドキュメントを見ると該当の proguard 設定がないんですが、0.81.1 のドキュメントには追加されていました。
-keep @io.realm.annotations.RealmModule class *
-dontwarn javax.**
-dontwarn io.realm.**
-keep class io.realm.annotations.RealmModule
-keep @io.realm.annotations.RealmModule class *
-dontwarn javax.**
-dontwarn io.realm.**
検証内容ここまで
※最初に書いてあった内容は混乱を避けるために削除しておきます。
でかすぎるトランザクションはメモリ不足で死ぬ
今回Realmを導入したアプリでは比較的大きなデータ(数万件のツイート=JSON文字列)をSQLiteからRealmに移行する処理がありました。また、アプリの設定次第では1回に100~200件のツイートを取得し、それをRealmに登録する処理がありました。
これらの処理を行う際に一部の端末でメモリ不足が発生していました。
やっぱりRealmのトランザクション、巨大にするとメモリ不足で死んだり永遠に終わらなかったりするので比較的細かく切ったほうがいいっぽい。手元でベンチマーク取れなくて残念だけど動作報告があったので。
— 竹内裕昭 (@takke) August 3, 2015
そっか、取得件数が100とかになってるとRealmデータを100件一気にコミットできなくてメモリ不足で死ぬパターンか。苦々しいけど対策するか。
— 竹内裕昭 (@takke) August 4, 2015
そこで10件程度で細かくコミットしてみるとメモリ不足が発生していた端末でも処理が完了できました。
MyRealmUtil.runWithRealmInstance(mContext, new MyRealmUtil.RunnableWithRealmInstance<Void>() {
@Override
public Void run(Realm realm) {
int inserted = 0;
int inTransaction = 0;
final int count = mDumpInfoList.size();
for (final StatusDumpInfo di : mDumpInfoList) {
if (inTransaction >= C.REALM_COMMIT_RECORD_COUNT) {
// 細かくコミットする
realm.commitTransaction();
inTransaction = 0;
}
final String jsonText = di.jsonText;
if (jsonText != null) {
if (inTransaction == 0) {
realm.beginTransaction();
}
MyRealmUtil.setRawJson(realm, C.ROW_TYPE_STATUS, di.id, jsonText);
inTransaction ++;
inserted ++;
}
}
if (inTransaction > 0) {
realm.commitTransaction();
}
return null;
}
});
あとclose漏れで何度かはまったので下記のようなメソッドとInterfaceも用意して使っています。
(デバッグ用カウント処理、ログ出力、例外処理などは割愛)
public interface RunnableWithRealmInstance<T> {
T run(Realm realm);
}
public static <T> T runWithRealmInstance(Context context, RunnableWithRealmInstance<T> logic) {
Realm realm = null;
try {
realm = MyRealmUtil.getRealmInstance(context);
return logic.run(realm);
} finally {
if (realm != null) {
realm.close();
}
}
}
削除に RealmObject.removeFromRealm を使うと絶望的に遅い(RealmResults.clear を使うこと)
ちまちま removeFromRealm 呼んでたら数時間かかる勢いだったので clear に変更してみたら1秒ほどで削除できた。
— 竹内裕昭 (@takke) July 25, 2015
JavaのRealmには一括Updateがない
どうやらJavaのRealmには一括Updateの仕組みがないようなので別の手段が必要そう。
— 竹内裕昭 (@takke) July 28, 2015
必要なら Realm.compactRealm を実行したほうがいいかも
SQLite の vacuum と同じ位置づけだと思うのでどうしても必要なら Realm.compactRealm を実行するといいと思います。
TwitPane では「内部DBの最適化」という設定項目から実行できるようにしてあります。
公式ドキュメントを鵜呑みにしない
proguard のところで書いたとおり Realm のドキュメントは色々間違ってることもあるので Issue などもよく確認したほうがいいです。
特に日本語のほう https://realm.io/jp/docs/java/latest/ はかなり古い内容(8/5 現在 v0.80.3)なので英語の最新版を必ず確認するべきです。例えば proguard 設定は盛大に間違ってるのでまじ気をつけてほしい。
まとめ
つまりRealmは(基本的な操作は確かにSQLiteより)速いけど万能じゃないしMigrationが現状では実用上不可能と考えてよいのでスキーマとか要件にあわせてギチギチに設計してから実戦投入しないとまじで途中で詰むと思う。
— 竹内裕昭 (@takke) July 28, 2015