Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android: Realm.getInstance(context) returns an already closed realm instance

Realm.getInstance(context) will rarely return an already closed realm instance. How is this possible?

I am using Realm with RxJava, per https://realm.io/news/using-realm-with-rxjava/

In particular, this method throws an IllegalStateException: This Realm instance has already been closed, making it unusable.

@Override
    public void call(final Subscriber<? super RealmList<T>> subscriber) {
        final Realm realm = Realm.getInstance(context);
        subscriber.add(Subscriptions.create(new Action0() {
            @Override
            public void call() {
                try {
                    realm.close();
                } catch (RealmException ex) {
                    subscriber.onError(ex);
                }
            }
        }));

        RealmList<T> object;
        realm.beginTransaction(); //THROWS EXCEPTION

        //...
}

If I comment out the realm.close(); issue, no problems. Though I think this will lead into a native memory leak, then.

My best guess as to why this is occurring is that multiple calls to this method are being made, and if these method calls line up perfectly, then an already closed realm instance can be retrieved?

EDIT: By using Schedulers.io(), I get a lot of Calling close() on a Realm that is already closed warnings. My guess here is that somehow after I am done using the .io() thread, the realm instance is automatically closed. Not sure why this would happen though.

EDIT2: By switching to using Schedulers.newThread() instead of Schedulers.io() for my observables, the issue stopped appearing. But I am seeing a lot of Remember to call close() on all Realm instances warnings. I am pretty sure I am closing them, so I am very confused about this.

EDIT3: By switching to using AndroidSchedulers.mainThread(), no errors. Except my Realm calls run on the main thread, which is bad bad bad. My guess why this causes no warnings is because the realm now lives on the main thread, which is also where realm.close() is called (via the rx.subscriber).

EDIT4: Here's the logic for my realm observable call.

@Override
public Observable<List<ImageArticleCategoryEntity>> getArticleBuckets() {

    return RealmObservable.list(context, GET_ARTICLE_BUCKETS)
            .filter(FILTER_OUT_NULL_OR_EMPTY_LIST)
            .switchIfEmpty(refreshAndSaveAndLoadNewDataFromDb)
            .map(CONVERT_FROM_REALMLIST_TO_IMAGE_ARTICLE_ENTITYLIST);

}

public void loadArticleImages() {
    articleRepo.getArticleBuckets()
            .subscribeOn(RealmThread.get())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Subscriber<List<ImageArticleCategoryEntity>>() {
                @Override
                public void onCompleted() {
                    Timber.v("Loading article images complete!");
                    if (view != null)
                        view.hideLoadingAnimation();
                }

                @Override
                public void onError(Throwable e) {
                    Timber.e("Error loading article images", e);
                    Log.e("tag", "Error loading article images", e);
                    if (view != null) {
                        view.hideLoadingAnimation();
                        view.showLoadingErrorToast();
                    }
                }

                @Override
                public void onNext(List<ImageArticleCategoryEntity> integerImageArticleCategoryEntityHashMap) {
                    if (view != null)
                        view.loadArticleImages(integerImageArticleCategoryEntityHashMap);
                }
            });
like image 806
ZakTaccardi Avatar asked Jun 03 '15 19:06

ZakTaccardi


1 Answers

Let's simplify how the lifecycle of the Realm instance is managed. If we manage it in the context of the resume/pause cycle of an Activity or Fragment, we can much more easily control and stop work that might consume that Realm instance. The tools in RxAndroid help a lot with that.

So, I'm going to assume for this example this is happening from an Activity, but a very similar approach could be used from a Fragment, or extracted into helper classes with just a hint more plumbing.

If you bind your Observable to the lifecycle of the Activity or Fragment using RxAndroid, which it appears you are already using for mainThread(), you should be able to manage your Realm instance easily within a lifecycle. I'm also using RxAsyncUtil for Async.toAsync, which makes the original creation of the Realm instance easier.

I've used Rx a lot more than Realm (though I've toyed with it a good bit), so forgive me if some of my API usage isn't perfect. I've also abbreviated things to java8-style lambdas just for ease-of-writing and reading. If you're not using something like retrolambda it should still be pretty easy to convert it back.

class MyActivity extends Activity {
  private final CompositeSubscriptions realmSubscriptions = new CompositeSubscription();

  private AsyncSubject<Realm> realm;

  @Override
  protected void onResume() {
    super.onResume();
    realm = AsyncSubject.create();
    realmSubscriptions.add(getRealmInstance());

    // This could actually happen anytime between onResume and onPause.
    realmSubscriptions.add(loadArticlesImages());
  }

  private <T> Observable<T> bindToRealm(final Observable<T> observable) {
    // Utility to bind to the activity lifecycle, observe on the main thread
    // (implicit in the bindActivity call), and do work on the Realm thread.
    return AppObservable.bindActivity(this, observable.subscribeOn(RealmThread.get()));
  }

  private Observable<List<ImageArticleCategoryEntity>> getArticleBuckets(
      final Realm realm) {
    /* As is, except the realm instance should be passed to RealmObservable.list instead
       of the context. */
  }

  private Subscription getRealmInstance() {
    // Grab the realm instance on the realm thread, while caching it in our AsyncSubject.
    return bindtoRealm(Async.toAsync(() -> Realm.getInstance(MyActivity.this)))
        .subscribe(realm);
  }

  private Subscription loadArticleImages() {
      // Using the flatMap lets us defer this execution until the
      // Realm instance comes back from being created on the Realm thread.
      // Since it is in an AsyncSubject, it will be available nearly
      // immediately once it has been created, and is cached for any future
      // subscribers.
      return bindToRealm(realm.flatMap((realm) ->
          articleRepo.getArticleBuckets(realm).subscribeOn(RealmThread.get())))
              .subscribe(
                  (next) -> {
                    if (view != null) {
                      view.loadArticleImages(next);
                    }
                  },
                  (error) -> {
                    Timber.e("Error loading article images", e);
                    Log.e("tag", "Error loading article images", e);
                    if (view != null) { 
                      view.hideLoadingAnimation(); 
                      view.showLoadingErrorToast(); 
                    } 
                  },
                  // onCompleted
                  () -> {
                    Timber.v("Loading article images complete!"); 
                    if (view != null) view.hideLoadingAnimation(); 
                  });
  }  

  @Override
  protected void onPause() {
    // Stop any work which we added that involves the Realm instance.
    realmSubscriptions.clear();

    // Clean up the AsyncObservable. If it has a Realm instance, close it.
    if (realm.getValue() != null) {
      realm.getValue().close();
    }
    realm.dispose();
    realm = null;

    super.onPause();
  }
}

You should be able to easily extract this outside of an activity as needed, and just pass an Activity/Fragment instance for the lifecycle binding, as well as the Observable<Realm> which in this case would be the AsyncSubject. If there are still race conditions due to the subscription work, you may want to experiment with adding .unsubscribeOn(AndroidSchedulers.mainThread()) or even .unsubscribeOn(Schedulers.immediate()) (I'm actually not sure which would be best in this scenario) to bindToRealm to ensure unsubscribing happens when you want it to in the onPause, before the Realm instance is closed.

like image 153
lopar Avatar answered Oct 27 '22 10:10

lopar