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
PureComponent
help, instead of usingComponent
. Just a thought, as it minimises the number of re-renders, could help a little bit with memory leaks. – Bosch