1

I'm using RxJava's Single.fromCallable() to wrap around a third party library that makes an API call. I was testing different states on the call, success, failed, low network, no network.

But on the no network test I ran into a memory leak for the first time ever using RxJava. I spent the last hour combing through the code and trying to narrow down the leak with the LeakCanary library.

I figured out it was coming from subscribing to the Single.fromCallable().

Single.fromCallable(() -> {
       return remoteRepository.makeTransaction(signedTransaction).execute();
})
       .subscribeOn(Schedulers.newThread())
       .observeOn(AndroidSchedulers.mainThread())
       .subscribe(txHash -> {
              Log.d(TAG, "makeTransaction: " + txHash);
                    
                   
       }, err -> {
             Log.e(TAG, "makeTransaction: ", err);
                    
       });

Once I remove the

.subscribe(txHash -> { ... });

It no longer leaks.

I've tried googling stackoverflow with RxJava Single unsubscribe and I'm getting answers saying that you don't need to unsubscribe from Single

https://stackoverflow.com/a/43332198/11110509.

But if I don't I'll be getting memory leaks.

I've tried to unsubscribe by making the Single call an instance variable in my ViewModel:

Disposable mDisposable;

mDisposable = Single.fromCallable(() -> {...}).subscribe(txHash -> {..});

and unsubscribing it in the Fragment's onDestroy() method so it will unsubscribe if the user exits the screen before the call is finished.

@Override
    public void onDestroy() {
        super.onDestroy();
        mViewModel.getDisposable().dispose();
    }

But it's still leaking. I'm not sure if this is the correct way to unsubscribe or maybe I'm doing something else incorrectly.

How can I correctly unsubscribe from the Single Callable?

Edit:_________________________________

How I'm recreating the issue:

Screen one is launches screen two. The call is performed instantly on the creation of screen two. Since I'm testing it with no network, the query is continuing to perform on screen two until it times out. So closing the screen two before the call finishes causes the leak.

It doesn't seem to be a context leak because I've removed tried testing it by removing all the methods inside the .subscribe():

Single.fromCallable(() -> {
           
            return remoteRepository.makeTransaction(signedTransaction).execute();
})
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(txHash-> {
              //Removed all methods here.
              //Still leaks.

         }, err -> {

                    
         });

But when I remove:

.subscribe(txHash-> {
                   
}, err -> {  
              
});

it no longer leaks.

LeakCanary logs:

