RxJava Single - 内存泄漏,如何正确取消订阅?

RxJava Single - Getting a memory leak, how to correctly unsubscribe?

我正在使用 RxJava 的 Single.fromCallable() 来包装一个第三方库,该库进行 API 调用。我在通话中测试了不同的状态,成功、失败、低网络、无网络。

但是在无网络测试中,我 运行 第一次使用 RxJava 遇到内存泄漏。我花了最后一个小时梳理代码并尝试使用 LeakCanary 库缩小泄漏范围。

我发现它来自订阅 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);
                    
       });

一旦我删除

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

它不再泄漏了。

我尝试使用 RxJava Single unsubscribe 谷歌搜索 Whosebug,得到的答案是您不需要取消订阅 Single

.

但如果我不这样做,就会出现内存泄漏。

我尝试通过在我的 ViewModel 中将 Single 调用设为实例变量来取消订阅:

Disposable mDisposable;

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

并在 Fragment 的 onDestroy() 方法中取消订阅,因此如果用户在调用结束前退出屏幕,它将取消订阅。

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

但它还在漏水。我不确定这是否是取消订阅的正确方法,或者我可能做错了其他事情。

如何正确取消订阅单个可调用?

编辑:_________________________________

我是如何重现问题的:

屏幕一是启动屏幕二。在创建屏幕二时立即执行调用。由于我在没有网络的情况下对其进行测试,因此查询将继续在屏幕二上执行,直到超时。所以在通话结束前两次关闭屏幕会导致泄漏。

它似乎不是上下文泄漏,因为我已经通过删除 .subscribe():

中的所有方法来尝试测试它
Single.fromCallable(() -> {
           
            return remoteRepository.makeTransaction(signedTransaction).execute();
})
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(txHash-> {
              //Removed all methods here.
              //Still leaks.

         }, err -> {

                    
         });

但是当我删除时:

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

它不再泄漏了。

LeakCanary 日志:

┬───
    │ 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

在 Java 中容易犯的一个错误是在 non-static 上下文中实例化匿名 class 时忘记对外部 class 实例的隐式引用.

例如:

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

实际上等同于:

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

因此,您所做的是实例化一个实现 Callable 接口的匿名 class 的新实例。

现在,让我们把它放在一些上下文中。

假设我们在服务中有这个 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;
        });
    }
}

大多数时候这不是问题,因为外部 class 比方法内部实例化的 short-lived 对象寿命更长。但是,如果您的实例化对象可以比外部对象长寿(在您的情况下,即使在 ViewModel 超出范围后 Single 仍然 运行 ),整个外部对象(在您的情况下,ViewModel)仍保留在内存中Callable 仍然有对它的隐式引用。

有很多方法可以摆脱这种不需要的引用 - 最简单的方法是在静态上下文中实例化对象,您只捕获真正需要的内容(而不是整个“外部 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));
    }
}

另一种方法是完全避免使用匿名对象并使用静态内部 classes,但这很快会使代码膨胀。