image_cache.dart 6.8 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'image_stream.dart';
6

7
const int _kDefaultSize = 1000;
8
const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
9

10 11
/// Class for the [imageCache] object.
///
12 13 14 15 16 17 18 19
/// Implements a least-recently-used cache of up to 1000 images, and up to 100
/// MB. The maximum size can be adjusted using [maximumSize] and
/// [maximumSizeBytes]. Images that are actively in use (i.e. to which the
/// application is holding references, either via [ImageStream] objects,
/// [ImageStreamCompleter] objects, [ImageInfo] objects, or raw [dart:ui.Image]
/// objects) may get evicted from the cache (and thus need to be refetched from
/// the network if they are referenced in the [putIfAbsent] method), but the raw
/// bits are kept in memory for as long as the application is using them.
20
///
21 22 23 24
/// The [putIfAbsent] method is the main entry-point to the cache API. It
/// returns the previously cached [ImageStreamCompleter] for the given key, if
/// available; if not, it calls the given callback to obtain it first. In either
/// case, the key is moved to the "most recently used" position.
25
///
26 27
/// Generally this class is not used directly. The [ImageProvider] class and its
/// subclasses automatically handle the caching of images.
28
class ImageCache {
29 30
  final Map<Object, ImageStreamCompleter> _pendingImages = <Object, ImageStreamCompleter>{};
  final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
31

32 33 34 35
  /// Maximum number of entries to store in the cache.
  ///
  /// Once this many entries have been cached, the least-recently-used entry is
  /// evicted when adding a new entry.
36 37
  int get maximumSize => _maximumSize;
  int _maximumSize = _kDefaultSize;
38 39 40 41 42
  /// Changes the maximum cache size.
  ///
  /// If the new size is smaller than the current number of elements, the
  /// extraneous elements are evicted immediately. Setting this to zero and then
  /// returning it to its original value will therefore immediately clear the
43
  /// cache.
44
  set maximumSize(int value) {
45 46 47 48 49 50 51
    assert(value != null);
    assert(value >= 0);
    if (value == maximumSize)
      return;
    _maximumSize = value;
    if (maximumSize == 0) {
      _cache.clear();
52
      _currentSizeBytes = 0;
53
    } else {
54
      _checkCacheSize();
55 56
    }
  }
57

58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
  /// The current number of cached entries.
  int get currentSize => _cache.length;

  /// Maximum size of entries to store in the cache in bytes.
  ///
  /// Once more than this amount of bytes have been cached, the
  /// least-recently-used entry is evicted until there are fewer than the
  /// maximum bytes.
  int get maximumSizeBytes => _maximumSizeBytes;
  int _maximumSizeBytes = _kDefaultSizeBytes;
  /// Changes the maximum cache bytes.
  ///
  /// If the new size is smaller than the current size in bytes, the
  /// extraneous elements are evicted immediately. Setting this to zero and then
  /// returning it to its original value will therefore immediately clear the
  /// cache.
  set maximumSizeBytes(int value) {
    assert(value != null);
    assert(value >= 0);
    if (value == _maximumSizeBytes)
      return;
    _maximumSizeBytes = value;
    if (_maximumSizeBytes == 0) {
      _cache.clear();
      _currentSizeBytes = 0;
    } else {
      _checkCacheSize();
    }
  }

  /// The current size of cached entries in bytes.
  int get currentSizeBytes => _currentSizeBytes;
  int _currentSizeBytes = 0;

92 93 94 95
  /// Evicts all entries from the cache.
  ///
  /// This is useful if, for instance, the root asset bundle has been updated
  /// and therefore new images must be obtained.
96 97 98
  ///
  /// Images which have not finished loading yet will not be removed from the
  /// cache, and when they complete they will be inserted as normal.
99 100
  void clear() {
    _cache.clear();
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
    _currentSizeBytes = 0;
  }

  /// Evicts a single entry from the cache, returning true if successful.
  ///
  /// The [key] must be equal to an object used to cache an image in
  /// [ImageCache.putIfAbsent].
  ///
  /// If the key is not immediately available, as is common, consider using
  /// [ImageProvider.evict] to call this method indirectly instead.
  ///
  /// See also:
  ///
  ///   * [ImageProvider], for providing images to the [Image] widget.
  bool evict(Object key) {
    final _CachedImage image = _cache.remove(key);
    if (image != null) {
      _currentSizeBytes -= image.sizeBytes;
      return true;
    }
    return false;
122 123
  }

124 125 126
  /// Returns the previously cached [ImageStream] for the given key, if available;
  /// if not, calls the given callback to obtain it first. In either case, the
  /// key is moved to the "most recently used" position.
127
  ///
128
  /// The arguments must not be null. The `loader` cannot return null.
129 130 131
  ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader()) {
    assert(key != null);
    assert(loader != null);
132 133 134 135 136 137 138 139 140 141
    ImageStreamCompleter result = _pendingImages[key];
    // Nothing needs to be done because the image hasn't loaded yet.
    if (result != null)
      return result;
    // Remove the provider from the list so that we can move it to the
    // recently used position below.
    final _CachedImage image = _cache.remove(key);
    if (image != null) {
      _cache[key] = image;
      return image.completer;
142
    }
143 144 145
    result = loader();
    void listener(ImageInfo info, bool syncCall) {
      // Images that fail to load don't contribute to cache size.
146
      final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
147
      final _CachedImage image = new _CachedImage(result, imageSize);
148 149 150 151 152 153
      // If the image is bigger than the maximum cache size, and the cache size
      // is not zero, then increase the cache size to the size of the image plus
      // some change.
      if (maximumSizeBytes > 0 && imageSize > maximumSizeBytes) {
        _maximumSizeBytes = imageSize + 1000;
      }
154 155 156 157 158 159 160 161 162
      _currentSizeBytes += imageSize;
      _pendingImages.remove(key);
      _cache[key] = image;
      result.removeListener(listener);
      _checkCacheSize();
    }
    if (maximumSize > 0 && maximumSizeBytes > 0) {
      _pendingImages[key] = result;
      result.addListener(listener);
163 164
    }
    return result;
165
  }
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186

  // Remove images from the cache until both the length and bytes are below
  // maximum, or the cache is empty.
  void _checkCacheSize() {
    while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
      final Object key = _cache.keys.first;
      final _CachedImage image = _cache[key];
      _currentSizeBytes -= image.sizeBytes;
      _cache.remove(key);
    }
    assert(_currentSizeBytes >= 0);
    assert(_cache.length <= maximumSize);
    assert(_currentSizeBytes <= maximumSizeBytes);
  }
}

class _CachedImage {
  _CachedImage(this.completer, this.sizeBytes);

  final ImageStreamCompleter completer;
  final int sizeBytes;
187
}