Skip to content

Commit 52eed26

Browse files
committed
Honor cache: 'reload' for bundled images on iOS so they leave the system image cache
Bundled images are loaded with +[UIImage imageNamed:], whose system cache keeps every image ever shown, even after it is unmounted and across memory warnings. When the image source sets cache: 'reload', the bundle asset loaders now read the file directly instead. Default behavior is unchanged. Fixes #12220
1 parent 7fc911a commit 52eed26

8 files changed

Lines changed: 132 additions & 25 deletions

File tree

‎packages/react-native/Libraries/Image/ImageSource.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ export interface ImageURISource {
5656
*
5757
* - `reload`: The data for the URL will be loaded from the originating source.
5858
* No existing cache data should be used to satisfy a URL load request.
59+
* On iOS, images bundled with the app are then also not kept in the system
60+
* image cache once they are no longer shown.
5961
*
6062
* - `force-cache`: The existing cached data will be used to satisfy the request,
6163
* regardless of its age or expiration date. If there is no existing data in the cache

‎packages/react-native/Libraries/Image/RCTBundleAssetImageLoader.mm‎

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,24 @@
1818
@interface RCTBundleAssetImageLoader () <RCTTurboModule>
1919
@end
2020

21+
static void RCTCompleteLocalAssetLoad(
22+
UIImage *image,
23+
NSURL *imageURL,
24+
RCTImageLoaderProgressBlock progressHandler,
25+
RCTImageLoaderCompletionBlock completionHandler)
26+
{
27+
if (image != nullptr) {
28+
if (progressHandler != nullptr) {
29+
progressHandler(1, 1);
30+
}
31+
completionHandler(nil, image);
32+
} else {
33+
NSString *message = [NSString stringWithFormat:@"Could not find image %@", imageURL];
34+
RCTLogWarn(@"%@", message);
35+
completionHandler(RCTErrorWithMessage(message), nil);
36+
}
37+
}
38+
2139
@implementation RCTBundleAssetImageLoader
2240

2341
RCT_EXPORT_MODULE()
@@ -49,18 +67,26 @@ - (nullable RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL
4967
partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
5068
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
5169
{
52-
UIImage *image = RCTImageFromLocalAssetURL(imageURL);
53-
if (image != nullptr) {
54-
if (progressHandler != nullptr) {
55-
progressHandler(1, 1);
56-
}
57-
completionHandler(nil, image);
58-
} else {
59-
NSString *message = [NSString stringWithFormat:@"Could not find image %@", imageURL];
60-
RCTLogWarn(@"%@", message);
61-
completionHandler(RCTErrorWithMessage(message), nil);
62-
}
70+
RCTCompleteLocalAssetLoad(RCTImageFromLocalAssetURL(imageURL), imageURL, progressHandler, completionHandler);
71+
return nil;
72+
}
6373

74+
- (nullable RCTImageLoaderCancellationBlock)
75+
loadImageIgnoringCacheForURL:(NSURL *)imageURL
76+
size:(CGSize)size
77+
scale:(CGFloat)scale
78+
resizeMode:(RCTResizeMode)resizeMode
79+
progressHandler:(RCTImageLoaderProgressBlock)progressHandler
80+
partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
81+
completionHandlerWithMetadata:(RCTImageLoaderCompletionBlockWithMetadata)completionHandler
82+
{
83+
RCTCompleteLocalAssetLoad(
84+
RCTImageFromLocalAssetURLIgnoringSystemCache(imageURL),
85+
imageURL,
86+
progressHandler,
87+
^(NSError *error, UIImage *image) {
88+
completionHandler(error, image, nil);
89+
});
6490
return nil;
6591
}
6692

‎packages/react-native/Libraries/Image/RCTImageLoader.mm‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,13 +335,27 @@ - (void)setImageCache:(id<RCTImageCache>)cache
335335
static RCTImageLoaderCancellationBlock RCTLoadImageURLFromLoader(
336336
id<RCTImageURLLoader> loadHandler,
337337
NSURL *imageURL,
338+
BOOL ignoreCache,
338339
CGSize size,
339340
CGFloat scale,
340341
RCTResizeMode resizeMode,
341342
RCTImageLoaderProgressBlock progressHandler,
342343
RCTImageLoaderPartialLoadBlock partialLoadHandler,
343344
RCTImageLoaderCompletionBlockWithMetadata completionHandler)
344345
{
346+
if (ignoreCache &&
347+
[loadHandler respondsToSelector:@selector
348+
(loadImageIgnoringCacheForURL:
349+
size:scale:resizeMode:progressHandler:partialLoadHandler
350+
:completionHandlerWithMetadata:)]) {
351+
return [loadHandler loadImageIgnoringCacheForURL:imageURL
352+
size:size
353+
scale:scale
354+
resizeMode:resizeMode
355+
progressHandler:progressHandler
356+
partialLoadHandler:partialLoadHandler
357+
completionHandlerWithMetadata:completionHandler];
358+
}
345359
if ([loadHandler
346360
respondsToSelector:@selector
347361
(loadImageForURL:size:scale:resizeMode:progressHandler:partialLoadHandler:completionHandlerWithMetadata:)]) {
@@ -619,6 +633,7 @@ - (RCTImageURLLoaderRequest *)_loadImageOrDataWithURLRequest:(NSURLRequest *)req
619633
RCTImageLoaderCancellationBlock cb = RCTLoadImageURLFromLoader(
620634
loadHandler,
621635
request.URL,
636+
request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData,
622637
size,
623638
scale,
624639
resizeMode,
@@ -663,6 +678,7 @@ - (RCTImageURLLoaderRequest *)_loadImageOrDataWithURLRequest:(NSURLRequest *)req
663678
cancelLoadLocal = RCTLoadImageURLFromLoader(
664679
loadHandler,
665680
request.URL,
681+
request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData,
666682
size,
667683
scale,
668684
resizeMode,

‎packages/react-native/Libraries/Image/RCTImageURLLoader.h‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,21 @@ typedef dispatch_block_t RCTImageLoaderCancellationBlock;
7171
completionHandlerWithMetadata:
7272
(RCTImageLoaderCompletionBlockWithMetadata)completionHandlerWithMetadata;
7373

74+
/*
75+
* Load the image without using any cache the loader keeps of its own (for
76+
* example the system cache behind +[UIImage imageNamed:]). Called instead of
77+
* the methods above when the image source asks to ignore local caches
78+
* (`cache: 'reload'`). Loaders that don't implement it are called as usual.
79+
*/
80+
- (nullable RCTImageLoaderCancellationBlock)
81+
loadImageIgnoringCacheForURL:(NSURL *)imageURL
82+
size:(CGSize)size
83+
scale:(CGFloat)scale
84+
resizeMode:(RCTResizeMode)resizeMode
85+
progressHandler:(RCTImageLoaderProgressBlock)progressHandler
86+
partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
87+
completionHandlerWithMetadata:(RCTImageLoaderCompletionBlockWithMetadata)completionHandlerWithMetadata;
88+
7489
/**
7590
* If more than one RCTImageURLLoader responds YES to `-canLoadImageURL:`
7691
* then `loaderPriority` is used to determine which one to use. The loader

‎packages/react-native/Libraries/Image/RCTLocalAssetImageLoader.mm‎

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,24 @@
1818
@interface RCTLocalAssetImageLoader () <RCTTurboModule>
1919
@end
2020

21+
static void RCTCompleteLocalAssetLoad(
22+
UIImage *image,
23+
NSURL *imageURL,
24+
RCTImageLoaderProgressBlock progressHandler,
25+
RCTImageLoaderCompletionBlock completionHandler)
26+
{
27+
if (image != nullptr) {
28+
if (progressHandler != nullptr) {
29+
progressHandler(1, 1);
30+
}
31+
completionHandler(nil, image);
32+
} else {
33+
NSString *message = [NSString stringWithFormat:@"Could not find image %@", imageURL];
34+
RCTLogWarn(@"%@", message);
35+
completionHandler(RCTErrorWithMessage(message), nil);
36+
}
37+
}
38+
2139
@implementation RCTLocalAssetImageLoader
2240

2341
RCT_EXPORT_MODULE()
@@ -49,18 +67,26 @@ - (nullable RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL
4967
partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
5068
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
5169
{
52-
UIImage *image = RCTImageFromLocalAssetURL(imageURL);
53-
if (image != nullptr) {
54-
if (progressHandler != nullptr) {
55-
progressHandler(1, 1);
56-
}
57-
completionHandler(nil, image);
58-
} else {
59-
NSString *message = [NSString stringWithFormat:@"Could not find image %@", imageURL];
60-
RCTLogWarn(@"%@", message);
61-
completionHandler(RCTErrorWithMessage(message), nil);
62-
}
70+
RCTCompleteLocalAssetLoad(RCTImageFromLocalAssetURL(imageURL), imageURL, progressHandler, completionHandler);
71+
return nil;
72+
}
6373

74+
- (nullable RCTImageLoaderCancellationBlock)
75+
loadImageIgnoringCacheForURL:(NSURL *)imageURL
76+
size:(CGSize)size
77+
scale:(CGFloat)scale
78+
resizeMode:(RCTResizeMode)resizeMode
79+
progressHandler:(RCTImageLoaderProgressBlock)progressHandler
80+
partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
81+
completionHandlerWithMetadata:(RCTImageLoaderCompletionBlockWithMetadata)completionHandler
82+
{
83+
RCTCompleteLocalAssetLoad(
84+
RCTImageFromLocalAssetURLIgnoringSystemCache(imageURL),
85+
imageURL,
86+
progressHandler,
87+
^(NSError *error, UIImage *image) {
88+
completionHandler(error, image, nil);
89+
});
6490
return nil;
6591
}
6692

‎packages/react-native/React/Base/RCTUtils.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@ RCT_EXTERN BOOL RCTIsLocalAssetURL(NSURL *__nullable imageURL);
167167
// does not correspond to a local asset.
168168
RCT_EXTERN UIImage *__nullable RCTImageFromLocalAssetURL(NSURL *imageURL);
169169

170+
// Same as RCTImageFromLocalAssetURL, but reads the image file directly instead of
171+
// using +[UIImage imageNamed:], so the image is not kept in the system image cache
172+
// after it is no longer used. Images compiled into an asset catalog can only be
173+
// loaded by name and still go through the system cache.
174+
RCT_EXTERN UIImage *__nullable RCTImageFromLocalAssetURLIgnoringSystemCache(NSURL *imageURL);
175+
170176
// Only used in case when RCTImageFromLocalAssetURL fails to get an image
171177
// This method basically checks for the image in the bundle location, instead
172178
// of the CodePush location

‎packages/react-native/React/Base/RCTUtils.mm‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,7 +1115,7 @@ static BOOL RCTUseAssetCatalog(void)
11151115
return RCTImageFromLocalAssetURL(bundleImageUrl);
11161116
}
11171117

1118-
UIImage *__nullable RCTImageFromLocalAssetURL(NSURL *imageURL)
1118+
static UIImage *__nullable RCTImageFromLocalAssetURLWithSystemCache(NSURL *imageURL, BOOL useSystemCache)
11191119
{
11201120
if (RCTUseAssetCatalog()) {
11211121
NSString *catalogName = RCTAssetCatalogNameForURL(imageURL);
@@ -1158,7 +1158,7 @@ static BOOL RCTUseAssetCatalog(void)
11581158
}
11591159

11601160
UIImage *image = nil;
1161-
if (imageName) {
1161+
if (imageName && useSystemCache) {
11621162
if (bundle) {
11631163
image = [UIImage imageNamed:imageName inBundle:bundle compatibleWithTraitCollection:nil];
11641164
} else {
@@ -1187,7 +1187,11 @@ static BOOL RCTUseAssetCatalog(void)
11871187
error:nil];
11881188
for (NSURL *frameworkURL in possibleFrameworks) {
11891189
bundle = [NSBundle bundleWithURL:frameworkURL];
1190-
image = [UIImage imageNamed:imageName inBundle:bundle compatibleWithTraitCollection:nil];
1190+
if (useSystemCache) {
1191+
image = [UIImage imageNamed:imageName inBundle:bundle compatibleWithTraitCollection:nil];
1192+
} else {
1193+
image = [UIImage imageWithContentsOfFile:[bundle.bundlePath stringByAppendingPathComponent:imageName]];
1194+
}
11911195
if (image) {
11921196
RCTLogWarn(@"Image %@ not found in mainBundle, but found in %@", imageName, bundle);
11931197
break;
@@ -1197,6 +1201,16 @@ static BOOL RCTUseAssetCatalog(void)
11971201
return image;
11981202
}
11991203

1204+
UIImage *__nullable RCTImageFromLocalAssetURL(NSURL *imageURL)
1205+
{
1206+
return RCTImageFromLocalAssetURLWithSystemCache(imageURL, YES);
1207+
}
1208+
1209+
UIImage *__nullable RCTImageFromLocalAssetURLIgnoringSystemCache(NSURL *imageURL)
1210+
{
1211+
return RCTImageFromLocalAssetURLWithSystemCache(imageURL, NO);
1212+
}
1213+
12001214
RCT_EXTERN NSString *__nullable RCTTempFilePath(NSString *extension, NSError **error)
12011215
{
12021216
static NSError *setupError = nil;

‎packages/react-native/types_DEPRECATED/Libraries/Image/ImageSource.d.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export interface ImageURISource {
4141
*
4242
* - `reload`: The data for the URL will be loaded from the originating source.
4343
* No existing cache data should be used to satisfy a URL load request.
44+
* On iOS, images bundled with the app are then also not kept in the system
45+
* image cache once they are no longer shown.
4446
*
4547
* - `force-cache`: The existing cached data will be used to satisfy the request,
4648
* regardless of its age or expiration date. If there is no existing data in the cache

0 commit comments

Comments
 (0)