Утечка памяти: анонимный класс, реализующий io.reactivex.Observer - PullRequest
0 голосов
/ 13 марта 2020

Я работаю над проектом Android. Некоторые из моих действий расширяются SiteFinderActivity. Этот класс отвечает за проверку текущего сеанса и передачу его своим дочерним элементам с помощью нескольких абстрактных функций.

Я использую RxRelay JakeWharton для передачи результатов из ViewModels подписчикам, SiteFinderActivity в это / мой случай. Это упрощенная версия моей ViewModel.

sealed class AuthState {
    object AuthOnError: AuthState()
    class AuthOnSuccessToken(val accessToken: String): AuthState()
}

class AuthViewModel(
        val context: Context,
        val logger: Logger
) {

    /**
     * Subscribe to this observer in order to be notified when the result is ready.
     */
    val relay: PublishRelay<AuthState> = PublishRelay.create()

    fun validateToken(): {
        // Validation...
        // Once it is done then
        relay.accept(AuthOnSuccessToken(it))
    }
}

Действие вызывает validateToken() и прослушивает его реле для результата.

abstract class SiteFinderActivity : AppCompatActivity() {

    abstract fun onAuthenticationError()
    abstract fun onAzureAccessTokenReceived(azureAccessToken: String)

    private var azureAuthVM: AuthViewModel? = getAzureAuthVM()

    private var authObserver: Observer<AuthState>? = createDefaultObserver(logger) {
        val weakThis = WeakReference(this@SiteFinderActivity)
        if (weakThis.get() == null) return@createDefaultObserver

        when (this) {
            is AuthOnSuccessToken -> {
                //...
                onAzureAccessTokenReceived(azureAccessToken)
            }
            else -> {
                onAuthenticationError()
            }
        }
    }

    override fun onStart() {
        super.onStart()
        azureAuthVM?.relay?.safeSubscribe(authObserver)
    }

    override fun onStop() {
        super.onStop()

        // We need to nullify it here otherwise it leaks the context
        azureAuthVM = null
        authObserver = null
    }
}

Поскольку я очень часто использую этот подход в проекте тогда я создал эту служебную функцию. Это, на мой взгляд, root причина утечки памяти. Это в файле где-то в проекте (вне действия).

import com.atco.logger.Logger
import io.reactivex.Observer
import io.reactivex.disposables.Disposable

inline fun <T : Any> createDefaultObserver(logger: Logger, crossinline onNext: T.() -> Unit) = object : Observer<T> {
    override fun onComplete() {}

    override fun onSubscribe(d: Disposable) {}

    override fun onNext(t: T) {
        onNext(t)
    }

    override fun onError(e: Throwable) {
        logger.logException(e)
    }
}

Наконец, это то, что Leakcanary регистрирует для меня. Это указывает на 888 утечки. Я прочитал много документов и посмотрел много ответов на Stackoverflow, но не мог точно понять, в чем проблема.

====================================
    HEAP ANALYSIS RESULT
    ====================================
    1 APPLICATION LEAKS

    References underlined with "~~~" are likely causes.
    Learn more at https://squ.re/leaks.

    204736 bytes retained by leaking objects
    Signature: d477dd791e60b0167ba58d241bd7f6ce875a33d4
    ┬───
    │ GC Root: System class
    │
    ├─ android.provider.FontsContract class
    │    Leaking: NO (SiteFinderApplication↓ is not leaking and a class is never leaking)
    │    ↓ static FontsContract.sContext
    ├─ com.atco.forsite.app.SiteFinderApplication instance
    │    Leaking: NO (SiteFinderApplication↓ is not leaking and Application is a singleton)
    │    SiteFinderApplication does not wrap an activity context
    │    ↓ SiteFinderApplication.shadow$_klass_
    ├─ com.atco.forsite.app.SiteFinderApplication class
    │    Leaking: NO (a class is never leaking)
    │    ↓ static SiteFinderApplication.appComponent
    │                                   ~~~~~~~~~~~~
    ├─ com.atco.forsite.di.DaggerAppComponent instance
    │    Leaking: UNKNOWN
    │    ↓ DaggerAppComponent.provideAzureAuthTokenProvider
    │                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    ├─ dagger.internal.DoubleCheck instance
    │    Leaking: UNKNOWN
    │    ↓ DoubleCheck.instance
    │                  ~~~~~~~~
    ├─ com.atco.auth.AuthViewModel instance
    │    Leaking: UNKNOWN
    │    ↓ AuthViewModel.relay
    │                    ~~~~~
    ├─ com.jakewharton.rxrelay2.PublishRelay instance
    │    Leaking: UNKNOWN
    │    ↓ PublishRelay.subscribers
    │                   ~~~~~~~~~~~
    ├─ java.util.concurrent.atomic.AtomicReference instance
    │    Leaking: UNKNOWN
    │    ↓ AtomicReference.value
    │                      ~~~~~
    ├─ com.jakewharton.rxrelay2.PublishRelay$PublishDisposable[] array
    │    Leaking: UNKNOWN
    │    ↓ PublishRelay$PublishDisposable[].[0]
    │                                       ~~~
    ├─ com.jakewharton.rxrelay2.PublishRelay$PublishDisposable instance
    │    Leaking: UNKNOWN
    │    ↓ PublishRelay$PublishDisposable.downstream
    │                                     ~~~~~~~~~~
    ├─ io.reactivex.observers.SafeObserver instance
    │    Leaking: UNKNOWN
    │    ↓ SafeObserver.downstream
    │                   ~~~~~~~~~~
    ├─ com.atco.forsite.app.activity.SiteFinderActivity$$special$$inlined$createDefaultObserver$1 instance
    │    Leaking: UNKNOWN
    │    Anonymous class implementing io.reactivex.Observer
    │    ↓ SiteFinderActivity$$special$$inlined$createDefaultObserver$1.this$0
    │                                                                   ~~~~~~
    ╰→ com.atco.forsite.screens.splash.StartupActivity instance
    ​     Leaking: YES (ObjectWatcher was watching this because com.atco.forsite.screens.splash.StartupActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)
    ​     key = b7c3c771-d399-475a-ab22-a7985eaec020
    ​     watchDurationMillis = 9874
    ​     retainedDurationMillis = 4873
    ====================================
    0 LIBRARY LEAKS

    Library Leaks are leaks coming from the Android Framework or Google libraries.
    ====================================
    METADATA

    Please include this in bug reports and Stack Overflow questions.

    Build.VERSION.SDK_INT: 29
    Build.MANUFACTURER: Google
    LeakCanary version: 2.2
    App process name: com.atco.forsite
    Analysis duration: 13179 ms
    Heap dump file path: /data/user/0/com.atco.forsite/files/leakcanary/2020-03-13_11-58-39_932.hprof
    Heap dump timestamp: 1584122335006
    ====================================

1 Ответ

0 голосов
/ 18 марта 2020

Этот фрагмент кода определенно неверен:

    val weakThis = WeakReference(this@SiteFinderActivity)
    if (weakThis.get() == null) return@createDefaultObserver

Поскольку нет смысла создавать слабый реф для экземпляра и сразу проверять на ноль. Если слабая ссылка здесь была решением, то она должна быть создана заранее, чтобы обратный вызов сохранял ссылку только на слабую ссылку вместо действия.

При этом я думаю, что слабая ссылка ненужным. Ключевая проблема здесь заключается в том, что в onStop вы должны очистить подписку вместо того, чтобы просто установить наблюдателя в ноль.

...