react-native-screens: [Android] Memory leak leading to intermittent crashing

It looks like a memory leak in MainActivity related to the ScreenFragment onDestroy function is causing app crashes.

We have a Tab Navigator with a nested Auth Stack Navigator for login flows. We are using react-native-screens with enableScreens() at the top of App.tsx outside of the root component.

When we turn off enableScreens() the memory leak goes away. But this causes exceptionally poor performance on Android 8 and lower-end devices.

{
    "react-native-screens": "^2.11.0",
    "react-native": "0.63.3",
    "@react-navigation/native": "^5.7.6",
    "@react-navigation/stack": "^5.9.3",
}

LeakCanary report:

┬───
│ GC Root: Global variable in native code
│
├─ com.facebook.react.bridge.JavaModuleWrapper instance
│    Leaking: UNKNOWN
│    ↓ JavaModuleWrapper.mModuleHolder
│                        ~~~~~~~~~~~~~
├─ com.facebook.react.bridge.ModuleHolder instance
│    Leaking: UNKNOWN
│    ↓ ModuleHolder.mModule
│                   ~~~~~~~
├─ com.facebook.react.uimanager.UIManagerModule instance
│    Leaking: UNKNOWN
│    ↓ UIManagerModule.mUIImplementation
│                      ~~~~~~~~~~~~~~~~~
├─ com.facebook.react.uimanager.UIImplementation instance
│    Leaking: UNKNOWN
│    ↓ UIImplementation.mOperationsQueue
│                       ~~~~~~~~~~~~~~~~
├─ com.facebook.react.uimanager.UIViewOperationQueue instance
│    Leaking: UNKNOWN
│    ↓ UIViewOperationQueue.mNativeViewHierarchyManager
│                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~
├─ com.facebook.react.uimanager.NativeViewHierarchyManager instance
│    Leaking: UNKNOWN
│    ↓ NativeViewHierarchyManager.mTagsToViews
│                                 ~~~~~~~~~~~~
├─ android.util.SparseArray instance
│    Leaking: UNKNOWN
│    ↓ SparseArray.mValues
│                  ~~~~~~~
├─ java.lang.Object[] array
│    Leaking: UNKNOWN
│    ↓ Object[].[65]
│               ~~~~
├─ com.swmansion.rnscreens.Screen instance
│    Leaking: YES (View detached and has parent)
│    mContext instance of com.facebook.react.uimanager.ThemedReactContext, wrapping activity com.XXXX.MainActivity with mDestroyed = false
│    View#mParent is set
│    View#mAttachInfo is null (view detached)
│    View.mID = R.id.null
│    View.mWindowAttachCount = 1
│    ↓ Screen.mFragment
╰→ com.swmansion.rnscreens.ScreenFragment instance
​     Leaking: YES (ObjectWatcher was watching this because com.swmansion.rnscreens.ScreenFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
​     key = 55523ed9-b81c-4d11-a5b7-42155878b7f6
​     watchDurationMillis = 7610
​     retainedDurationMillis = 2572
METADATA
Build.VERSION.SDK_INT: 29
Build.MANUFACTURER: OnePlus
LeakCanary version: 2.4
App process name: com.XXXX
Analysis duration: 12898 ms

MainTabNavigator.tsx

const Tab = createBottomTabNavigator<MainNavParamList>()

const MainTabNavigator: FC<{}> = () => {
  return (
    <Tab.Navigator initialRouteName={'Home'} tabBarOptions={tabBarOptions}>
      <Tab.Screen
        name="Home"
        component={HomeStack}
        options={{ tabBarLabel: tabBarLabel(I18n.t('Home')), tabBarIcon: tabBarIcon('home-alt') }}
      />
      <Tab.Screen
        name="Menu"
        component={MenuStack}
        options={{ tabBarLabel: tabBarLabel(I18n.t('Menu')), tabBarIcon: tabBarIcon('coffee') }}
      />
      <Tab.Screen
        name="Order"
        component={OrderStack}
        options={{ tabBarLabel: tabBarLabel(I18n.t('Orders')), tabBarIcon: tabBarIcon('receipt') }}
      />
      <Tab.Screen
        name="Account"
        component={AccountStack}
        options={{ tabBarLabel: tabBarLabel(I18n.t('Account')), tabBarIcon: tabBarIcon('user') }}
      />
      <Tab.Screen name="Cart" component={CartStackNavigator} options={{ tabBarButton: () => null, tabBarVisible: false }} />
      <Tab.Screen name="Auth" component={LoginStack} options={{ tabBarButton: () => null }} />
    </Tab.Navigator>
  )
}

Auth Stack Navigator

const Stack = createStackNavigator<AuthStackParamList>()

interface Props {
  navigation?: StackNavigationProp<MainNavParamList, 'Auth'>
  route?: RouteProp<MainNavParamList, 'Auth'>
}

export type LeftAction = ((props: StackHeaderLeftButtonProps) => React.ReactNode) | undefined

const AuthFlowNavigator: React.FC<Props> = (props: Props) => {
  let initialRoute: AuthInitialScreen = 'SignIn'
  if (props.route && props.route.params) {
    initialRoute = props.route.params.screen || initialRoute
  }

  let goBack: LeftAction
  const navigation = props.navigation
  if (navigation) {
    goBack = (stackProps: StackHeaderLeftButtonProps) => (
      <HeaderBackButton
        {...stackProps}
        onPress={() => navigation.goBack()}
        backImage={(imgProps: { tintColor: string }) => <BackIcon tintColor={imgProps.tintColor} />}
      />
    )
  }

  return (
    <Stack.Navigator
      mode="card"
      headerMode="float"
      initialRouteName={initialRoute}
      screenOptions={stackNavigationOptions}>
      <Stack.Screen
        name="SignIn"
        component={SignIn}
        options={{
          headerTitle: I18n.t('Sign in to my Account'),
          headerLeft: goBack,
        }}
      />
      <Stack.Screen
        name="SignUp"
        component={SignUp}
        options={{
          headerTitle: I18n.t('Sign up'),
          headerLeft: initialRoute === 'SignUp' ? goBack : undefined,
        }}
      />
      <Stack.Screen
        name="SignUpWithEmail"
        component={SignUpWithEmail}
        options={{
          headerTitle: I18n.t('Sign up by email'),
        }}
      />
      <Stack.Screen
        name="ConfirmSignUp"
        component={ConfirmSignUp}
        options={{
          headerTitle: I18n.t('Sign up by email'),
        }}
      />
      <Stack.Screen
        name="ForgotPasswordEmail"
        component={ForgotPasswordEmail}
        options={{ headerTitle: I18n.t('Reset password') }}
      />
      <Stack.Screen
        name="ForgotPasswordChallenge"
        component={ForgotPasswordChallenge}
        options={{ headerTitle: I18n.t('Reset password') }}
      />
    </Stack.Navigator>
  )
}

MainActivity.java

import android.os.Bundle;

import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;

import org.devio.rn.splashscreen.SplashScreen;

public class MainActivity extends ReactActivity {

    /**
     * Returns the name of the main component registered from JavaScript. This is used to schedule
     * rendering of the component.
     */
    @Override
    protected String getMainComponentName() {
        return "XXXX";
    }

    @Override
    protected ReactActivityDelegate createReactActivityDelegate() {

        return new ReactActivityDelegate(this, getMainComponentName()) {
            @Override
            protected ReactRootView createRootView() {
                return new RNGestureHandlerEnabledRootView(MainActivity.this);
            }
        };
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        SplashScreen.show(this, R.style.SplashStatusBarTheme);
        super.onCreate(null);
    }
}

The memory dump occurs when you navigate to the Auth Stack. App doesn’t seem to crash, however.

Crashes are reported when user is redirected to Auth Stack when trying to place an order while not logged in. These crashes are intermittent and cannot be reliably induced.

Cheers, Jon

EDIT:

App Center Diagnostics shows this stack trace occuring multiple times, for only Android users:

com.swmansion.rnscreens.ScreenFragment.<init> ScreenFragment.java:44
java.lang.reflect.Constructor.newInstance0 Constructor.java
androidx.fragment.app.Fragment.instantiate Fragment.java:548
androidx.fragment.app.FragmentContainer.instantiate FragmentContainer.java:57
androidx.fragment.app.FragmentManager$3.instantiate FragmentManager.java:390
androidx.fragment.app.FragmentStateManager.<init> FragmentStateManager.java:74
androidx.fragment.app.FragmentManager.restoreSaveState FragmentManager.java:2454
androidx.fragment.app.FragmentController.restoreSaveState FragmentController.java:196
androidx.fragment.app.FragmentActivity.onCreate FragmentActivity.java:287
androidx.appcompat.app.AppCompatActivity.onCreate AppCompatActivity.java:106
com.facebook.react.ReactActivity.onCreate ReactActivity.java:44
com.XXXX.MainActivity.onCreate MainActivity.java:37
android.app.Activity.performCreate Activity.java:8000

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 3
  • Comments: 18 (9 by maintainers)

Most upvoted comments

Any updates on this, I am still facing the same issue

"react-native": "0.63.4",
"react-navigation": "^4.0.0",   
"react-navigation-material-bottom-tabs": "^1.0.0",   
"react-navigation-stack": "^2.10.4",   
"react-navigation-tabs": "^2.3.0",

as @d3vhound said super.onCreate(null) didn’t fix the issue.

Screen Shot 2021-04-22 at 4 32 13 PM

Then I think there is no more to be done unfortunately, as mentioned here: https://github.com/software-mansion/react-native-screens/issues/843#issuecomment-832034119.

Using this tool on a project with a basic react-navigation set-up should show the leaks. I don’t think me or @jmkmay are doing anything out of the ordinary. A auth stack and main stack is what we both have in common. But unfortunately applying super.onCreate(null) doesn’t stop the leaks.

Screen Shot 2021-02-23 at 3 12 24 PM