Утечка памяти в фрагменте
Я использую библиотеку 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
Ответы
Ответ 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);
}
}