LoginSignup
10
8

More than 5 years have passed since last update.

realm-javaでRealm#close()をちゃんと呼ぼうとすると意外とハマる

Posted at

Realmはデータベースなので、使ったらちゃんとclose()を呼ぶ必要があります。

まえおき

最近まで、某プロダクトで全くRealmのclose()を呼ばずに使っていたんですが(それでも、実はそこそこ問題なく動くんですがw)、やっぱり「きっちりcloseしておかないと思わぬバグになっちゃ困るよね」と思い直し、ちゃんとRealm#close() を呼ぶように対処をしました。

その際に、realm-javaのソースをよく読まないで適当な実装をしたところハマった、というところがいくつかあるので、シェアしたいと思います。

Realm#executeTransactionAsyncした直後にRealm#close()すると、onSuccessもonErrorも呼ばれない

コールバックが正しく呼ばれない例
try (Realm realm = Realm.getDefaultInstance()) {
  realm.executeTransactionAsync(new Realm.Transaction(){
    @Override
    public void execute(Realm realm) {

      //・・・realmに何か書く・・・

    }
  }, new Realm.Transaction.OnSuccess() {
    @Override
    public void onSuccess() {

      //・・・成功時の処理・・・

    }
  }, new Realm.Transaction.OnError() {
    @Override
    public void onError(Throwable error) {

      //・・・失敗時の処理・・・

    }
  });
}

こんな感じのコードを書くと、成功時の処理も失敗時の処理も呼ばれずハマります。

