Skip to content

Conversation

@intmain
Copy link

@intmain intmain commented Dec 6, 2025

Description

This PR fixes critical memory leaks in RNSScreen that occur when React Native is completely dismissed in brownfield applications. In brownfield environments, when RN views and the Hermes engine are fully torn down, RNSScreen and related objects were not being deallocated, causing significant memory leaks.

Root causes identified:

  1. Retain cycle between RNSScreen ↔ RNSScreenView: UIViewController holds a strong reference to its view, and RNSScreenView holds a strong reference back to controller
  2. CADisplayLink retain cycle: CADisplayLink holds a strong reference to its target (self)
  3. Missing cleanup: No dealloc method and incomplete timer cleanup in setupProgressNotification
  4. Lifecycle management: Controller needs temporary strong reference until parent takes ownership

Fixes memory leaks in brownfield React Native environments.

Changes

  • Changed RNSScreenView.controller property from strong to weak reference
  • Added _retainedController internal property to temporarily hold strong reference until parentViewController takes ownership
  • Implemented RNSWeakProxy class using NSProxy pattern to break CADisplayLink retain cycle
  • Added cleanup logic at the start of setupProgressNotification to handle multiple viewWillDisappear calls
  • Added dealloc method to RNSScreen to properly clean up _animationTimer
  • Added nil assignment after invalidate in completion block
  • Release _retainedController in willMoveToParentViewController when parent takes ownership

Test code and steps to reproduce

Testing for memory leaks in brownfield environment:

Prerequisites: Brownfield iOS app with React Native integration

Steps:

  1. Integrate this version of react-native-screens in your brownfield app
  2. Present a React Native screen
  3. Completely dismiss the React Native screen (tear down entire RN instance)
  4. Use Xcode Instruments with "Leaks" template or check Allocations

Verification:
You can add a temporary log to verify dealloc is called:

// In RNSScreen.mm
- (void)dealloc
{
  NSLog(@"✅ RNSScreen dealloc called - memory leak fixed!");
  [_animationTimer invalidate];
  _animationTimer = nil;
}

When you completely dismiss RN, you should see the log message in console.

Expected behavior:

  • Before fix: RNSScreen remains in memory, dealloc never called
  • After fix: RNSScreen is properly deallocated, log appears in console

Testing in FabricExample:

cd FabricExample
yarn install
cd ios && pod install && cd ..
yarn ios
# Navigate through screens and observe memory behavior

Checklist

  • Included code example that can be used to test this change ✅ (Testing instructions above)
  • Updated TS types ❌ (Not applicable - iOS-only memory management fix)
  • Updated documentation: ❌ (Not applicable - internal implementation fix, no API changes)
    • GUIDE_FOR_LIBRARY_AUTHORS.md
    • native-stack/README.md
    • src/types.tsx
    • src/native-stack/types.tsx
  • Ensured that CI passes ⏳ (Will verify after PR submission)

Note: This is an iOS-only internal implementation fix with no API changes, so TypeScript types and documentation updates are not required.

This commit fixes critical memory leaks in RNSScreen that occur when React Native
is completely dismissed in brownfield applications, preventing RNSScreen and Hermes
from being deallocated.

Root causes addressed:
1. Retain cycle between RNSScreen and RNSScreenView
   - Changed controller property from strong to weak reference
   - Added _retainedController to temporarily hold strong reference until parent takes ownership

2. Retain cycle with CADisplayLink in setupProgressNotification
   - Implemented RNSWeakProxy pattern to break the strong reference cycle
   - CADisplayLink now holds weak reference to RNSScreen through proxy

3. Missing cleanup of _animationTimer
   - Added invalidation at the start of setupProgressNotification (handles multiple viewWillDisappear calls)
   - Added dealloc method to RNSScreen to clean up _animationTimer
   - Added nil assignment after invalidation in completion block

4. Proper lifetime management
   - Release _retainedController in willMoveToParentViewController when parent takes ownership

Tested in brownfield environment with complete RN teardown - verified all memory leaks resolved.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant