Подтвердить что ты не робот

Утечка памяти в фрагменте

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

05-09 09:32:14.731  28497-31220/? D/LeakCanary﹕ In com.etiennelawlor.minesweeper:0.0.21:21.
    * com.etiennelawlor.minesweeper.fragments.MinesweeperFragment has leaked:
    * GC ROOT com.google.android.gms.games.internal.GamesClientImpl$PopupLocationInfoBinderCallbacks.zzahO
    * references com.google.android.gms.games.internal.PopupManager$PopupManagerHCMR1.zzajo
    * references com.google.android.gms.games.internal.GamesClientImpl.mContext
    * references com.etiennelawlor.minesweeper.activities.MinesweeperActivity.mFragments
    * references android.app.FragmentManagerImpl.mAdded
    * references java.util.ArrayList.array
    * references array java.lang.Object[].[0]
    * leaks com.etiennelawlor.minesweeper.fragments.MinesweeperFragment instance
    * Reference Key: 2f367393-6dfd-4797-8d85-7ac52c431d07
    * Device: LGE google Nexus 5 hammerhead
    * Android Version: 5.1 API: 22
    * Durations: watch=5015ms, gc=141ms, heap dump=1978ms, analysis=23484ms

Это мое репо: https://github.com/lawloretienne/Minesweeper

Это кажется неуловимым. Я установил Interface для связи между Fragment и Activity. Я установил эту переменную mCoordinator Interface в onAttach(), тогда я понял, что я не обнуляю ее в onDetach(). Я исправил эту проблему, но все еще получаю утечку памяти. Любые идеи?

Update

Я отключил отслеживание утечки Fragment, и я все еще получаю уведомление об активности, протекающей со следующей трассировкой утечки:

05-09 17:07:33.074  12934-14824/? D/LeakCanary﹕ In com.etiennelawlor.minesweeper:0.0.21:21.
    * com.etiennelawlor.minesweeper.activities.MinesweeperActivity has leaked:
    * GC ROOT com.google.android.gms.games.internal.GamesClientImpl$PopupLocationInfoBinderCallbacks.zzahO
    * references com.google.android.gms.games.internal.PopupManager$PopupManagerHCMR1.zzajo
    * references com.google.android.gms.games.internal.GamesClientImpl.mContext
    * leaks com.etiennelawlor.minesweeper.activities.MinesweeperActivity instance
    * Reference Key: f4d06830-0e16-43a2-9750-7e2cb77ae24d
    * Device: LGE google Nexus 5 hammerhead
    * Android Version: 5.1 API: 22
    * Durations: watch=5016ms, gc=164ms, heap dump=3430ms, analysis=39535ms
4b9b3361

Ответ 1

Документация указывает, что безопасно вызывать connect(), даже если состояние "подключено" или "подключено". Он также указывает, что вы можете безопасно называть disconnect() независимо от состояния подключения. Поэтому я бы удалял выражения "if" вокруг вызовов connect() и disconnect(). Тем не менее, я сомневаюсь, что это заставит эту "утечку" уйти.

Понятно, что GamesClientImpl хранит ссылку на ваш Activity как Context. Я предполагаю, что это происходит при построении GoogleApiClient, который происходит, когда вы вызываете GoogleApiClient.Builder.build(). Мне кажется, что ошибка в том, что экземпляр GoogleApiClient по-прежнему находится после завершения вашего Activity. Однако если вы должны называть connect() в onStart() и disconnect() в onStop(), что означает, что вы можете повторно использовать соединение (поскольку onStart() и onStop() могут быть вызваны повторно). Чтобы это сработало, GoogleApiClient должен сохранить ссылку на ваш Context даже после того, как вы вызвали disconnect().

Вы можете попробовать использовать глобальный контекст приложения вместо вашего Activity контекста при создании GoogleApiClient, поскольку глобальный контекст приложения живет вечно (пока процесс не будет убит). Это должно заставить вашу "утечку" уйти:

// Create the Google Api Client with access to Plus and Games
mGoogleApiClient = new GoogleApiClient.Builder(getApplicationContext())
    .addConnectionCallbacks(this)
    .addOnConnectionFailedListener(this)
    .addApi(Plus.API).addScope(Plus.SCOPE_PLUS_LOGIN)
    .addApi(Games.API).addScope(Games.SCOPE_GAMES)
    .build();

