react-native-fast-image 8.16.1 → 8.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -163,6 +163,12 @@ Not used with `cache: 'web'`, which follows the HTTP cache (keyed by url).
163
163
 
164
164
  ---
165
165
 
166
+ ### `source.memoryCache?: boolean`
167
+
168
+ Whether the decoded image is kept in the memory cache. **Default: true.** With `false` it's only kept on disk: a view doesn't leave it in memory once it stops showing it, and `FastImage.preload` downloads it without decoding it. Use it for large images that are shown once or rarely, like a full-screen photo: a decoded photo can take tens of MB of memory. Images shown again, like a list scrolled back, are decoded from disk again.
169
+
170
+ ---
171
+
166
172
  ### `defaultSource?: number`
167
173
 
168
174
  - An asset loaded with `require(...)`.
@@ -351,6 +357,12 @@ for (const result of results) {
351
357
 
352
358
  Each source's `cache` applies, as for a view: with `web` the preload follows the HTTP cache, and with `cacheOnly` it doesn't download. A `cacheOnly` preload resolves `ok` only if the image is cached, and loads it from the disk cache into memory, so a view shows it at once.
353
359
 
360
+ A source with `memoryCache: false` is only downloaded to the disk cache, without being decoded into memory, and is decoded when it's shown. Use it to preload many images, or large ones, e.g. the next pages of a feed: a decoded photo can take tens of MB of memory. Other sources are also kept decoded in memory, so they show at once.
361
+
362
+ ```js
363
+ await FastImage.preload(photos.map((uri) => ({ uri, memoryCache: false })))
364
+ ```
365
+
354
366
  ### `FastImage.clearMemoryCache: () => Promise<void>`
355
367
 
356
368
  Clear all images from memory cache.
@@ -2,6 +2,7 @@ package com.dylanvann.fastimage;
2
2
 
3
3
  import android.content.Context;
4
4
  import androidx.annotation.NonNull;
5
+ import androidx.annotation.Nullable;
5
6
 
6
7
  import com.bumptech.glide.Glide;
7
8
  import com.bumptech.glide.Registry;
@@ -42,6 +43,15 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
42
43
 
43
44
  private static final DispatchingProgressListener progressListener = new DispatchingProgressListener();
44
45
  private static final long WEB_CACHE_SIZE = 50 * 1024 * 1024;
46
+ // The HTTP cache of `cache: 'web'` images, once Glide has set up.
47
+ @Nullable
48
+ private static Cache webCache;
49
+
50
+ // Empties the HTTP cache of `cache: 'web'` images (clearDiskCache).
51
+ static void clearWebCache() throws IOException {
52
+ Cache cache = webCache;
53
+ if (cache != null) cache.evictAll();
54
+ }
45
55
 
46
56
  @Override
47
57
  public void registerComponents(
@@ -64,15 +74,14 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
64
74
  OkHttpUrlLoader.Factory factory = new OkHttpUrlLoader.Factory(client);
65
75
  registry.replace(GlideUrl.class, InputStream.class, factory);
66
76
 
67
- // `cache: 'web'` skips Glide's caches and relies on HTTP caching, but
68
- // React Native's shared client has no HTTP cache (unless the app gave
69
- // it one), so those urls get a client with one (#280). Other urls are
70
- // cached by Glide, so they don't use it (that would store them twice).
71
- OkHttpClient webClient = client.cache() != null
72
- ? client
73
- : client.newBuilder()
74
- .cache(new Cache(new File(context.getCacheDir(), "fast-image-http-cache"), WEB_CACHE_SIZE))
75
- .build();
77
+ // `cache: 'web'` skips Glide's caches and relies on HTTP caching, so
78
+ // those urls get a client with an HTTP cache (#280): one of their own,
79
+ // not the app's (if it gave React Native's shared client one), so
80
+ // clearDiskCache can empty it without the app's other responses.
81
+ // Other urls are cached by Glide, so they don't use it (that would
82
+ // store them twice).
83
+ webCache = new Cache(new File(context.getCacheDir(), "fast-image-http-cache"), WEB_CACHE_SIZE);
84
+ OkHttpClient webClient = client.newBuilder().cache(webCache).build();
76
85
  registry.prepend(FastImageWebGlideUrl.class, InputStream.class, new WebUrlLoaderFactory(webClient));
77
86
  }
78
87
 
@@ -124,6 +124,27 @@ public class FastImageSource extends ImageSource {
124
124
  mWebCache = webCache;
125
125
  }
126
126
 
127
+ boolean isWebCache() {
128
+ return mWebCache;
129
+ }
130
+
131
+ // `memoryCache`: whether the decoded image is kept in the memory cache.
132
+ private boolean mMemoryCache = true;
133
+
134
+ void setMemoryCache(boolean memoryCache) {
135
+ mMemoryCache = memoryCache;
136
+ }
137
+
138
+ boolean isMemoryCache() {
139
+ return mMemoryCache;
140
+ }
141
+
142
+ // An http(s) url.
143
+ boolean isRemote() {
144
+ String scheme = mUri == null ? null : mUri.getScheme();
145
+ return "http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme);
146
+ }
147
+
127
148
  // `cacheKey`: the key to cache the image under instead of its url.
128
149
  @Nullable
129
150
  private String mCacheKey = null;
@@ -64,6 +64,9 @@ class FastImageViewConverter {
64
64
  if (source == null) return null;
65
65
  FastImageSource imageSource = new FastImageSource(context, source.getString("uri"), getHeaders(source));
66
66
  imageSource.setWebCache(getCacheControl(source) == FastImageCacheControl.WEB);
67
+ if (source.hasKey("memoryCache") && source.getType("memoryCache") == ReadableType.Boolean) {
68
+ imageSource.setMemoryCache(source.getBoolean("memoryCache"));
69
+ }
67
70
  if (source.hasKey("cacheKey") && source.getType("cacheKey") == ReadableType.String) {
68
71
  imageSource.setCacheKey(source.getString("cacheKey"));
69
72
  }
@@ -116,7 +119,8 @@ class FastImageViewConverter {
116
119
  RequestOptions options = new RequestOptions()
117
120
  .diskCacheStrategy(diskCacheStrategy)
118
121
  .onlyRetrieveFromCache(onlyFromCache)
119
- .skipMemoryCache(skipMemoryCache)
122
+ // memoryCache false: only kept on disk.
123
+ .skipMemoryCache(skipMemoryCache || (imageSource != null && !imageSource.isMemoryCache()))
120
124
  .priority(priority)
121
125
  .placeholder(TRANSPARENT_DRAWABLE);
122
126
 
@@ -1,6 +1,8 @@
1
1
  package com.dylanvann.fastimage;
2
2
 
3
3
  import android.app.Activity;
4
+ import android.content.Context;
5
+ import android.graphics.BitmapFactory;
4
6
  import android.graphics.drawable.Drawable;
5
7
 
6
8
  import androidx.annotation.NonNull;
@@ -9,6 +11,8 @@ import androidx.annotation.Nullable;
9
11
  import com.bumptech.glide.Glide;
10
12
  import com.bumptech.glide.Priority;
11
13
  import com.bumptech.glide.load.DataSource;
14
+ import com.bumptech.glide.load.ImageHeaderParser;
15
+ import com.bumptech.glide.load.ImageHeaderParserUtils;
12
16
  import com.bumptech.glide.load.engine.GlideException;
13
17
  import com.bumptech.glide.request.RequestListener;
14
18
  import com.bumptech.glide.request.RequestOptions;
@@ -24,7 +28,13 @@ import com.facebook.react.bridge.UiThreadUtil;
24
28
  import com.facebook.react.bridge.WritableArray;
25
29
  import com.facebook.react.bridge.WritableMap;
26
30
 
31
+ import java.io.File;
32
+ import java.io.FileInputStream;
33
+ import java.io.IOException;
34
+ import java.io.InputStream;
27
35
  import java.util.ArrayDeque;
36
+ import java.util.concurrent.Executor;
37
+ import java.util.concurrent.Executors;
28
38
 
29
39
  class FastImageViewModule extends ReactContextBaseJavaModule {
30
40
 
@@ -62,8 +72,44 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
62
72
  }
63
73
  }
64
74
 
75
+ // Reads the size of images preloaded to disk only, off the UI thread.
76
+ private static final Executor sizeExecutor = Executors.newSingleThreadExecutor();
77
+
78
+ // The image's size from its file's header, as a decoded image has it (with
79
+ // its EXIF orientation), or null if it can't be read.
80
+ @Nullable
81
+ private static int[] imageSize(Context context, File file) {
82
+ BitmapFactory.Options bounds = new BitmapFactory.Options();
83
+ bounds.inJustDecodeBounds = true;
84
+ BitmapFactory.decodeFile(file.getPath(), bounds);
85
+ if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null;
86
+ int orientation = ImageHeaderParser.UNKNOWN_ORIENTATION;
87
+ Glide glide = Glide.get(context);
88
+ try (InputStream stream = new FileInputStream(file)) {
89
+ orientation = ImageHeaderParserUtils.getOrientation(
90
+ glide.getRegistry().getImageHeaderParsers(), stream, glide.getArrayPool());
91
+ } catch (IOException e) {
92
+ // Taken as not rotated.
93
+ }
94
+ // 5 to 8 turn the image by 90 degrees.
95
+ boolean transposed = orientation >= 5 && orientation <= 8;
96
+ return transposed
97
+ ? new int[] {bounds.outHeight, bounds.outWidth}
98
+ : new int[] {bounds.outWidth, bounds.outHeight};
99
+ }
100
+
101
+ private static WritableMap success(int width, int height) {
102
+ WritableMap result = Arguments.createMap();
103
+ result.putBoolean("ok", true);
104
+ result.putInt("width", width);
105
+ result.putInt("height", height);
106
+ return result;
107
+ }
108
+
65
109
  // Resolves with a result per source, in order, once all have loaded or
66
110
  // failed: { ok, width, height } or { ok: false, error }. Never rejects.
111
+ // A remote source with memoryCache false is only downloaded to the disk
112
+ // cache, without decoding it (its size comes from the header).
67
113
  @ReactMethod
68
114
  public void preload(final ReadableArray sources, final Promise promise) {
69
115
  final ReactApplicationContext context = getReactApplicationContext();
@@ -110,6 +156,58 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
110
156
  final RequestOptions preloadOptions = source.hasKey("priority") && !source.isNull("priority")
111
157
  ? options
112
158
  : options.priority(Priority.LOW);
159
+ // A source's result, which frees its slot.
160
+ final ResultCallback done = new ResultCallback() {
161
+ @Override
162
+ public void run(WritableMap result) {
163
+ results[index] = result;
164
+ preloadsInFlight--;
165
+ finishOne.run();
166
+ startPendingPreloads();
167
+ }
168
+ };
169
+ // Remote images (web ones are in the HTTP cache instead,
170
+ // and local ones are already on disk).
171
+ if (!imageSource.isMemoryCache() && !imageSource.isWebCache() && imageSource.isRemote()) {
172
+ pendingPreloads.add(new Runnable() {
173
+ @Override
174
+ public void run() {
175
+ Glide
176
+ .with(context)
177
+ .asFile()
178
+ .load(imageSource.getSourceForLoad())
179
+ .apply(preloadOptions)
180
+ .listener(new RequestListener<File>() {
181
+ @Override
182
+ public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
183
+ done.run(failure(FastImageRequestListener.errorMessage(e)));
184
+ return false;
185
+ }
186
+
187
+ @Override
188
+ public boolean onResourceReady(final File file, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
189
+ sizeExecutor.execute(new Runnable() {
190
+ @Override
191
+ public void run() {
192
+ final int[] size = imageSize(context, file);
193
+ UiThreadUtil.runOnUiThread(new Runnable() {
194
+ @Override
195
+ public void run() {
196
+ done.run(size != null
197
+ ? success(size[0], size[1])
198
+ : failure("Failed to read the image's size"));
199
+ }
200
+ });
201
+ }
202
+ });
203
+ return false;
204
+ }
205
+ })
206
+ .preload();
207
+ }
208
+ });
209
+ continue;
210
+ }
113
211
  pendingPreloads.add(new Runnable() {
114
212
  @Override
115
213
  public void run() {
@@ -122,10 +220,7 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
122
220
  .listener(new RequestListener<Drawable>() {
123
221
  @Override
124
222
  public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
125
- results[index] = failure(FastImageRequestListener.errorMessage(e));
126
- preloadsInFlight--;
127
- finishOne.run();
128
- startPendingPreloads();
223
+ done.run(failure(FastImageRequestListener.errorMessage(e)));
129
224
  return false;
130
225
  }
131
226
 
@@ -133,14 +228,7 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
133
228
  public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
134
229
  // Preloaded at its original size, so this is
135
230
  // the image's own size.
136
- WritableMap result = Arguments.createMap();
137
- result.putBoolean("ok", true);
138
- result.putInt("width", resource.getIntrinsicWidth());
139
- result.putInt("height", resource.getIntrinsicHeight());
140
- results[index] = result;
141
- preloadsInFlight--;
142
- finishOne.run();
143
- startPendingPreloads();
231
+ done.run(success(resource.getIntrinsicWidth(), resource.getIntrinsicHeight()));
144
232
  return false;
145
233
  }
146
234
  })
@@ -157,6 +245,10 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
157
245
  });
158
246
  }
