From 1d452b17bf16bfca04daaf6fef7911193e022f86 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 14 Jul 2025 01:11:54 -0700 Subject: [PATCH] Release cached images when image component gets recycled on iOS (#51493) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51493 Changelog: [IOS][FIXED] Don't retain cached images in state after `RCTImageComponentView` gets recycled Fixes https://github.com/facebook/react-native/issues/51198 Crosspost from the task comment: From what I've been able to figure out, it seems like the image shadow nodes (keeping the loaded image in state) are being kept in memory by shadow node reference wrappers. It doesn't seem strictly like a memory leak - manually triggering garbage collection causes those nodes to be deallocated, but since Hermes isn't aware of the memory they are retaining, I think, it doesn't trigger it automatically. This diff releases the image data when the observers are notified and adds a new (`Consumed`) status to signify that. Reviewed By: sammy-SC Differential Revision: D75137263 fbshipit-source-id: 97eda7e6d1ef5cd633c4a5a4c37babc5e08968fb --- .../react/renderer/imagemanager/ImageResponse.h | 1 + .../ImageResponseObserverCoordinator.cpp | 16 +++++++++++++++- .../ImageResponseObserverCoordinator.h | 5 +++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/ImageResponse.h b/packages/react-native/ReactCommon/react/renderer/imagemanager/ImageResponse.h index 4f56688b082..2e6ab4455db 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/ImageResponse.h +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/ImageResponse.h @@ -21,6 +21,7 @@ class ImageResponse final { Completed, Failed, Cancelled, + Consumed, }; ImageResponse(std::shared_ptr image, std::shared_ptr metadata); diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/ImageResponseObserverCoordinator.cpp b/packages/react-native/ReactCommon/react/renderer/imagemanager/ImageResponseObserverCoordinator.cpp index 5b3e2574a9f..bbe23fa9565 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/ImageResponseObserverCoordinator.cpp +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/ImageResponseObserverCoordinator.cpp @@ -10,6 +10,7 @@ #include #include +#include namespace facebook::react { @@ -31,6 +32,7 @@ void ImageResponseObserverCoordinator::addObserver( case ImageResponse::Status::Completed: { auto imageData = imageData_; auto imageMetadata = imageMetadata_; + consumeResponse(); mutex_.unlock(); observer.didReceiveImage(ImageResponse{imageData, imageMetadata}); break; @@ -41,7 +43,8 @@ void ImageResponseObserverCoordinator::addObserver( observer.didReceiveFailure(ImageLoadError{imageErrorData}); break; } - case ImageResponse::Status::Cancelled: { + case ImageResponse::Status::Cancelled: + case ImageResponse::Status::Consumed: { observers_.push_back(&observer); status_ = ImageResponse::Status::Loading; mutex_.unlock(); @@ -93,6 +96,9 @@ void ImageResponseObserverCoordinator::nativeImageResponseComplete( status_ == ImageResponse::Status::Cancelled); status_ = ImageResponse::Status::Completed; auto observers = observers_; + if (!observers.empty()) { + consumeResponse(); + } mutex_.unlock(); for (auto observer : observers) { @@ -116,4 +122,12 @@ void ImageResponseObserverCoordinator::nativeImageResponseFailed( } } +void ImageResponseObserverCoordinator::consumeResponse() const { + if (ReactNativeFeatureFlags::releaseImageDataWhenConsumed()) { + status_ = ImageResponse::Status::Consumed; + imageData_.reset(); + imageMetadata_.reset(); + } +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/ImageResponseObserverCoordinator.h b/packages/react-native/ReactCommon/react/renderer/imagemanager/ImageResponseObserverCoordinator.h index 3aae8ee9ab6..f70ac494707 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/ImageResponseObserverCoordinator.h +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/ImageResponseObserverCoordinator.h @@ -61,6 +61,11 @@ class ImageResponseObserverCoordinator { void nativeImageResponseFailed(const ImageLoadError& loadError) const; private: + /* + * Resets the cached image data pointers. Needs to be protected by mutex_. + */ + void consumeResponse() const; + /* * List of observers. * Mutable: protected by mutex_.