Ответ 2

05-27 13:15:04.478  24415-25236/com.package D/LeakCanary﹕ In com.package:0.0.52-dev:202.
* com.package.launcher.LauncherActivity has leaked:
* GC ROOT com.google.android.gms.ads.internal.request.q.a
* references com.google.android.gms.ads.internal.request.m.d
* references com.google.android.gms.ads.internal.request.c.a
* references com.google.android.gms.ads.internal.j.b
* references com.google.android.gms.ads.internal.aa.f
* references com.google.android.gms.ads.internal.ab.mParent
* references com.google.android.gms.ads.doubleclick.PublisherAdView.mParent
* references android.widget.FrameLayout.mContext
* leaks com.package.launcher.LauncherActivity instance
* Reference Key: 9ba3c5ea-2888-4677-9cfa-ebf38444c994
* Device: LGE google Nexus 5 hammerhead
* Android Version: 5.1.1 API: 22
* Durations: watch=5128ms, gc=150ms, heap dump=5149ms, analysis=29741ms

Я использовал библиотеку gms ads, и была аналогичная утечка. Поэтому я исправил вышеупомянутый случай, обработав его onDestroyView() моего фрагмента.

@Override
public void onDestroyView() {

    if (mAdView != null) {
        ViewParent parent = mAdView.getParent();
        if (parent != null && parent instanceof ViewGroup) {
            ((ViewGroup) parent).removeView(mAdView);
        }
    }
    super.onDestroyView();
}

Итак, я в основном удаляю свой PublisherAdView из него parent on onDestroyView().

Также обратите внимание, что мне пришлось использовать контекст приложения, когда я создал PublisherAdView, иначе я бы получил следующую утечку:

05-27 13:59:23.684  10041-11496/com.package D/LeakCanary﹕ In com.package:0.0.52-dev:202.
* com.package.launcher.LauncherActivity has leaked:
* GC ROOT com.google.android.gms.ads.internal.request.q.a
* references com.google.android.gms.ads.internal.request.m.b
* leaks com.package.launcher.LauncherActivity instance
* Reference Key: 5acaa61a-ea04-430a-b405-b734216e7e80
* Device: LGE google Nexus 5 hammerhead
* Android Version: 5.1.1 API: 22
* Durations: watch=7275ms, gc=138ms, heap dump=5260ms, analysis=22447ms

Не уверен, что он решит проблему непосредственно, но надеюсь, что это поможет.

Ответ 3

Вы не должны полагаться на выполнение обратного вызова onDestroy(), в некоторых случаях его нельзя было бы назвать. Более твердое решение состоит в том, чтобы поместить ваш регистр/незарегистрированный код внутри onResume()/onPause().

То же самое идет (по другой причине, конечно) для Fragment onDetach(), переместите свой разумный код на onStop() или onPause().

Ответ 4

В моем случае у меня был следующий код:

googleApiClient = new GoogleApiClient.Builder(activity.getApplicationContext())
            .addConnectionCallbacks(this)
            .addOnConnectionFailedListener(this)
            .addApi(Games.API).addScope(Games.SCOPE_GAMES)
            .build();

Проблема заключалась в вызовах addConnectionCallbacks(this) и addConnectionCallbacks(this). Класс, на который ссылается this, ссылался на активность, и поскольку GoogleApiClient не отпустил ссылки на обратные вызовы/прослушиватели соединения, это привело к утечке памяти.

Моим решением было зарегистрировать/отменить регистрацию обратных вызовов, так как googleApiClient подключается/отключается:

public void connect() {
    mGoogleApiClient.registerConnectionCallbacks(this);
    mGoogleApiClient.registerConnectionFailedListener(this);
    mGoogleApiClient.connect();
}

public void disconnect() {
    if (mGoogleApiClient.isConnected()) {
        mGoogleApiClient.disconnect();
        mGoogleApiClient.unregisterConnectionCallbacks(this);
        mGoogleApiClient.unregisterConnectionFailedListener(this);
    }
}