159
247
 
248
+ private interface ResultCallback {
249
+ void run(WritableMap result);
250
+ }
251
+
160
252
  private static WritableMap failure(String error) {
161
253
  WritableMap result = Arguments.createMap();
162
254
  result.putBoolean("ok", false);
@@ -190,6 +282,13 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
190
282
  }
191
283
 
192
284
  Glide.get(activity.getApplicationContext()).clearDiskCache();
285
+ // And the HTTP cache of `cache: 'web'` images, which Glide doesn't
286
+ // cache.
287
+ try {
288
+ FastImageOkHttpProgressGlideModule.clearWebCache();
289
+ } catch (IOException e) {
290
+ // Cleared as far as it could.
291
+ }
193
292
  promise.resolve(null);
194
293
  }
195
294
  }
@@ -4,6 +4,7 @@ import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_ERROR_EV
4
4
 
5
5
  import android.annotation.SuppressLint;
6
6
  import android.content.Context;
7
+ import android.graphics.Bitmap;
7
8
  import android.graphics.drawable.BitmapDrawable;
8
9
  import android.graphics.drawable.Drawable;
9
10
 
@@ -108,8 +109,20 @@ class FastImageViewWithUrl extends AppCompatImageView {
108
109
  // Shows each GIF as this view's own animation. Glide's target also starts
109
110
  // and stops it with the Activity, as it does Glide's own GifDrawable.
110
111
  private final class OwnGifTarget extends DrawableImageViewTarget {
111
- OwnGifTarget() {
112
+ // Shown instead of the placeholder while loading (see shownCopy).
113
+ @Nullable
114
+ private Drawable mMeanwhile;
115
+
116
+ OwnGifTarget(@Nullable Drawable meanwhile) {
112
117
  super(FastImageViewWithUrl.this);
118
+ mMeanwhile = meanwhile;
119
+ }
120
+
121
+ @Override
122
+ public void onLoadStarted(@Nullable Drawable placeholder) {
123
+ Drawable meanwhile = mMeanwhile;
124
+ mMeanwhile = null;
125
+ super.onLoadStarted(meanwhile != null ? meanwhile : placeholder);
113
126
  }
114
127
 
115
128
  @Override
@@ -287,12 +300,35 @@ class FastImageViewWithUrl extends AppCompatImageView {
287
300
  private void reloadForSize() {
288
301
  RequestBuilder<Drawable> shown = mShownRequest;
289
302
  RequestBuilder<Drawable> current = fromCache(shown);
303
+ Drawable meanwhile = shownCopy(shown);
290
304
  mLoadCount++;
291
305
  clearView(requestManager);
292
- into(shown, shown.clone()
293
- .thumbnail(current)
306
+ RequestBuilder<Drawable> builder = shown.clone()
294
307
  .error(current.clone())
295
- .listener(new FastImageRequestListener(null, null, true, false)));
308
+ .listener(new FastImageRequestListener(null, null, true, false));
309
+ if (meanwhile == null) builder = builder.thumbnail(current);
310
+ into(shown, builder, meanwhile);
311
+ }
312
+
313
+ // A copy of the image showing, for the view to show while the next one
314
+ // loads, when it isn't in Glide's memory cache (source.memoryCache false):
315
+ // a thumbnail of it would come from the disk, after the view has cleared,
316
+ // and its own bitmap is reused once its request is cleared. Only the view
317
+ // keeps the copy, until the next image replaces it. Null when the image
318
+ // is in the memory cache, or isn't a bitmap (a GIF).
319
+ @Nullable
320
+ private Drawable shownCopy(RequestBuilder<Drawable> shown) {
321
+ if (shown.isMemoryCacheable()) return null;
322
+ Drawable drawable = getDrawable();
323
+ if (!(drawable instanceof BitmapDrawable)) return null;
324
+ Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
325
+ if (bitmap == null || bitmap.isRecycled()) return null;
326
+ Bitmap.Config config = bitmap.getConfig();
327
+ Bitmap copy = bitmap.copy(config != null ? config : Bitmap.Config.ARGB_8888, false);
328
+ if (copy == null) return null;
329
+ BitmapDrawable result = new BitmapDrawable(getResources(), copy);
330
+ if (mPixelated) result.setFilterBitmap(false);
331
+ return result;
296
332
  }
297
333
 
298
334
  // The shown image, from the cache only (never loaded again), at the size
@@ -304,15 +340,15 @@ class FastImageViewWithUrl extends AppCompatImageView {
304
340
  return request;
305
341
  }
306
342
 
307
- // Starts loading the request (built by builder), recording the size Glide
308
- // loads it at. The size callback is added first, so it has the size by
343
+ // Starts loading the request (built by builder), showing meanwhile (if
344
+ // not null) until it loads, and recording the size Glide loads it at. The size callback is added first, so it has the size by
309
345
  // the time the image loads, even from the memory cache.
310
- private void into(RequestBuilder<Drawable> request, RequestBuilder<Drawable> builder) {
346
+ private void into(RequestBuilder<Drawable> request, RequestBuilder<Drawable> builder, @Nullable Drawable meanwhile) {
311
347
  final int load = mLoadCount;
312
348
  mLoadingRequest = request;
313
349
  mLoadingWidth = 0;
314
350
  mLoadingHeight = 0;
315
- OwnGifTarget target = new OwnGifTarget();
351
+ OwnGifTarget target = new OwnGifTarget(meanwhile);
316
352
  target.getSize(new SizeReadyCallback() {
317
353
  @Override
318
354
  public void onSizeReady(int width, int height) {
@@ -399,6 +435,9 @@ class FastImageViewWithUrl extends AppCompatImageView {
399
435
  untrackUrl(viewsForUrlsMap);
400
436
  }
401
437
 
438
+ // Before the view clears (see shownCopy).
439
+ Drawable meanwhile = shownRequest != null && imageSource != null ? shownCopy(shownRequest) : null;
440
+
402
441
  // Cancel existing request.
403
442
  this.glideUrl = glideUrl;
404
443
  clearView(requestManager);
@@ -440,7 +479,8 @@ class FastImageViewWithUrl extends AppCompatImageView {
440
479
  .apply(renderingOptions(model));
441
480
  RequestBuilder<Drawable> request = builder.clone();
442
481
 
443
- boolean thumbnail = shownRequest != null && model != null;
482
+ if (model == null) meanwhile = null;
483
+ boolean thumbnail = shownRequest != null && model != null && meanwhile == null;
444
484
  if (thumbnail) {
445
485
  builder = builder.thumbnail(fromCache(shownRequest));
446
486
  }
@@ -448,7 +488,7 @@ class FastImageViewWithUrl extends AppCompatImageView {
448
488
  if (key != null)
449
489
  builder.listener(new FastImageRequestListener(key, imageSource, thumbnail, true));
450
490
 
451
- into(request, builder);
491
+ into(request, builder, meanwhile);
452
492
  }
453
493
  }
454
494
 
@@ -77,6 +77,7 @@ export type FastImageSource = {
77
77
  priority?: Priorities,
78
78
  cache?: CacheControls,
79
79
  cacheKey?: string,
80
+ memoryCache?: boolean,
80
81
  }
81
82
 
82
83
  export type FastImageProps = $ReadOnly<{|
package/dist/index.d.ts CHANGED
@@ -33,6 +33,14 @@ export type Source = {
33
33
  * an expiry: use something stable, like the image's id.
34
34
  */
35
35
  cacheKey?: string;
36
+ /**
37
+ * Whether the decoded image is kept in the memory cache (default true).
38
+ * With false it's only kept on disk: a view doesn't leave it in memory
39
+ * once it stops showing it, and `FastImage.preload` downloads it without
40
+ * decoding it. For large images shown once, like a full-screen photo; a
41
+ * list scrolled back decodes them again.
42
+ */
43
+ memoryCache?: boolean;
36
44
  };
37
45
  export interface OnLoadEvent {
38
46
  nativeEvent: {
@@ -77,6 +77,7 @@ export type FastImageSource = {
77
77
  priority?: Priorities,
78
78
  cache?: CacheControls,
79
79
  cacheKey?: string,
80
+ memoryCache?: boolean,
80
81
  }
81
82
 
82
83
  export type FastImageProps = $ReadOnly<{|
@@ -3,6 +3,7 @@
3
3
  #import <SDWebImage/SDWebImageDownloaderRequestModifier.h>
4
4
  #import <SDWebImage/SDWebImageCacheKeyFilter.h>
5
5
  #import <SDWebImage/SDWebImageDefine.h>
6
+ #import <SDWebImage/SDImageLoader.h>
6
7
 
7
8
  typedef NS_ENUM(NSInteger, FFFPriority) {
8
9
  FFFPriorityLow,
@@ -31,6 +32,10 @@ typedef NS_ENUM(NSInteger, FFFCacheControl) {
31
32
  @property (nonatomic) FFFCacheControl cacheControl;
32
33
  // The key to cache the image under instead of its url, or nil.
33
34
  @property (nonatomic, copy) NSString *cacheKey;
35
+ // Whether the decoded image is kept in the memory cache (`memoryCache`,
36
+ // default YES). Without it, it's only kept on disk: views don't leave it in
37
+ // memory, and preload doesn't decode it.
38
+ @property (nonatomic) BOOL memoryCache;
34
39
 
35
40
  - (instancetype)initWithURL:(NSURL *)url
36
41
  priority:(FFFPriority)priority
@@ -47,4 +52,13 @@ typedef NS_ENUM(NSInteger, FFFCacheControl) {
47
52
  // downloads. For views and preload alike.
48
53
  - (SDWebImageOptions)cacheOptions;
49
54
 
55
+ // The loader for `cache: 'web'` images, whose HTTP cache is their own (see
56
+ // webURLCache), or nil for SDWebImage's shared one.
57
+ - (id<SDImageLoader>)imageLoader;
58
+
59
+ // The HTTP cache of `cache: 'web'` images: their own rather than the app's
60
+ // shared NSURLCache, so clearDiskCache can empty it without the app's other
61
+ // responses. 50 MB, as on Android.
62
+ + (NSURLCache *)webURLCache;
63
+
50
64
  @end
@@ -1,4 +1,9 @@
1
1
  #import "FFFastImageSource.h"
2
+ #import <SDWebImage/SDWebImageDownloader.h>
3
+ #import <SDWebImage/SDWebImageDownloaderResponseModifier.h>
4
+ #import <SDWebImage/SDWebImageDownloaderDecryptor.h>
5
+
6
+ static NSUInteger const FFFWebCacheSize = 50 * 1024 * 1024;
2
7
 
3
8
  @implementation FFFastImageSource
4
9
 
@@ -13,6 +18,7 @@
13
18
  _priority = priority;
14
19
  _headers = headers;
15
20
  _cacheControl = cacheControl;
21
+ _memoryCache = YES;
16
22
  }
17
23
  return self;
18
24
  }
@@ -42,6 +48,58 @@
42
48
  return 0;
43
49
  }
44
50
 
51
+ - (id<SDImageLoader>)imageLoader
52
+ {
53
+ if (_cacheControl != FFFCacheControlWeb) {
54
+ return nil;
55
+ }
56
+ static SDWebImageDownloader *downloader;
57
+ static dispatch_once_t once;
58
+ dispatch_once(&once, ^{
59
+ // As SDWebImage's shared downloader is set up, with the HTTP cache.
60
+ SDWebImageDownloaderConfig *config = [SDWebImageDownloader.sharedDownloader.config copy];
61
+ NSURLSessionConfiguration *session = [(config.sessionConfiguration ?: NSURLSessionConfiguration.defaultSessionConfiguration) copy];
62
+ session.URLCache = [FFFastImageSource webURLCache];
63
+ config.sessionConfiguration = session;
64
+ downloader = [[SDWebImageDownloader alloc] initWithConfig:config];
65
+ // The shared downloader's response modifier and decryptor, when it
66
+ // has them (as they are at each download). Its request modifier
67
+ // wouldn't apply anyway: each load's own (for its headers) replaces
68
+ // it. Headers set with setValue:forHTTPHeaderField: can't be read
69
+ // back, so web images don't get those.
70
+ downloader.responseModifier = [SDWebImageDownloaderResponseModifier responseModifierWithBlock:^NSURLResponse * _Nullable(NSURLResponse * _Nonnull response) {
71
+ id<SDWebImageDownloaderResponseModifier> shared = SDWebImageDownloader.sharedDownloader.responseModifier;
72
+ return shared ? [shared modifiedResponseWithResponse:response] : response;
73
+ }];
74
+ downloader.decryptor = [SDWebImageDownloaderDecryptor decryptorWithBlock:^NSData * _Nullable(NSData * _Nonnull data, NSURLResponse * _Nullable response) {
75
+ id<SDWebImageDownloaderDecryptor> shared = SDWebImageDownloader.sharedDownloader.decryptor;
76
+ return shared ? [shared decryptedDataWithData:data response:response] : data;
77
+ }];
78
+ });
79
+ return downloader;
80
+ }
81
+
82
+ + (NSURLCache *)webURLCache
83
+ {
84
+ static NSURLCache *cache;
85
+ static dispatch_once_t once;
86
+ dispatch_once(&once, ^{
87
+ NSString *name = @"fast-image-http-cache";
88
+ if (@available(iOS 13.0, tvOS 13.0, *)) {
89
+ NSURL *caches = [NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask].firstObject;
90
+ cache = [[NSURLCache alloc] initWithMemoryCapacity:0
91
+ diskCapacity:FFFWebCacheSize
92
+ directoryURL:[caches URLByAppendingPathComponent:name]];
93
+ } else {
94
+ // In the app's caches directory.
95
+ cache = [[NSURLCache alloc] initWithMemoryCapacity:0
96
+ diskCapacity:FFFWebCacheSize
97
+ diskPath:name];
98
+ }
99
+ });
100
+ return cache;
101
+ }
102
+
45
103
  - (SDWebImageDownloaderRequestModifier *)requestModifier
46
104
  {
47
105
  NSDictionary* headers = _headers;
@@ -508,16 +508,26 @@ NSString *FFFErrorMessage(NSError *error)
508
508
  - (SDWebImageContext*) loadContext {
509
509
  SDWebImageMutableContext* context = [NSMutableDictionary dictionary];
510
510
  context[SDWebImageContextDownloadRequestModifier] = _source.requestModifier;
511
+ context[SDWebImageContextImageLoader] = _source.imageLoader;
511
512
  context[SDWebImageContextCacheKeyFilter] = _source.cacheKeyFilter;
512
513
  CGSize box = [self decodeBox];
513
514
  self.decodedBox = box;
514
515
  self.decodedCover = [self decodeCovers];
515
516
  if (CGSizeEqualToSize(box, CGSizeZero)) {
516
517
  context[SDWebImageContextAnimatedImageClass] = [SDAnimatedImage class];
518
+ if (!_source.memoryCache) {
519
+ // Only on disk, also when it comes from there.
520
+ context[SDWebImageContextStoreCacheType] = @(SDImageCacheTypeDisk);
521
+ }
517
522
  return context;
518
523
  }
519
524
  NSString* key = _source.cacheKeyFilter ? _source.cacheKey : _source.url.absoluteString;
520
525
  [FFFDownsampledImage addToContext: context forKey: key box: box cover: self.decodedCover];
526
+ if (!_source.memoryCache) {
527
+ // The smaller copy is only kept in memory, so it isn't kept (the
528
+ // downloaded file is still on disk).
529
+ context[SDWebImageContextStoreCacheType] = @(SDImageCacheTypeNone);
530
+ }
521
531
  return context;
522
532
  }
523
533
 
@@ -42,6 +42,7 @@ static SDWebImageMutableContext *FFFPreloadContext(FFFastImageSource *source)
42
42
  {
43
43
  SDWebImageMutableContext *context = [NSMutableDictionary dictionary];
44
44
  context[SDWebImageContextDownloadRequestModifier] = source.requestModifier;
45
+ context[SDWebImageContextImageLoader] = source.imageLoader;
45
46
  context[SDWebImageContextCacheKeyFilter] = source.cacheKeyFilter;
46
47
  return context;
47
48
  }
@@ -68,7 +69,9 @@ static void FFFStartPendingPreloads(void)
68
69
  }
69
70
 
70
71
  // Resolves with a result per source, in order, once all have loaded or failed:
71
- // { ok, width, height } or { ok: false, error }. Never rejects.
72
+ // { ok, width, height } or { ok: false, error }. Never rejects. A source with
73
+ // memoryCache false is only stored on disk, without decoding it (its size
74
+ // comes from the header).
72
75
  RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources
73
76
  resolve:(RCTPromiseResolveBlock)resolve
74
77
  reject:(__unused RCTPromiseRejectBlock)reject)
@@ -114,13 +117,20 @@ RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources
114
117
  }
115
118
  }
116
119
  options |= [source cacheOptions];
120
+ SDWebImageMutableContext *context = FFFPreloadContext(source);
121
+ if (!source.memoryCache) {
122
+ // Not decoded (only its header is read, for the size), and not
123
+ // kept in memory, also when it comes from the disk cache.
124
+ options |= SDWebImageAvoidDecodeImage;
125
+ context[SDWebImageContextStoreCacheType] = @(SDImageCacheTypeDisk);
126
+ }
117
127
  [FFFPendingPreloads addObject:[^{
118
128
  // Once per slot: SDWebImage calls this once with finished set
119
129
  // (a progressive load calls it more, without).
120
130
  __block BOOL done = NO;
121
131
  [[SDWebImageManager sharedManager] loadImageWithURL:source.url
122
132
  options:options
123
- context:[FFFPreloadContext(source) copy]
133
+ context:[context copy]
124
134
  progress:nil
125
135
  completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
126
136
  if (!finished || done) {
@@ -153,6 +163,8 @@ RCT_EXPORT_METHOD(clearMemoryCache:(RCTPromiseResolveBlock)resolve reject:(RCTPr
153
163
 
154
164
  RCT_EXPORT_METHOD(clearDiskCache:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
155
165
  {
166
+ // And the HTTP cache of `cache: 'web'` images.
167
+ [FFFastImageSource.webURLCache removeAllCachedResponses];
156
168
  [SDImageCache.sharedImageCache clearDiskOnCompletion:^(){
157
169
  resolve(NULL);
158
170
  }];
@@ -49,6 +49,8 @@ RCT_ENUM_CONVERTER(FFFCacheControl, (@{
49
49
  imageSource.hasPriority = priorityJSON && priorityJSON != (id)kCFNull;
50
50
  NSString *cacheKey = [self NSString:json[@"cacheKey"]];
51
51
  imageSource.cacheKey = cacheKey.length > 0 ? cacheKey : nil;
52
+ id memoryCache = json[@"memoryCache"];
53
+ imageSource.memoryCache = memoryCache && memoryCache != (id)kCFNull ? [self BOOL:memoryCache] : YES;
52
54
 
53
55
  return imageSource;
54
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "8.16.1",
3
+ "version": "8.17.0",
4
4
  "description": "🚩 FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",