Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(iOS,Fabric): prevent memory leak by calling
invalidate
on delet…
…ed screens (#2402) ## Description On new architecture there is no callback that notifies components that they're deleted and won't be used anymore. `RCTComponentViewProtocol#prepareForRecycle` was supposed to fulfill this role, however after it became possible to disable view recycling for given class of components, it is not always called. In our case, we need such callback in `Screen` to break the retain (strong reference) cycle between `ScreenView` and its `Screen` (controller), otherwise we leak the `RNSScreenView` and `RNSScreen` instances. ## Changes Overrode the `mountingTransactionWillMount:withSurfaceTelemetry:` in `RNSScreenStack`, where screens that are meant to receive `Delete` mutation are retained, and later in `mountingTransactionDidMount:withSurfaceTelemetry:` we dispatch a series of `invalidate` calls & release the components. ## Before <img width="557" alt="image" src="https://github.com/user-attachments/assets/546050e2-5eeb-4c2f-b0f9-5d1d4212889d"> ## After <img width="539" alt="image" src="https://github.com/user-attachments/assets/e92a2778-b7c0-41e6-9ebb-68a8270aa786"> > [!note] > Please note, that these screenshots are done with a patch presented below 👇🏻, w/o it, the memory leak is not that big. ## Test code and steps to reproduce Added `TestMemoryLeak` test screen to our example app. The easiest way to test this is to apply following patch: <details> <summary>See bigFatMemoryChunk</summary> ```c diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm index 6d069499f..0e3a572b5 100644 --- a/ios/RNSScreen.mm +++ b/ios/RNSScreen.mm @@ -61,6 +61,7 @@ constexpr NSInteger SHEET_LARGEST_UNDIMMED_DETENT_NONE = -1; @implementation RNSScreenView { __weak ReactScrollViewBase *_sheetsScrollView; BOOL _didSetSheetAllowedDetentsOnController; + NSMutableArray<UIView *> *bigFatMemoryChunk; #ifdef RCT_NEW_ARCH_ENABLED RCTSurfaceTouchHandler *_touchHandler; react::RNSScreenShadowNode::ConcreteState::Shared _state; @@ -89,7 +90,12 @@ constexpr NSInteger SHEET_LARGEST_UNDIMMED_DETENT_NONE = -1; _props = defaultProps; _reactSubviews = [NSMutableArray new]; _contentWrapper = nil; + bigFatMemoryChunk = [[NSMutableArray alloc] init]; + for (int i = 0; i < 1024 * 5; ++i) { + [bigFatMemoryChunk addObject:[[UIView alloc] initWithFrame:frame]]; + } [self initCommonProps]; +// NSLog(@"[ALLOC][%ld] %p, memory chunk at %p, %@", self.tag, self, bigFatMemoryChunk, bigFatMemoryChunk[1023]); } return self; } @@ -753,6 +759,7 @@ constexpr NSInteger SHEET_LARGEST_UNDIMMED_DETENT_NONE = -1; { _controller = nil; [_sheetsScrollView removeObserver:self forKeyPath:@"bounds" context:nil]; + [bigFatMemoryChunk removeAllObjects]; } #if !TARGET_OS_TV && !TARGET_OS_VISION ``` </details> Try to remove call to `[strongSelf invalidate]` in `mountingTransactionDidMount:withSurfaceTelemetry:` and see that the screens are indeed retained indefinitely. ## Checklist - [x] Included code example that can be used to test this change - [ ] Ensured that CI passes
- Loading branch information