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