【问题标题】:RxJava Single - Getting a memory leak, how to correctly unsubscribe?RxJava Single - 出现内存泄漏,如何正确取消订阅?
【发布时间】:2022-01-27 11:17:10
【问题描述】:

我正在使用 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 搜索 stackoverflow,我得到的答案是您不需要取消订阅 Single

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

但如果我不这样做,我会遇到内存泄漏。

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

Disposable mDisposable;

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

并在 Fragment 的 onDestroy() 方法中取消订阅,这样如果用户在调用完成之前退出屏幕,它将取消订阅。

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

但它仍然在泄漏。我不确定这是否是正确的退订方式,或者我做错了什么。

如何正确退订 Single Callable?

编辑:___________________

我是如何重现这个问题的:

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

这似乎不是上下文泄漏,因为我已经删除了尝试通过删除 .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

【问题讨论】:

  • 也许在 RxJava 代码中调试有助于理解它?
  • 在 RxJava 代码中调试是什么意思?我已经尝试了几个小时,并缩小了问题的范围,但我不知道如何阻止泄漏
  • 可能是因为您在每次运行时都创建了新线程.subscribeOn(Schedulers.newThread())?考虑使用.subscribeOn(Schedulers.io())
  • 对我来说,这看起来更像是泄露了对调用Single.fromCallable 的外部类的引用。请记住,lambda 通过com.dave.testapp.ui.send.SendViewModel.this 隐式引用了外部类。您可以(仅用于调试)尝试将 lambda 替换为返回 lambda 的静态函数,并改为使用该函数吗?
  • 看我的回答。这其实是一个关于java内存泄漏的常见面试题。

标签: java android rx-java rx-java2 leakcanary


【解决方案1】:

在 Java 中容易犯的一个错误是,每当您在非静态上下文中实例化匿名类时,都会忘记对外部类实例的隐式引用。

例如:

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

其实是一样的:

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

所以你所做的是实例化实现Callable 接口的匿名类的新实例。

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

假设我们在服务类中有这个:


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;
        });
    }
}

大多数时候这不是问题,因为外部类比方法内实例化的短期对象寿命更长。但是,如果您的实例化对象可以比外部对象寿命更长(在您的情况下,即使在 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));
    }
}

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

【讨论】:

    猜你喜欢
    • 2019-03-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-12
    • 2019-03-19
    • 2018-10-17
    相关资源
    最近更新 更多