Edited at

Twitterクライアントの内部DBをSQLiteからRealmに移行したときのノウハウまとめ

More than 3 years have passed since last update.

拙作のAndroid用TwitterクライアントTwitPane の内部DBをSQLiteからRealmに変更しました。

この移行作業にあたって色々とノウハウがあったので備忘録代わりにメモしておきます。

Realmの日本語の情報はまだまだ不足していますので普及の一助になれば。

とはいえ、、開発中に自分がTwitterでつぶやいていたものから拾っていますので真偽不明のものが多いです。以下に書かれていることを鵜呑みにせず、必ずご自分で検証してから導入しましょう。

初めて Realm を使うという方はまず Realm for Android - Qiita を読むといいと思います。

下記はいずれも Realm 0.81.1 と 0.82.0 を対象にしています。


Query性能は3~4割程度速くなる


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 のスキーマを共用できました。


proguard-project.txt

# 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に登録する処理がありました。

これらの処理を行う際に一部の端末でメモリ不足が発生していました。

そこで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も用意して使っています。

(デバッグ用カウント処理、ログ出力、例外処理などは割愛)


MyRealmUtil.java

    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 を使うこと)


JavaのRealmには一括Updateがない


必要なら Realm.compactRealm を実行したほうがいいかも

SQLite の vacuum と同じ位置づけだと思うのでどうしても必要なら Realm.compactRealm を実行するといいと思います。

TwitPane では「内部DBの最適化」という設定項目から実行できるようにしてあります。

竹内裕昭(@takke)/「realm」の検索結果 - Twilog


公式ドキュメントを鵜呑みにしない

proguard のところで書いたとおり Realm のドキュメントは色々間違ってることもあるので Issue などもよく確認したほうがいいです。

特に日本語のほう https://realm.io/jp/docs/java/latest/ はかなり古い内容(8/5 現在 v0.80.3)なので英語の最新版を必ず確認するべきです。例えば proguard 設定は盛大に間違ってるのでまじ気をつけてほしい。


まとめ