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

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


## まえおき

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

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


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


```java:コールバックが正しく呼ばれない例
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で実行しています。

```java:
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()` のところで

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

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

### どうすればよいか？

ActivityやFragmentなど、ライフサイクルを持っているような中で使うのであれば、
Realmをローカル変数として使うのではなく、メンバ変数に持たせるのがよいでしょう。
詳しくは[ベストプラクティス](https://realm.io/jp/docs/java/latest/#realm-8)にもそれっぽいことが書いてあります。

とはいえ、、

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

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

```java:コールバックが正しく呼ばれる例
  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のインスタンスやリスナーを二重でもたないよう、以下のような感じでコードを書いていました。（要所だけ抜粋）

```java: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](http://slack.realm.io/)での神サポートにより、原因が多重closeにあるというのを特定できました、ありがとうございますm(_ _)m


### なぜcloseされすぎるか？

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

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

```java: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 の処理の中です。 **しかし、毎度は呼ばれません**

```java: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インスタンスに対する参照（リファレンスカウント）が０になった際にのみ、 doClose されるのです。

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

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

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


### どうすればいいか？

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

たとえば

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

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


## まとめ

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

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