┬───
    │ GC Root: Java local variable
    │
    ├─ java.lang.Thread thread
    │    Leaking: UNKNOWN
    │    Retaining 2.4 kB in 81 objects
    │    Thread name: 'RxCachedThreadScheduler-2'
    │    ↓ Thread.<Java Local>
    │             ~~~~~~~~~~~~
    ╰→ com.dave.testapp.ui.send.SendViewModel instance
    ​     Leaking: YES (ObjectWatcher was watching this because com.dave.testapp.ui.send.SendViewModel received
    ​     ViewModel#onCleared() callback)
    ​     Retaining 588 B in 19 objects
    ​     key = 6828ea76-a75c-448b-8278-d0e0bb0229c8
    ​     watchDurationMillis = 10324
    ​     retainedDurationMillis = 5321
    ​     baseApplication instance of com.dave.testapp.BaseApplication
    
    METADATA
    
    Build.VERSION.SDK_INT: 27
    Build.MANUFACTURER: Google
    LeakCanary version: 2.7
    App process name: com.dave.testapp
DIRTY DAVE
  • 2,523
  • 2
  • 20
  • 83
  • Maybe debugging in the RxJava code helps to understand it? – Thomas S. Jan 29 '22 at 11:42
  • What do you mean by debugging in the RxJava code? I've tried for a few hours, and narrowed down the issue but I can't figure out how to stop the leak – DIRTY DAVE Jan 29 '22 at 11:44
  • Maybe that's because you are creating new thread `.subscribeOn(Schedulers.newThread())` on every single run? consider using `.subscribeOn(Schedulers.io())` – Nonika Jan 29 '22 at 11:46
  • I switched it to ```.subscribeOn(Schedulers.io())``` but it's still happening – DIRTY DAVE Jan 29 '22 at 11:56
  • if you remove `.subscribe` that means that you are not invoking the `.makeTransaction(signedTransaction).execute()` any more. so probably your leak is hidden somewhere down this path. maybe there is some retry logic behind your repository – Nonika Jan 29 '22 at 12:11
  • ```.makeTransaction(signedTransaction).execute()``` is successfully being fired on the backend even when I don't call .subscribe() . The repository is very simple no retry logic: ```public Request makeTransaction(SignedTransaction signedTransaction){ return service.makeTransaction(signedTransaction); }``` – DIRTY DAVE Jan 29 '22 at 12:17
  • 1
    To me this looks more like you're leaking a reference to the outer class invoking the `Single.fromCallable`. Remember that the lambda has an implicit reference to the outer class through `com.dave.testapp.ui.send.SendViewModel.this`. Can you (just for debugging) try to replace the lambda with a static function that returns a lambda, and clal that function instead? – adnan_e Jan 29 '22 at 12:19
  • e.g. `static Callable helper(RemoteRepository repository) { return () -> repository.makeTransaction(signedTransaction).execute(); }` and then use `Single.fromCallable(helper(remoteRepository));` – adnan_e Jan 29 '22 at 12:21
  • @adnan_e you are correct the leak is coming from the ```Single.fromCallable```. I'm now trying to narrow down the issue a bit more. Thanks. Yes I'm testing the static helper write now – DIRTY DAVE Jan 29 '22 at 12:29
  • Can you please try the proposed change with the static helper and report if it still leaks? – adnan_e Jan 29 '22 at 12:30
  • Ok wow, that did indeed fix the leak. I'm now trying to reread your paragraph above and digest why – DIRTY DAVE Jan 29 '22 at 12:45
  • 1
    See my answer. This is actually a common interview question about java memory leaks. – adnan_e Jan 29 '22 at 12:46
  • Thanks for the learning opportunity and helping me with my question. It won't let me award you the bounty until 22 hours later. – DIRTY DAVE Jan 29 '22 at 12:48
  • @adnan_e if you also wanna help me with my other rxjava question: https://stackoverflow.com/q/70891637/11110509 or you can wait until it's available for bounty in 20 hours :D – DIRTY DAVE Jan 29 '22 at 13:06

1 Answers1

2

One easy mistake to be made in Java is forgetting about the implicit reference to the outer class instance whenever you instantiate an anonymous class in a non-static context.

For example:

Single.fromCallable(() -> {
   // some logic
   return 5;
});

Is actually the same as:

Single.fromCallable(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    // some logic
                    return 5;
                }
            });

So what you did is instantiate a new instance of the anonymous class which implements the Callable interface.

Now, let's put this in some context.

Let's assume we have this inside a service class:


class SomeService {
    int aNumber = 5;
    Single<Integer> doLogic() {
        return Single.fromCallable(() -> {
            // using "aNumber" here implicates Service.this.aNumber
            // e.g. writing "return 5 + aNumber" is actually the same as
            return 5 + SomeService.this.aNumber;
        });
    }
}

Most of the time this isn't an issue because the outer classes outlive the short-lived objects instantiated inside the methods. However, if your instantiated object can outlive the outer object (in your case the Single is still running even after the ViewModel is out of scope), the whole outer object (in your case, the ViewModel) remains in memory as the Callable still has the implicit reference to it.

There are many ways how to get rid of this unwanted reference - the easiest being instantiating the object in a static context where you capture only what you really need (instead of the whole "outer this").


class SomeService {
    int aNumber = 5;
    
    static Callable staticCallableThatCapturesOnlyParameters(int param) {
        return () -> {
            // outer this is not available here
            return 5 + param; // param is captured through the function args
        };
    }
    Single<Integer> doLogic() {
        return Single.fromCallable(staticCallableThatCapturesOnlyParameters(aNumber));
    }
}

Another approach would be to simply avoid the anonymous object altogether and use static inner classes, but that quickly bloats code.

adnan_e
  • 1,764
  • 2
  • 16
  • 25