mirror of
https://github.com/react/react-native.git
synced 2026-09-28 05:13:06 +08:00
Ignore stale image callbacks after an Image view is recycled on iOS (#58669)
Summary: Fixes https://github.com/react/react-native/issues/58667. On iOS with the new architecture, an `<Image>` can show the image of another `<Image>` that was unmounted just before. `RCTImageResponseObserverProxy` dispatches its callbacks to the main queue. `RCTImageComponentView` used one proxy for its whole lifetime, and `didReceiveImage` only checked that the view still had a state. When a view was recycled and handed to a new `<Image>` while a callback for the old request was still queued, that callback was applied to the new image. If the new source was cached it had already been applied synchronously on subscribe, so the late old image stayed on screen. This change: - creates a new `RCTImageResponseObserverProxy` for every subscription in `_setStateAndResubscribeImageResponseObserver` - ignores image, progress and failure callbacks whose `observer` is not the current proxy (the `fromObserver:` argument was already passed but not used). For failures this also stops a stale error from clearing the new image. The proxy's address identifies the subscription. The new proxy is allocated before the previous one is released, so two consecutive subscriptions never share an address. Reuse of an old address would need two resubscriptions while a callback from the first one is still queued. ## Changelog: [IOS] [FIXED] - Image no longer shows the image of a previous source after its native view is recycled Pull Request resolved: https://github.com/react/react-native/pull/58669 Test Plan: Reproducer: https://github.com/mattijsf/rn-image-recycled-view-stale-image (React Native 0.87.1, also as a Snack: https://snack.expo.dev/mattijsf/image-recycle-stale-load-ios?platform=ios). It mounts 64 uncached remote images (red), replaces them with a cached one (green) as soon as the first red one has loaded, and counts a wrong image when the new image's `onLoad` reports the old image's size. A run is 50 rounds. Release build on the iOS simulator (iPhone 17 Pro Max, iOS 26.5), React Native built from source (`RCT_USE_PREBUILT_RNCORE=0`), one run of 50 rounds each. The diff applies unchanged to 0.87.1. | | rounds with a wrong image | wrong images | | --- | --- | --- | | 0.87.1 | 27 of 50 | 120 | | 0.87.1 with this change | 0 of 50 | 0 | With the change the red images still show up briefly before each swap, as intended, but none of them end up on a green tile. Reviewed By: christophpurrer Differential Revision: D121619393 Pulled By: javache fbshipit-source-id: 78c29450bb25083667515c95f8c704d3b7db5eef
This commit is contained in:
committed by
meta-codesync[bot]
parent
86c2cf1b84
commit
53bf98bf40
+13
-5
@@ -38,8 +38,6 @@ using namespace facebook::react;
|
||||
_imageView.layer.minificationFilter = kCAFilterTrilinear;
|
||||
_imageView.layer.magnificationFilter = kCAFilterTrilinear;
|
||||
|
||||
_imageResponseObserverProxy = std::make_shared<RCTImageResponseObserverProxy>(self);
|
||||
|
||||
self.contentView = _imageView;
|
||||
}
|
||||
|
||||
@@ -124,6 +122,11 @@ using namespace facebook::react;
|
||||
_state = state;
|
||||
|
||||
if (_state) {
|
||||
// A new observer per subscription: callbacks of a previous request can still be queued on the
|
||||
// main queue (e.g. after this view was recycled and reused), and must not be applied here.
|
||||
// The callbacks are matched by the proxy's address. The new proxy is allocated before the
|
||||
// previous one is released, so two consecutive subscriptions never share an address.
|
||||
_imageResponseObserverProxy = std::make_shared<RCTImageResponseObserverProxy>(self);
|
||||
auto &observerCoordinator = _state->getData().getImageRequest().getObserverCoordinator();
|
||||
observerCoordinator.addObserver(_imageResponseObserverProxy);
|
||||
}
|
||||
@@ -140,8 +143,9 @@ using namespace facebook::react;
|
||||
|
||||
- (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(const void *)observer
|
||||
{
|
||||
if (!_eventEmitter || !_state) {
|
||||
// Notifications are delivered asynchronously and might arrive after the view is already recycled.
|
||||
if (!_eventEmitter || !_state || observer != _imageResponseObserverProxy.get()) {
|
||||
// Notifications are delivered asynchronously and might arrive after the view is already recycled,
|
||||
// or after it has been reused for another image.
|
||||
// In the future, we should incorporate an `EventEmitter` into a separate object owned by `ImageRequest` or `State`.
|
||||
// See for more info: T46311063.
|
||||
return;
|
||||
@@ -187,7 +191,7 @@ using namespace facebook::react;
|
||||
total:(int64_t)total
|
||||
fromObserver:(const void *)observer
|
||||
{
|
||||
if (!_eventEmitter) {
|
||||
if (!_eventEmitter || observer != _imageResponseObserverProxy.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -196,6 +200,10 @@ using namespace facebook::react;
|
||||
|
||||
- (void)didReceiveFailure:(NSError *)error fromObserver:(const void *)observer
|
||||
{
|
||||
if (observer != _imageResponseObserverProxy.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
_imageView.image = nil;
|
||||
|
||||
if (!_eventEmitter) {
|
||||
|
||||
Reference in New Issue
Block a user