上のコード例は try (Realm realm = Realm.getDefaultInstance()) { のようにtry-with-resourcesを使っていますが、try/finally で finallyの中で realm.close() を実行するようなコードでも同じくハマります。

なぜコールバックが呼ばれないか?

realm-javaの Realm#executeTransactionAsyncのコードは、ざっくり以下のような処理をAsyncTaskで実行しています。

transactionCommitted = false;

realm.beginTransaction();
try {
  transaction.execute(realm);

  realm.commitTransaction();
  transactionCommitted = true;

} catch (Exception e) {

  //・・・エラーオブジェクトをメンバ変数に保持・・・

} finally {
  if (realm.isInTransaction()) {
    realm.cancelTransaction();
  }

  // Send response as the final step to ensure the bg thread quit before others get the response!
  if (hasValidNotifier() && !Thread.currentThread().isInterrupted()) {
    if (transactionCommitted) {

      //・・・コールバック処理をいろいろ・・・

executeTransactionAsyncの直後に realm.close() してしまうと、 hasValidNotifier() のところで

BaseRealm.java
    // Return true if this Realm can receive notifications.
    boolean hasValidNotifier() {
        return sharedRealm.realmNotifier != null && sharedRealm.realmNotifier.isValid();
    }

sharedRealmがnullになって、ぬるぽで(黙って)死にます。
その結果、コールバック処理が一切呼ばれず「おやおや??」となってしまうのです。

どうすればよいか?

ActivityやFragmentなど、ライフサイクルを持っているような中で使うのであれば、
Realmをローカル変数として使うのではなく、メンバ変数に持たせるのがよいでしょう。
詳しくはベストプラクティスにもそれっぽいことが書いてあります。

とはいえ、、

「そうはいかない」ということもあるでしょう。

どうしてもローカル変数でrealmのインスタンスをもちつつ executeTransactionAsyncを実行したいんだ!というときには、以下のように、コールバック処理が完了したタイミングで、 realm.close() を呼ぶようにしましょう。

コールバックが正しく呼ばれる例
  final Realm realm = Realm.getDefaultInstance();
  realm.executeTransactionAsync(new Realm.Transaction(){
    @Override
    public void execute(Realm realm) {

      //・・・realmに何か書く・・・

    }
  }, new Realm.Transaction.OnSuccess() {
    @Override
    public void onSuccess() {
      realm.close();

      //・・・成功時の処理・・・

    }
  }, new Realm.Transaction.OnError() {
    @Override
    public void onError(Throwable error) {
      realm.close();

      //・・・失敗時の処理・・・

    }
  });
}

 
 

Realm#close() しても Realm#isClosed() がfalseを返すことがある!

Realmのリスナーをラップするよなクラスを作る際に、Realmのインスタンスやリスナーを二重でもたないよう、以下のような感じでコードを書いていました。(要所だけ抜粋)

closeされすぎてバグる例
class RealmObjectObserver<T extends RealmObject> {

  private Realm mRealm;

  public void register() {
    unregister();

    mRealm = Realm.getDefaultInstance();

    //・・・いろいろregister・・・
  }

  public void unregister() {
    //・・・いろいろunregister・・・

    if (mRealm != null && !mRealm.isClosed()) {
      mRealm.close();
    }
  }
}

「registerするときに、すでにregisterされてたら、ちゃんとunregisterしてからregisterするようにしよう」という意図があって、上のようなコードになっていました。

しかし、これで、実際に動かしてみると、

  • いきなり This Realm instance has already been closed, making it unusable みたいなエラーで落ちたり
  • 突然リスナーが利かなくなったり

「常にバグってるわけじゃないけど、ふとした拍子にバグる」みたいな謎現象に見舞われました。

最初は、「Realmってこういうもんなのかな」と思って設計を進めていたりしたので、原因が「unregisterが二重に呼ばれること」にある、というのを特定するのにも時間がかかりました。
@zaki50 さんによるSlackでの神サポートにより、原因が多重closeにあるというのを特定できました、ありがとうございますm(_ _)m

なぜcloseされすぎるか?

Realmのclose()は即座にDBリソースをclose()しているわけではない、というのがポイントです。

Realmのclose/isClosedのソースを見てみましょう。

BaseRealm.java
    public void close() {
        if (this.threadId != Thread.currentThread().getId()) {
            throw new IllegalStateException(INCORRECT_THREAD_CLOSE_MESSAGE);
        }

        RealmCache.release(this);
    }

    void doClose() {
        if (sharedRealm != null) {
            sharedRealm.close();
            sharedRealm = null;
        }
        if (schema != null) {
            schema.close();
        }
    }

    public boolean isClosed() {
        if (this.threadId != Thread.currentThread().getId()) {
            throw new IllegalStateException(INCORRECT_THREAD_MESSAGE);
        }

        return sharedRealm == null || sharedRealm.isClosed();
    }

Realm#isClosed() は素直にsharedRealmというものがcloseされたかどうかを返しているのに対し、 Realm#close() は直接sharedRealmをcloseするようなコードにはなっていません。

doClose() というのがちょうど isClosed() に対応するsharedRealmのcloseっぽい処理を実装しているところになりますが、これは一体どこから呼ばれるのでしょう?

正解は、 RealmCache#release の処理の中です。 しかし、毎度は呼ばれません

RealmCache.java
    static synchronized void release(BaseRealm realm) {
        // ・・・(省略)・・・

        // Decrease the local counter.
        refCount -= 1;

        if (refCount == 0) {
            // ・・・(省略)・・・

            // No more local reference to this Realm in current thread, close the instance.
            realm.doClose();

このように、Realmインスタンスに対する参照(リファレンスカウント)が0になった際にのみ、 doClose されるのです。

なので、多重closeを防ごうとして

if (realm != null && realm.isClosed()) {
  realm.close();
}

のようなコードを書いても、意図せず多重にcloseされてしまうことがある、ということなのです。

どうすればいいか?

単純に、多重closeを防ぐためにRealm#isClosed()を使っているのを、やめればいいのです。

たとえば

if (mRealm != null) {
  mRealm.close();
  mRealm = null;
}

のように、null代入をしてしまって、一度きりしかcloseできないことを保証する、とかですね。

まとめ

実はほかにもちょいちょいハマったのですがw
今回は特に激しくハマった(一晩以上考えるレベル)ものを2つだけ紹介させていただきました。

Realmは慣れてしまえば本当に使いやすく、パフォーマンスもかなりよいので、
これらのハマりどころをしっかり押さえて、つまらないことでRealmを嫌いになったりせずに、
Realmで幸せなエンジニアリングをつづけていきましょう。

10
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
8