Skip to content

Commit f3a813a

Browse files
committed
Ignore stale image callbacks after an Image view is recycled on iOS
Create a new observer proxy per subscription and ignore callbacks from an older one, so a late image for a recycled view's previous source is not applied.
1 parent b71d466 commit f3a813a

1 file changed

Lines changed: 13 additions & 5 deletions

File tree

‎packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ - (instancetype)initWithFrame:(CGRect)frame
3838
_imageView.layer.minificationFilter = kCAFilterTrilinear;
3939
_imageView.layer.magnificationFilter = kCAFilterTrilinear;
4040

41-
_imageResponseObserverProxy = std::make_shared<RCTImageResponseObserverProxy>(self);
42-
4341
self.contentView = _imageView;
4442
}
4543

@@ -124,6 +122,11 @@ - (void)_setStateAndResubscribeImageResponseObserver:(const ImageShadowNode::Con
124122
_state = state;
125123

126124
if (_state) {
125+
// A new observer per subscription: callbacks of a previous request can still be queued on the
126+
// main queue (e.g. after this view was recycled and reused), and must not be applied here.
127+
// The callbacks are matched by the proxy's address. The new proxy is allocated before the
128+
// previous one is released, so two consecutive subscriptions never share an address.
129+
_imageResponseObserverProxy = std::make_shared<RCTImageResponseObserverProxy>(self);
127130
auto &observerCoordinator = _state->getData().getImageRequest().getObserverCoordinator();
128131
observerCoordinator.addObserver(_imageResponseObserverProxy);
129132
}
@@ -140,8 +143,9 @@ - (void)prepareForRecycle
140143

141144
- (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(const void *)observer
142145
{
143-
if (!_eventEmitter || !_state) {
144-
// Notifications are delivered asynchronously and might arrive after the view is already recycled.
146+
if (!_eventEmitter || !_state || observer != _imageResponseObserverProxy.get()) {
147+
// Notifications are delivered asynchronously and might arrive after the view is already recycled,
148+
// or after it has been reused for another image.
145149
// In the future, we should incorporate an `EventEmitter` into a separate object owned by `ImageRequest` or `State`.
146150
// See for more info: T46311063.
147151
return;
@@ -187,7 +191,7 @@ - (void)didReceiveProgress:(float)progress
187191
total:(int64_t)total
188192
fromObserver:(const void *)observer
189193
{
190-
if (!_eventEmitter) {
194+
if (!_eventEmitter || observer != _imageResponseObserverProxy.get()) {
191195
return;
192196
}
193197

@@ -196,6 +200,10 @@ - (void)didReceiveProgress:(float)progress
196200

197201
- (void)didReceiveFailure:(NSError *)error fromObserver:(const void *)observer
198202
{
203+
if (observer != _imageResponseObserverProxy.get()) {
204+
return;
205+
}
206+
199207
_imageView.image = nil;
200208

201209
if (!_eventEmitter) {

0 commit comments

Comments
 (0)