We are encountering an issue where our React Native Android application crashes on certain devices due to a memory leak. While it works perfectly on most devices, roughly 25% of users have reported this crash. The problem has been tracked via Crashlytics, and upon further investigation using LeakCanary, it appears that the memory leak occurs when navigating between screens, either bottom tabs or stack navigation.
Repo that Demonstrates the issue Github Link
Below is the navigation structure:
// main navigation
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Auth" component={Auth} />
<Stack.Screen name="App" component={Drawer} />
</Stack.Navigator>
</NavigationContainer>
// drawer
<Drawer.Navigator drawerContent={(props) => <DrawerContent {...props} />}>
<Drawer.Screen name="Main" component={BottomTabs} />
</Drawer.Navigator>
// Bottom Tabs
<BottomTab.Navigator>
<BottomTab.Screen name="Tab1" component={Stack1} />
<BottomTab.Screen name="Tab2" component={Stack2} />
<BottomTab.Screen name="Tab3" component={Stack3} />
<BottomTab.Screen name="Tab4" component={Stack4} />
</BottomTab.Navigator>
// Stack 1
<Stack.Navigator>
<Stack.Screen name="Main" component={Screen} />
<Stack.Screen name="Screen2" component={Screen2} />
<Stack.Screen name="Screen3" component={Screen3} />
<Stack.Screen name="Screen4" component={Screen4} />
<Stack.Screen name="Screen5" component={Screen5} />
</Stack.Navigator>
Issue Logcat
┬───
│ GC Root: Thread object
│
├─ android.net.ConnectivityThread instance
│ Leaking: NO (PathClassLoader↓ is not leaking)
│ Thread name: 'ConnectivityThread'
│ ↓ Thread.contextClassLoader
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never
│ leaking)
│ ↓ ClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (InternalLeakCanary↓ is not leaking)
│ ↓ Object[1048]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.appname.MainActivity instance
│ Leaking: NO (Activity#mDestroyed is false)
│ mApplication instance of com.appname.MainApplication
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper
│ ↓ AppCompatActivity.mDelegate
│ ~~~~~~~~~
├─ androidx.appcompat.app.AppCompatDelegateImpl instance
│ Leaking: UNKNOWN
│ Retaining 1.1 kB in 16 objects
│ mAppCompatCallback instance of com.appname.MainActivity with
│ mDestroyed = false
│ mContext instance of com.appname.MainActivity with mDestroyed = false
│ mHost instance of com.appname.MainActivity with mDestroyed = false
│ ↓ AppCompatDelegateImpl.mActionBar
│ ~~~~~~~~~~
├─ androidx.appcompat.app.ToolbarActionBar instance
│ Leaking: UNKNOWN
│ Retaining 5.7 MB in 12165 objects
│ ↓ ToolbarActionBar.mDecorToolbar
│ ~~~~~~~~~~~~~
├─ androidx.appcompat.widget.ToolbarWidgetWrapper instance
│ Leaking: UNKNOWN
│ Retaining 5.7 MB in 12161 objects
│ ↓ ToolbarWidgetWrapper.mToolbar
│ ~~~~~~~~
├─ com.swmansion.rnscreens.ScreenStackHeaderConfig$DebugMenuToolbar instance
│ Leaking: UNKNOWN
│ Retaining 5.7 MB in 12148 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mWindowAttachCount = 2
│ mPopupContext instance of com.facebook.react.uimanager.ThemedReactContext,
│ wrapping activity com.appname.MainActivity with mDestroyed = false
│ mContext instance of com.facebook.react.uimanager.ThemedReactContext,
│ wrapping activity com.appname.MainActivity with mDestroyed = false
│ ↓ View.mParent
│ ~~~~~~~
├─ com.google.android.material.appbar.AppBarLayout instance
│ Leaking: UNKNOWN
│ Retaining 3.3 kB in 80 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mWindowAttachCount = 1
│ mContext instance of com.appname.MainActivity with mDestroyed = false
│ ↓ View.mParent
│ ~~~~~~~
╰→ com.swmansion.rnscreens.ScreenStackFragment$ScreensCoordinatorLayout instance
Leaking: YES (ObjectWatcher was watching this because com.swmansion.
rnscreens.ScreenStackFragment received Fragment#onDestroyView() callback
(references to its views should be cleared to prevent leaks))
Retaining 2.8 kB in 74 objects
key = edfb8295-6373-4ec3-b16b-565e1448a34d
watchDurationMillis = 6377
retainedDurationMillis = 1377
View not part of a window view hierarchy
View.mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
mContext instance of com.appname.MainActivity with mDestroyed = false
METADATA
Build.VERSION.SDK_INT: 33
Build.MANUFACTURER: Google
LeakCanary version: 2.11
App process name: com.appname
Class count: 27700
Instance count: 268061
Primitive array count: 148295
Object array count: 41071
Thread count: 56
Heap total bytes: 32668238
Bitmap count: 83
Bitmap total bytes: 24208979
Large bitmap count: 0
Large bitmap total bytes: 0
Db 1: open /data/user/0/com.appnamw/databases/com.
google.android.datatransport.events
Db 2: open /data/user/0/com.
appname/databases/RKStorage
Db 3: open /data/user/0/com.appname/no_backup/androidx.work.workdb
Stats: LruCache[maxSize=3000,hits=162815,misses=288528,hitRate=36%]
RandomAccess[bytes=14810283,reads=288528,travel=86887776715,range=38641306,size=
47641520]
Analysis duration: 13551 ms
│ GC Root: Thread object
│
├─ android.net.ConnectivityThread instance
│ Leaking: NO (PathClassLoader↓ is not leaking)
│ Thread name: 'ConnectivityThread'
│ ↓ Thread.contextClassLoader
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never
│ leaking)
│ ↓ ClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (InternalLeakCanary↓ is not leaking)
│ ↓ Object[2191]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.appname.MainActivity instance
│ Leaking: NO (Activity#mDestroyed is false)
│ mApplication instance of com.appname.MainApplication
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper
│ ↓ AppCompatActivity.mDelegate
│ ~~~
├─ androidx.appcompat.app.AppCompatDelegateImpl instance
│ Leaking: UNKNOWN
│ Retaining 1.0 kB in 16 objects
│ mAppCompatCallback instance of com.appname.MainActivity with
│ mDestroyed = false
│ mContext instance of com.appname.MainActivity with mDestroyed = false
│ mHost instance of com.appname.MainActivity with mDestroyed = false
│ ↓ AppCompatDelegateImpl.mActionBar
│ ~~~~
├─ androidx.appcompat.app.ToolbarActionBar instance
│ Leaking: UNKNOWN
│ Retaining 552.4 kB in 2395 objects
│ ↓ ToolbarActionBar.mDecorToolbar
│ ~~~~~
├─ androidx.appcompat.widget.ToolbarWidgetWrapper instance
│ Leaking: UNKNOWN
│ Retaining 552.4 kB in 2391 objects
│ ↓ ToolbarWidgetWrapper.mToolbar
│ ~~~~
├─ com.swmansion.rnscreens.ScreenStackHeaderConfig$DebugMenuToolbar instance
│ Leaking: UNKNOWN
│ Retaining 552.2 kB in 2387 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mWindowAttachCount = 1
│ mPopupContext instance of com.facebook.react.uimanager.ThemedReactContext,
│ wrapping activity com.appname.MainActivity with mDestroyed = false
│ mContext instance of com.facebook.react.uimanager.ThemedReactContext,
│ wrapping activity com.appname.MainActivity with mDestroyed = false
│ ↓ CustomToolbar.config
│ ~~
├─ com.swmansion.rnscreens.ScreenStackHeaderConfig instance
│ Leaking: UNKNOWN
│ Retaining 2.0 kB in 14 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.null
│ View.mWindowAttachCount = 1
│ mContext instance of com.facebook.react.uimanager.ThemedReactContext,
│ wrapping activity com.appname.MainActivity with mDestroyed = false
│ ↓ View.mParent
│ ~~~
├─ com.swmansion.rnscreens.Screen instance
│ Leaking: UNKNOWN
│ Retaining 511.8 kB in 1829 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.null
│ View.mWindowAttachCount = 1
│ mContext instance of com.facebook.react.uimanager.ThemedReactContext,
│ wrapping activity com.appname.MainActivity with mDestroyed = false
│ ↓ Screen.fragment
│ ~~~~
╰→ com.swmansion.rnscreens.ScreenStackFragment instance
Leaking: YES (ObjectWatcher was watching this because com.swmansion.
rnscreens.ScreenStackFragment received Fragment#onDestroy() callback and
Fragment#mFragmentManager is null)
Retaining 2.1 kB in 72 objects
key = b116c7d1-e55c-4a42-9170-eca82ba9dd7d
watchDurationMillis = 7292
retainedDurationMillis = 2249
METADATA
Build.VERSION.SDK_INT: 28
Build.MANUFACTURER: HUAWEI
LeakCanary version: 2.11
App process name: appname
Class
───
│ GC Root: Global variable in native code
│
├─ com.swmansion.reanimated.NativeProxy instance
│ Leaking: UNKNOWN
│ Retaining 221 B in 8 objects
│ ↓ NativeProxyCommon.mNodesManager
│ ~~~~~
├─ com.swmansion.reanimated.NodesManager instance
│ Leaking: UNKNOWN
│ Retaining 9.4 kB in 318 objects
│ mContext instance of com.facebook.react.bridge.ReactApplicationContext,
│ wrapping com.appname.MainApplication
│ mReactApplicationContext instance of com.facebook.react.bridge.
│ ReactApplicationContext, wrapping com.appname.MainApplication
│ ↓ NodesManager.mAnimationManager
│ ~~~~~~~
├─ com.swmansion.reanimated.layoutReanimation.AnimationsManager instance
│ Leaking: UNKNOWN
│ Retaining 794 B in 24 objects
│ mContext instance of com.facebook.react.bridge.ReactApplicationContext,
│ wrapping com.appname.MainApplication
│ ↓ AnimationsManager.mReanimatedNativeHierarchyManager
│ ~~~~~~~~~~~
├─ com.swmansion.reanimated.layoutReanimation.ReanimatedNativeHierarchyManager
│ instance
│ Leaking: UNKNOWN
│ Retaining 942.6 kB in 6901 objects
│ ↓ NativeViewHierarchyManager.mTagsToViews
│ ~~~~
├─ android.util.SparseArray instance
│ Leaking: UNKNOWN
│ Retaining 929.7 kB in 6881 objects
│ ↓ SparseArray.mValues
│ ~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ Retaining 923.6 kB in 6879 objects
│ ↓ Object[217]
│ ~~~
├─ com.swmansion.rnscreens.Screen instance
│ Leaking: UNKNOWN
│ Retaining 2.2 kB in 17 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.null
│ View.mWindowAttachCount = 5
│ mContext instance of com.facebook.react.uimanager.ThemedReactContext,
│ wrapping activity com.appname.MainActivity with mDestroyed = false
│ ↓ View.mParent
│ ~~~
╰→ com.swmansion.rnscreens.ScreenStackFragment$ScreensCoordinatorLayout instance
Leaking: YES (ObjectWatcher was watching this because com.swmansion.
rnscreens.ScreenStackFragment received Fragment#onDestroyView() callback
(references to its views should be cleared to prevent leaks))
Retaining 3.7 kB in 72 objects
key = 8b340022-4f6e-4653-a6c8-ad5639e3ff8e
watchDurationMillis = 5828
retainedDurationMillis = 827
View not part of a window view hierarchy
View.mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
mContext instance of com.appname.MainActivity with mDestroyed = false
METADATA
Build.VERSION.SDK_INT: 28
Build.MANUFACTURER: HUAWEI
LeakCanary version: 2.11
App process name:appname
Class count: 18907
Instance count: 266551
Primitive array count: 172560
Object array count: 29962
Thread count: 57
Heap total bytes: 26966868
Bitmap count: 75
Bitmap total bytes: 14020308
Large bitmap count: 0
Large bitmap total bytes: 0
Db 1: open /data/user/0/appnamer/databases/RKStorage
Db 2: open /data/user/0/com.appname/databases/com.
google.android.datatransport.events
Db 3: open /data/user/0/com.appname/no_backup/androidx.work.workdb
Stats: LruCache[maxSize=3000,hits=84650,misses=180148,hitRate=31%]
RandomAccess[bytes=9194085,reads=180148,travel=66525003196,range=33782477,size=4
0598308]
Analysis duration: 15559 ms
Solutions attempted so far without success:
// it fixes the leak when navigating between the ButtomTabs
// but it leaks when navigating in the stack(ex:to screen2)
- enableScreens(false)
- super.onCreate(null);
Module Versions:
"react-native-screens": "^3.22.0",
"@react-navigation/bottom-tabs": "^6.3.2",
"@react-navigation/drawer": "^6.4.4",
"@react-navigation/native": "^6.0.11",
"@react-navigation/native-stack": "^6.7.0",
"react-native-reanimated": "^3.3.0",
"react-native-gesture-handler": "^2.12.0",
The problem was initially reported three years ago during the v4 release. Despite the updates, it continues to persist in v6. I suspect the root of the issue lies in the interaction between react-native-screens
, react-native-reanimated
, and react-navigation
. Any solutions or workarounds to address this challenge would be greatly appreciated.
Related reports
2
Answers
It is not a actual memory leak
The behavior described by the tools as a leak is the consequence of keeping the
ScreenFragment
s in the memory. It is done like this because, inreact-native
, we cannot destroy and then make new views by restoring the state of theFragment
, since each view has itsreactTag
etc. The behavior is shown as a leak due to heuristics of the leak detector tools, which say that ifonDestroy
was called on aFragment
, then the reference to it should not be kept anywhere, but, as mentioned above, it is not applicable toreact-native
apps, since we do not recreate the views of theFragment
, but rathercall
remove
on the them when they become invisible and thenadd
them back on theScreen
becoming visible with the sameScreen
attached to it.Hopefully this will resolve your issue
Referance from developer of Software Mansion https://github.com/software-mansion/react-native-screens/issues/843#issuecomment-832034119
Referance from react-native-screens
The use of navigation listeners that are not properly removed when the component unmounts is a typical cause of memory leaks in React Navigation. Here is some sample code that shows how to properly handle navigation listeners:
Using the addListener method offered by React Navigation, we build a custom hook called useNavigationListeners that connects a navigation listener. When the Stack1Screen component mounts, the listener is attached using the useNavigationListeners hook. By returning a cleaning function from the useEffect hook, the listener is appropriately unmounted when the component unmounts.
To avoid memory leaks that can happen if the navigation listeners are not properly cleaned up, you make sure they are properly removed when the related components are unmounted.