• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'image_stream.dart';
6
7const int _kDefaultSize = 1000;
8const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
9
10/// Class for caching images.
11///
12/// Implements a least-recently-used cache of up to 1000 images, and up to 100
13/// MB. The maximum size can be adjusted using [maximumSize] and
14/// [maximumSizeBytes]. Images that are actively in use (i.e. to which the
15/// application is holding references, either via [ImageStream] objects,
16/// [ImageStreamCompleter] objects, [ImageInfo] objects, or raw [dart:ui.Image]
17/// objects) may get evicted from the cache (and thus need to be refetched from
18/// the network if they are referenced in the [putIfAbsent] method), but the raw
19/// bits are kept in memory for as long as the application is using them.
20///
21/// The [putIfAbsent] method is the main entry-point to the cache API. It
22/// returns the previously cached [ImageStreamCompleter] for the given key, if
23/// available; if not, it calls the given callback to obtain it first. In either
24/// case, the key is moved to the "most recently used" position.
25///
26/// Generally this class is not used directly. The [ImageProvider] class and its
27/// subclasses automatically handle the caching of images.
28///
29/// A shared instance of this cache is retained by [PaintingBinding] and can be
30/// obtained via the [imageCache] top-level property in the [painting] library.
31class ImageCache {
32  final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
33  final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
34
35  /// Maximum number of entries to store in the cache.
36  ///
37  /// Once this many entries have been cached, the least-recently-used entry is
38  /// evicted when adding a new entry.
39  int get maximumSize => _maximumSize;
40  int _maximumSize = _kDefaultSize;
41  /// Changes the maximum cache size.
42  ///
43  /// If the new size is smaller than the current number of elements, the
44  /// extraneous elements are evicted immediately. Setting this to zero and then
45  /// returning it to its original value will therefore immediately clear the
46  /// cache.
47  set maximumSize(int value) {
48    assert(value != null);
49    assert(value >= 0);
50    if (value == maximumSize)
51      return;
52    _maximumSize = value;
53    if (maximumSize == 0) {
54      clear();
55    } else {
56      _checkCacheSize();
57    }
58  }
59
60  /// The current number of cached entries.
61  int get currentSize => _cache.length;
62
63  /// Maximum size of entries to store in the cache in bytes.
64  ///
65  /// Once more than this amount of bytes have been cached, the
66  /// least-recently-used entry is evicted until there are fewer than the
67  /// maximum bytes.
68  int get maximumSizeBytes => _maximumSizeBytes;
69  int _maximumSizeBytes = _kDefaultSizeBytes;
70  /// Changes the maximum cache bytes.
71  ///
72  /// If the new size is smaller than the current size in bytes, the
73  /// extraneous elements are evicted immediately. Setting this to zero and then
74  /// returning it to its original value will therefore immediately clear the
75  /// cache.
76  set maximumSizeBytes(int value) {
77    assert(value != null);
78    assert(value >= 0);
79    if (value == _maximumSizeBytes)
80      return;
81    _maximumSizeBytes = value;
82    if (_maximumSizeBytes == 0) {
83      clear();
84    } else {
85      _checkCacheSize();
86    }
87  }
88
89  /// The current size of cached entries in bytes.
90  int get currentSizeBytes => _currentSizeBytes;
91  int _currentSizeBytes = 0;
92
93  /// Evicts all entries from the cache.
94  ///
95  /// This is useful if, for instance, the root asset bundle has been updated
96  /// and therefore new images must be obtained.
97  ///
98  /// Images which have not finished loading yet will not be removed from the
99  /// cache, and when they complete they will be inserted as normal.
100  void clear() {
101    _cache.clear();
102    _pendingImages.clear();
103    _currentSizeBytes = 0;
104  }
105
106  /// Evicts a single entry from the cache, returning true if successful.
107  /// Pending images waiting for completion are removed as well, returning true if successful.
108  ///
109  /// When a pending image is removed the listener on it is removed as well to prevent
110  /// it from adding itself to the cache if it eventually completes.
111  ///
112  /// The [key] must be equal to an object used to cache an image in
113  /// [ImageCache.putIfAbsent].
114  ///
115  /// If the key is not immediately available, as is common, consider using
116  /// [ImageProvider.evict] to call this method indirectly instead.
117  ///
118  /// See also:
119  ///
120  ///  * [ImageProvider], for providing images to the [Image] widget.
121  bool evict(Object key) {
122    final _PendingImage pendingImage = _pendingImages.remove(key);
123    if (pendingImage != null) {
124      pendingImage.removeListener();
125      return true;
126    }
127    final _CachedImage image = _cache.remove(key);
128    if (image != null) {
129      _currentSizeBytes -= image.sizeBytes;
130      return true;
131    }
132    return false;
133  }
134
135  /// Returns the previously cached [ImageStream] for the given key, if available;
136  /// if not, calls the given callback to obtain it first. In either case, the
137  /// key is moved to the "most recently used" position.
138  ///
139  /// The arguments must not be null. The `loader` cannot return null.
140  ///
141  /// In the event that the loader throws an exception, it will be caught only if
142  /// `onError` is also provided. When an exception is caught resolving an image,
143  /// no completers are cached and `null` is returned instead of a new
144  /// completer.
145  ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
146    assert(key != null);
147    assert(loader != null);
148    ImageStreamCompleter result = _pendingImages[key]?.completer;
149    // Nothing needs to be done because the image hasn't loaded yet.
150    if (result != null)
151      return result;
152    // Remove the provider from the list so that we can move it to the
153    // recently used position below.
154    final _CachedImage image = _cache.remove(key);
155    if (image != null) {
156      _cache[key] = image;
157      return image.completer;
158    }
159    try {
160      result = loader();
161    } catch (error, stackTrace) {
162      if (onError != null) {
163        onError(error, stackTrace);
164        return null;
165      } else {
166        rethrow;
167      }
168    }
169    void listener(ImageInfo info, bool syncCall) {
170      // Images that fail to load don't contribute to cache size.
171      final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
172      final _CachedImage image = _CachedImage(result, imageSize);
173      // If the image is bigger than the maximum cache size, and the cache size
174      // is not zero, then increase the cache size to the size of the image plus
175      // some change.
176      if (maximumSizeBytes > 0 && imageSize > maximumSizeBytes) {
177        _maximumSizeBytes = imageSize + 1000;
178      }
179      _currentSizeBytes += imageSize;
180      final _PendingImage pendingImage = _pendingImages.remove(key);
181      if (pendingImage != null) {
182        pendingImage.removeListener();
183      }
184
185      _cache[key] = image;
186      _checkCacheSize();
187    }
188    if (maximumSize > 0 && maximumSizeBytes > 0) {
189      final ImageStreamListener streamListener = ImageStreamListener(listener);
190      _pendingImages[key] = _PendingImage(result, streamListener);
191      // Listener is removed in [_PendingImage.removeListener].
192      result.addListener(streamListener);
193    }
194    return result;
195  }
196
197  // Remove images from the cache until both the length and bytes are below
198  // maximum, or the cache is empty.
199  void _checkCacheSize() {
200    while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
201      final Object key = _cache.keys.first;
202      final _CachedImage image = _cache[key];
203      _currentSizeBytes -= image.sizeBytes;
204      _cache.remove(key);
205    }
206    assert(_currentSizeBytes >= 0);
207    assert(_cache.length <= maximumSize);
208    assert(_currentSizeBytes <= maximumSizeBytes);
209  }
210}
211
212class _CachedImage {
213  _CachedImage(this.completer, this.sizeBytes);
214
215  final ImageStreamCompleter completer;
216  final int sizeBytes;
217}
218
219class _PendingImage {
220  _PendingImage(this.completer, this.listener);
221
222  final ImageStreamCompleter completer;
223  final ImageStreamListener listener;
224
225  void removeListener() {
226    completer.removeListener(listener);
227  }
228}
229