diff --git a/CHANGELOG.md b/CHANGELOG.md index e564a70cbc..c5d9d8ed52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ - Add `enableNdkAppHangTracking` and `ndkAppHangTimeoutIntervalMillis` options to enable Android NDK app hang tracking ([#6548](https://github.com/getsentry/sentry-react-native/pull/6548)) +### Fixes + +- Fix Android fragment memory leak in `RNSentryReactFragmentLifecycleTracer` ([#6599](https://github.com/getsentry/sentry-react-native/pull/6599)) + ### Dependencies - Bump Android SDK from v8.52.0 to v8.53.0 ([#6586](https://github.com/getsentry/sentry-react-native/pull/6586)) diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryReactFragmentLifecycleTracerTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryReactFragmentLifecycleTracerTest.kt index d7599ea274..c636a60a54 100644 --- a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryReactFragmentLifecycleTracerTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryReactFragmentLifecycleTracerTest.kt @@ -41,6 +41,21 @@ class RNSentryReactFragmentLifecycleTracerTest { verify(mockEventDispatcher, times(1)).addListener(any()) } + @Test + fun tracerRemovesListenerWhenFragmentViewDestroyed() { + val mockEventDispatcher = mock() + val fragment = mock() + + val tracer = createSutWith() + mockUIManager(mockEventDispatcher) + + callOnFragmentViewCreated(fragment, mockScreenViewWithReactContext(), tracer) + verify(mockEventDispatcher, times(1)).addListener(any()) + + callOnFragmentViewDestroyed(fragment, tracer) + verify(mockEventDispatcher, times(1)).removeListener(any()) + } + @Test fun tracerDoesNotAddListenerForGenericFragment() { val mockEventDispatcher = mock() @@ -87,8 +102,9 @@ class RNSentryReactFragmentLifecycleTracerTest { private fun callOnFragmentViewCreated( mockFragment: Fragment, mockView: View, + tracer: RNSentryReactFragmentLifecycleTracer = createSutWith(), ) { - createSutWith().onFragmentViewCreated( + tracer.onFragmentViewCreated( mock(), mockFragment, mockView, @@ -96,6 +112,13 @@ class RNSentryReactFragmentLifecycleTracerTest { ) } + private fun callOnFragmentViewDestroyed( + mockFragment: Fragment, + tracer: RNSentryReactFragmentLifecycleTracer = createSutWith(), + ) { + tracer.onFragmentViewDestroyed(mock(), mockFragment) + } + private fun createSutWith(): RNSentryReactFragmentLifecycleTracer { val logger: ILogger = mock() val buildInfo = BuildInfoProvider(logger) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryReactFragmentLifecycleTracer.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryReactFragmentLifecycleTracer.java index 4ee8700214..c8cbc4bc88 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryReactFragmentLifecycleTracer.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryReactFragmentLifecycleTracer.java @@ -16,6 +16,8 @@ import io.sentry.SentryLevel; import io.sentry.android.core.BuildInfoProvider; import io.sentry.android.core.internal.util.FirstDrawDoneListener; +import java.util.HashMap; +import java.util.Map; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -24,6 +26,7 @@ public class RNSentryReactFragmentLifecycleTracer extends FragmentLifecycleCallb private @NotNull final BuildInfoProvider buildInfoProvider; private @NotNull final Runnable emitNewFrameEvent; private @NotNull final ILogger logger; + private final Map listenerWrapperMap = new HashMap<>(); public RNSentryReactFragmentLifecycleTracer( @NotNull BuildInfoProvider buildInfoProvider, @@ -83,18 +86,40 @@ public void onFragmentViewCreated( return; } - final @NotNull Runnable emitNewFrameEvent = this.emitNewFrameEvent; - eventDispatcher.addListener( - new EventDispatcherListener() { + EventDispatcherListenerWrapper listenerWrapper = + new EventDispatcherListenerWrapper(eventDispatcher) { @Override public void onEventDispatch(Event event) { if ("com.swmansion.rnscreens.events.ScreenAppearEvent" .equals(event.getClass().getCanonicalName())) { - eventDispatcher.removeListener(this); + this.dispatcher.removeListener(this); + listenerWrapperMap.remove(f); FirstDrawDoneListener.registerForNextDraw(v, emitNewFrameEvent, buildInfoProvider); } } - }); + }; + + eventDispatcher.addListener(listenerWrapper); + listenerWrapperMap.put(f, listenerWrapper); + } + + @Override + public void onFragmentViewDestroyed(@NonNull FragmentManager fm, @NonNull Fragment f) { + super.onFragmentViewDestroyed(fm, f); + EventDispatcherListenerWrapper listenerWrapper = listenerWrapperMap.get(f); + if (listenerWrapper != null && listenerWrapper.dispatcher != null) { + listenerWrapper.dispatcher.removeListener(listenerWrapper); + listenerWrapperMap.remove(f); + } + } + + abstract static class EventDispatcherListenerWrapper implements EventDispatcherListener { + + protected final EventDispatcher dispatcher; + + public EventDispatcherListenerWrapper(EventDispatcher dispatcher) { + this.dispatcher = dispatcher; + } } private static @Nullable EventDispatcher getEventDispatcherForReactTag(