Line data Source code
1 : // Copyright 2014 The Flutter 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 :
5 : import 'dart:async';
6 : import 'dart:developer';
7 : import 'dart:ui' show hashValues;
8 :
9 : import 'package:flutter/cupertino.dart';
10 : import 'package:flutter/foundation.dart';
11 : import 'package:flutter/scheduler.dart';
12 : import 'package:power_image_ext/remove_aware_map.dart';
13 :
14 : import 'image_info_ext.dart';
15 : import 'image_provider_ext.dart';
16 :
17 :
18 : const int _kDefaultSize = 1000;
19 : const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
20 :
21 : /// Class for caching images.
22 : ///
23 : /// Implements a least-recently-used cache of up to 1000 images, and up to 100
24 : /// MB. The maximum size can be adjusted using [maximumSize] and
25 : /// [maximumSizeBytes].
26 : ///
27 : /// The cache also holds a list of 'live' references. An image is considered
28 : /// live if its [ImageStreamCompleter]'s listener count has never dropped to
29 : /// zero after adding at least one listener. The cache uses
30 : /// [ImageStreamCompleter.addOnLastListenerRemovedCallback] to determine when
31 : /// this has happened.
32 : ///
33 : /// The [putIfAbsent] method is the main entry-point to the cache API. It
34 : /// returns the previously cached [ImageStreamCompleter] for the given key, if
35 : /// available; if not, it calls the given callback to obtain it first. In either
36 : /// case, the key is moved to the 'most recently used' position.
37 : ///
38 : /// A caller can determine whether an image is already in the cache by using
39 : /// [containsKey], which will return true if the image is tracked by the cache
40 : /// in a pending or completed state. More fine grained information is available
41 : /// by using the [statusForKey] method.
42 : ///
43 : /// Generally this class is not used directly. The [ImageProvider] class and its
44 : /// subclasses automatically handle the caching of images.
45 : ///
46 : /// A shared instance of this cache is retained by [PaintingBinding] and can be
47 : /// obtained via the [imageCache] top-level property in the [painting] library.
48 : ///
49 : /// {@tool snippet}
50 : ///
51 : /// This sample shows how to supply your own caching logic and replace the
52 : /// global [imageCache] variable.
53 : ///
54 : /// ```dart
55 : /// /// This is the custom implementation of [ImageCache] where we can override
56 : /// /// the logic.
57 : /// class MyImageCache extends ImageCache {
58 : /// @override
59 : /// void clear() {
60 : /// print('Clearing cache!');
61 : /// super.clear();
62 : /// }
63 : /// }
64 : ///
65 : /// class MyWidgetsBinding extends WidgetsFlutterBinding {
66 : /// @override
67 : /// ImageCache createImageCache() => MyImageCache();
68 : /// }
69 : ///
70 : /// void main() {
71 : /// // The constructor sets global variables.
72 : /// MyWidgetsBinding();
73 : /// runApp(const MyApp());
74 : /// }
75 : ///
76 : /// class MyApp extends StatelessWidget {
77 : /// const MyApp({Key? key}) : super(key: key);
78 : ///
79 : /// @override
80 : /// Widget build(BuildContext context) {
81 : /// return Container();
82 : /// }
83 : /// }
84 : /// ```
85 : /// {@end-tool}
86 : class ImageCacheExt extends ImageCache {
87 : final RemoveAwareMap<Object, _PendingImage> _pendingImages = RemoveAwareMap<Object, _PendingImage>();
88 : final RemoveAwareMap<Object, _CachedImage> _cache = RemoveAwareMap<Object, _CachedImage>();
89 : /// ImageStreamCompleters with at least one listener. These images may or may
90 : /// not fit into the _pendingImages or _cache objects.
91 : ///
92 : /// Unlike _cache, the [_CachedImage] for this may have a null byte size.
93 : final RemoveAwareMap<Object, _LiveImage> _liveImages = RemoveAwareMap<Object, _LiveImage>();
94 :
95 1 : ImageCacheExt() {
96 3 : _pendingImages.hasRemovedCallback = _hasImageRemovedCallback;
97 3 : _cache.hasRemovedCallback = _hasImageRemovedCallback;
98 3 : _liveImages.hasRemovedCallback = _hasImageRemovedCallback;
99 : }
100 :
101 : Set<Object> _waitingToBeCheckedKeys = Set<Object>();
102 : bool _isScheduledImageStatusCheck = false;
103 :
104 1 : void _hasImageRemovedCallback(dynamic key, dynamic value) {
105 1 : if (key is ImageProviderExt) {
106 2 : _waitingToBeCheckedKeys.add(key);
107 : }
108 1 : if (_isScheduledImageStatusCheck) return;
109 1 : _isScheduledImageStatusCheck = true;
110 : //We should do check in MicroTask to avoid if image is remove and add right away
111 2 : scheduleMicrotask(() {
112 3 : _waitingToBeCheckedKeys.forEach((key) {
113 2 : if (!_pendingImages.containsKey(key) &&
114 2 : !_cache.containsKey(key) &&
115 2 : !_liveImages.containsKey(key)) {
116 1 : if (key is ImageProviderExt) {
117 1 : key.dispose();
118 : }
119 : }
120 : });
121 2 : _waitingToBeCheckedKeys.clear();
122 1 : _isScheduledImageStatusCheck = false;
123 : });
124 : }
125 :
126 : /// Maximum number of entries to store in the cache.
127 : ///
128 : /// Once this many entries have been cached, the least-recently-used entry is
129 : /// evicted when adding a new entry.
130 2 : int get maximumSize => _maximumSize;
131 : int _maximumSize = _kDefaultSize;
132 : /// Changes the maximum cache size.
133 : ///
134 : /// If the new size is smaller than the current number of elements, the
135 : /// extraneous elements are evicted immediately. Setting this to zero and then
136 : /// returning it to its original value will therefore immediately clear the
137 : /// cache.
138 0 : set maximumSize(int value) {
139 0 : assert(value != null);
140 0 : assert(value >= 0);
141 0 : if (value == maximumSize)
142 : return;
143 : TimelineTask? timelineTask;
144 : if (!kReleaseMode) {
145 0 : timelineTask = TimelineTask()..start(
146 : 'ImageCache.setMaximumSize',
147 0 : arguments: <String, dynamic>{'value': value},
148 : );
149 : }
150 0 : _maximumSize = value;
151 0 : if (maximumSize == 0) {
152 0 : clear();
153 : } else {
154 0 : _checkCacheSize(timelineTask);
155 : }
156 : if (!kReleaseMode) {
157 0 : timelineTask!.finish();
158 : }
159 : }
160 :
161 : /// The current number of cached entries.
162 0 : int get currentSize => _cache.length;
163 :
164 : /// Maximum size of entries to store in the cache in bytes.
165 : ///
166 : /// Once more than this amount of bytes have been cached, the
167 : /// least-recently-used entry is evicted until there are fewer than the
168 : /// maximum bytes.
169 2 : int get maximumSizeBytes => _maximumSizeBytes;
170 : int _maximumSizeBytes = _kDefaultSizeBytes;
171 : /// Changes the maximum cache bytes.
172 : ///
173 : /// If the new size is smaller than the current size in bytes, the
174 : /// extraneous elements are evicted immediately. Setting this to zero and then
175 : /// returning it to its original value will therefore immediately clear the
176 : /// cache.
177 0 : set maximumSizeBytes(int value) {
178 0 : assert(value != null);
179 0 : assert(value >= 0);
180 0 : if (value == _maximumSizeBytes)
181 : return;
182 : TimelineTask? timelineTask;
183 : if (!kReleaseMode) {
184 0 : timelineTask = TimelineTask()..start(
185 : 'ImageCache.setMaximumSizeBytes',
186 0 : arguments: <String, dynamic>{'value': value},
187 : );
188 : }
189 0 : _maximumSizeBytes = value;
190 0 : if (_maximumSizeBytes == 0) {
191 0 : clear();
192 : } else {
193 0 : _checkCacheSize(timelineTask);
194 : }
195 : if (!kReleaseMode) {
196 0 : timelineTask!.finish();
197 : }
198 : }
199 :
200 : /// The current size of cached entries in bytes.
201 0 : int get currentSizeBytes => _currentSizeBytes;
202 : int _currentSizeBytes = 0;
203 :
204 : /// Evicts all pending and keepAlive entries from the cache.
205 : ///
206 : /// This is useful if, for instance, the root asset bundle has been updated
207 : /// and therefore new images must be obtained.
208 : ///
209 : /// Images which have not finished loading yet will not be removed from the
210 : /// cache, and when they complete they will be inserted as normal.
211 : ///
212 : /// This method does not clear live references to images, since clearing those
213 : /// would not reduce memory pressure. Such images still have listeners in the
214 : /// application code, and will still remain resident in memory.
215 : ///
216 : /// To clear live references, use [clearLiveImages].
217 0 : void clear() {
218 : if (!kReleaseMode) {
219 0 : Timeline.instantSync(
220 : 'ImageCache.clear',
221 0 : arguments: <String, dynamic>{
222 0 : 'pendingImages': _pendingImages.length,
223 0 : 'keepAliveImages': _cache.length,
224 0 : 'liveImages': _liveImages.length,
225 0 : 'currentSizeInBytes': _currentSizeBytes,
226 : },
227 : );
228 : }
229 0 : for (final _CachedImage image in _cache.values) {
230 0 : image.dispose();
231 : }
232 0 : _cache.clear();
233 0 : _pendingImages.clear();
234 0 : _currentSizeBytes = 0;
235 : }
236 :
237 : /// Evicts a single entry from the cache, returning true if successful.
238 : ///
239 : /// Pending images waiting for completion are removed as well, returning true
240 : /// if successful. When a pending image is removed the listener on it is
241 : /// removed as well to prevent it from adding itself to the cache if it
242 : /// eventually completes.
243 : ///
244 : /// If this method removes a pending image, it will also remove
245 : /// the corresponding live tracking of the image, since it is no longer clear
246 : /// if the image will ever complete or have any listeners, and failing to
247 : /// remove the live reference could leave the cache in a state where all
248 : /// subsequent calls to [putIfAbsent] will return an [ImageStreamCompleter]
249 : /// that will never complete.
250 : ///
251 : /// If this method removes a completed image, it will _not_ remove the live
252 : /// reference to the image, which will only be cleared when the listener
253 : /// count on the completer drops to zero. To clear live image references,
254 : /// whether completed or not, use [clearLiveImages].
255 : ///
256 : /// The `key` must be equal to an object used to cache an image in
257 : /// [ImageCache.putIfAbsent].
258 : ///
259 : /// If the key is not immediately available, as is common, consider using
260 : /// [ImageProvider.evict] to call this method indirectly instead.
261 : ///
262 : /// The `includeLive` argument determines whether images that still have
263 : /// listeners in the tree should be evicted as well. This parameter should be
264 : /// set to true in cases where the image may be corrupted and needs to be
265 : /// completely discarded by the cache. It should be set to false when calls
266 : /// to evict are trying to relieve memory pressure, since an image with a
267 : /// listener will not actually be evicted from memory, and subsequent attempts
268 : /// to load it will end up allocating more memory for the image again. The
269 : /// argument must not be null.
270 : ///
271 : /// See also:
272 : ///
273 : /// * [ImageProvider], for providing images to the [Image] widget.
274 1 : bool evict(Object key, { bool includeLive = true }) {
275 0 : assert(includeLive != null);
276 : if (includeLive) {
277 : // Remove from live images - the cache will not be able to mark
278 : // it as complete, and it might be getting evicted because it
279 : // will never complete, e.g. it was loaded in a FakeAsync zone.
280 : // In such a case, we need to make sure subsequent calls to
281 : // putIfAbsent don't return this image that may never complete.
282 2 : final _LiveImage? image = _liveImages.remove(key);
283 1 : image?.dispose();
284 : }
285 2 : final _PendingImage? pendingImage = _pendingImages.remove(key);
286 : if (pendingImage != null) {
287 : if (!kReleaseMode) {
288 2 : Timeline.instantSync('ImageCache.evict', arguments: <String, dynamic>{
289 : 'type': 'pending',
290 : });
291 : }
292 1 : pendingImage.removeListener();
293 : return true;
294 : }
295 0 : final _CachedImage? image = _cache.remove(key);
296 : if (image != null) {
297 : if (!kReleaseMode) {
298 0 : Timeline.instantSync('ImageCache.evict', arguments: <String, dynamic>{
299 : 'type': 'keepAlive',
300 0 : 'sizeInBytes': image.sizeBytes,
301 : });
302 : }
303 0 : _currentSizeBytes -= image.sizeBytes!;
304 0 : image.dispose();
305 : return true;
306 : }
307 : if (!kReleaseMode) {
308 0 : Timeline.instantSync('ImageCache.evict', arguments: <String, dynamic>{
309 : 'type': 'miss',
310 : });
311 : }
312 : return false;
313 : }
314 :
315 : /// Updates the least recently used image cache with this image, if it is
316 : /// less than the [maximumSizeBytes] of this cache.
317 : ///
318 : /// Resizes the cache as appropriate to maintain the constraints of
319 : /// [maximumSize] and [maximumSizeBytes].
320 0 : void _touch(Object key, _CachedImage image, TimelineTask? timelineTask) {
321 0 : assert(timelineTask != null);
322 0 : if (image.sizeBytes != null && image.sizeBytes! <= maximumSizeBytes && maximumSize > 0) {
323 0 : _currentSizeBytes += image.sizeBytes!;
324 0 : _cache[key] = image;
325 0 : _checkCacheSize(timelineTask);
326 : } else {
327 0 : image.dispose();
328 : }
329 : }
330 :
331 1 : void _trackLiveImage(Object key, ImageStreamCompleter completer, int? sizeBytes) {
332 : // Avoid adding unnecessary callbacks to the completer.
333 3 : _liveImages.putIfAbsent(key, () {
334 : // Even if no callers to ImageProvider.resolve have listened to the stream,
335 : // the cache is listening to the stream and will remove itself once the
336 : // image completes to move it from pending to keepAlive.
337 : // Even if the cache size is 0, we still add this tracker, which will add
338 : // a keep alive handle to the stream.
339 1 : return _LiveImage(
340 : completer,
341 0 : () {
342 0 : _liveImages.remove(key);
343 : },
344 : );
345 1 : }).sizeBytes ??= sizeBytes;
346 : }
347 :
348 : /// Returns the previously cached [ImageStream] for the given key, if available;
349 : /// if not, calls the given callback to obtain it first. In either case, the
350 : /// key is moved to the 'most recently used' position.
351 : ///
352 : /// The arguments must not be null. The `loader` cannot return null.
353 : ///
354 : /// In the event that the loader throws an exception, it will be caught only if
355 : /// `onError` is also provided. When an exception is caught resolving an image,
356 : /// no completers are cached and `null` is returned instead of a new
357 : /// completer.
358 1 : ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter Function() loader, { ImageErrorListener? onError }) {
359 0 : assert(key != null);
360 0 : assert(loader != null);
361 : TimelineTask? timelineTask;
362 : TimelineTask? listenerTask;
363 : if (!kReleaseMode) {
364 2 : timelineTask = TimelineTask()..start(
365 : 'ImageCache.putIfAbsent',
366 1 : arguments: <String, dynamic>{
367 1 : 'key': key.toString(),
368 : },
369 : );
370 : }
371 2 : ImageStreamCompleter? result = _pendingImages[key]?.completer;
372 : // Nothing needs to be done because the image hasn't loaded yet.
373 : if (result != null) {
374 : if (!kReleaseMode) {
375 0 : timelineTask!.finish(arguments: <String, dynamic>{'result': 'pending'});
376 : }
377 : return result;
378 : }
379 : // Remove the provider from the list so that we can move it to the
380 : // recently used position below.
381 : // Don't use _touch here, which would trigger a check on cache size that is
382 : // not needed since this is just moving an existing cache entry to the head.
383 2 : final _CachedImage? image = _cache.remove(key);
384 : if (image != null) {
385 : if (!kReleaseMode) {
386 0 : timelineTask!.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
387 : }
388 : // The image might have been keptAlive but had no listeners (so not live).
389 : // Make sure the cache starts tracking it as live again.
390 0 : _trackLiveImage(
391 : key,
392 0 : image.completer,
393 0 : image.sizeBytes,
394 : );
395 0 : _cache[key] = image;
396 0 : return image.completer;
397 : }
398 :
399 2 : final _LiveImage? liveImage = _liveImages[key];
400 : if (liveImage != null) {
401 0 : _touch(
402 : key,
403 0 : _CachedImage(
404 0 : liveImage.completer,
405 0 : sizeBytes: liveImage.sizeBytes,
406 : ),
407 : timelineTask,
408 : );
409 : if (!kReleaseMode) {
410 0 : timelineTask!.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
411 : }
412 0 : return liveImage.completer;
413 : }
414 :
415 : try {
416 : result = loader();
417 1 : _trackLiveImage(key, result, null);
418 : } catch (error, stackTrace) {
419 : if (!kReleaseMode) {
420 0 : timelineTask!.finish(arguments: <String, dynamic>{
421 : 'result': 'error',
422 0 : 'error': error.toString(),
423 0 : 'stackTrace': stackTrace.toString(),
424 : });
425 : }
426 : if (onError != null) {
427 : onError(error, stackTrace);
428 : return null;
429 : } else {
430 : rethrow;
431 : }
432 : }
433 :
434 : if (!kReleaseMode) {
435 2 : listenerTask = TimelineTask(parent: timelineTask)..start('listener');
436 : }
437 : // If we're doing tracing, we need to make sure that we don't try to finish
438 : // the trace entry multiple times if we get re-entrant calls from a multi-
439 : // frame provider here.
440 : bool listenedOnce = false;
441 :
442 : // We shouldn't use the _pendingImages map if the cache is disabled, but we
443 : // will have to listen to the image at least once so we don't leak it in
444 : // the live image tracking.
445 : // If the cache is disabled, this variable will be set.
446 : _PendingImage? untrackedPendingImage;
447 0 : void listener(ImageInfo? info, bool syncCall) {
448 : int? sizeBytes;
449 : if (info != null) {
450 0 : if (info is PowerImageInfo) {
451 0 : sizeBytes = info.width! * info.height! * 4;
452 : } else {
453 0 : sizeBytes = info.image.height * info.image.width * 4;
454 : }
455 0 : info.dispose();
456 : }
457 0 : final _CachedImage image = _CachedImage(
458 : result!,
459 : sizeBytes: sizeBytes,
460 : );
461 :
462 0 : _trackLiveImage(key, result, sizeBytes);
463 :
464 : // Only touch if the cache was enabled when resolve was initially called.
465 : if (untrackedPendingImage == null) {
466 0 : _touch(key, image, listenerTask);
467 : } else {
468 0 : image.dispose();
469 : }
470 :
471 0 : final _PendingImage? pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
472 : if (pendingImage != null) {
473 0 : pendingImage.removeListener();
474 : }
475 : if (!kReleaseMode && !listenedOnce) {
476 0 : listenerTask!.finish(arguments: <String, dynamic>{
477 : 'syncCall': syncCall,
478 : 'sizeInBytes': sizeBytes,
479 : });
480 0 : timelineTask!.finish(arguments: <String, dynamic>{
481 0 : 'currentSizeBytes': currentSizeBytes,
482 0 : 'currentSize': currentSize,
483 : });
484 : }
485 : listenedOnce = true;
486 : }
487 :
488 1 : final ImageStreamListener streamListener = ImageStreamListener(listener);
489 4 : if (maximumSize > 0 && maximumSizeBytes > 0) {
490 3 : _pendingImages[key] = _PendingImage(result, streamListener);
491 : } else {
492 0 : untrackedPendingImage = _PendingImage(result, streamListener);
493 : }
494 : // Listener is removed in [_PendingImage.removeListener].
495 1 : result.addListener(streamListener);
496 :
497 : return result;
498 : }
499 :
500 : /// The [ImageCacheStatus] information for the given `key`.
501 : // ImageCacheStatus statusForKey(Object key) {
502 : // return ImageCacheStatus._(
503 : // pending: _pendingImages.containsKey(key),
504 : // keepAlive: _cache.containsKey(key),
505 : // live: _liveImages.containsKey(key),
506 : // );
507 : // }
508 :
509 : /// Returns whether this `key` has been previously added by [putIfAbsent].
510 0 : bool containsKey(Object key) {
511 0 : return _pendingImages[key] != null || _cache[key] != null;
512 : }
513 :
514 : /// The number of live images being held by the [ImageCache].
515 : ///
516 : /// Compare with [ImageCache.currentSize] for keepAlive images.
517 0 : int get liveImageCount => _liveImages.length;
518 :
519 : /// The number of images being tracked as pending in the [ImageCache].
520 : ///
521 : /// Compare with [ImageCache.currentSize] for keepAlive images.
522 0 : int get pendingImageCount => _pendingImages.length;
523 :
524 : /// Clears any live references to images in this cache.
525 : ///
526 : /// An image is considered live if its [ImageStreamCompleter] has never hit
527 : /// zero listeners after adding at least one listener. The
528 : /// [ImageStreamCompleter.addOnLastListenerRemovedCallback] is used to
529 : /// determine when this has happened.
530 : ///
531 : /// This is called after a hot reload to evict any stale references to image
532 : /// data for assets that have changed. Calling this method does not relieve
533 : /// memory pressure, since the live image caching only tracks image instances
534 : /// that are also being held by at least one other object.
535 0 : void clearLiveImages() {
536 0 : for (final _LiveImage image in _liveImages.values) {
537 0 : image.dispose();
538 : }
539 0 : _liveImages.clear();
540 : }
541 :
542 : // Remove images from the cache until both the length and bytes are below
543 : // maximum, or the cache is empty.
544 0 : void _checkCacheSize(TimelineTask? timelineTask) {
545 0 : final Map<String, dynamic> finishArgs = <String, dynamic>{};
546 : TimelineTask? checkCacheTask;
547 : if (!kReleaseMode) {
548 0 : checkCacheTask = TimelineTask(parent: timelineTask)..start('checkCacheSize');
549 0 : finishArgs['evictedKeys'] = <String>[];
550 0 : finishArgs['currentSize'] = currentSize;
551 0 : finishArgs['currentSizeBytes'] = currentSizeBytes;
552 : }
553 0 : while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
554 0 : final Object key = _cache.keys.first;
555 0 : final _CachedImage image = _cache[key]!;
556 0 : _currentSizeBytes -= image.sizeBytes!;
557 0 : image.dispose();
558 0 : _cache.remove(key);
559 : if (!kReleaseMode) {
560 0 : (finishArgs['evictedKeys'] as List<String>).add(key.toString());
561 : }
562 : }
563 : if (!kReleaseMode) {
564 0 : finishArgs['endSize'] = currentSize;
565 0 : finishArgs['endSizeBytes'] = currentSizeBytes;
566 0 : checkCacheTask!.finish(arguments: finishArgs);
567 : }
568 0 : assert(_currentSizeBytes >= 0);
569 0 : assert(_cache.length <= maximumSize);
570 0 : assert(_currentSizeBytes <= maximumSizeBytes);
571 : }
572 : }
573 :
574 : /// Information about how the [ImageCache] is tracking an image.
575 : ///
576 : /// A [pending] image is one that has not completed yet. It may also be tracked
577 : /// as [live] because something is listening to it.
578 : ///
579 : /// A [keepAlive] image is being held in the cache, which uses Least Recently
580 : /// Used semantics to determine when to evict an image. These images are subject
581 : /// to eviction based on [ImageCache.maximumSizeBytes] and
582 : /// [ImageCache.maximumSize]. It may be [live], but not [pending].
583 : ///
584 : /// A [live] image is being held until its [ImageStreamCompleter] has no more
585 : /// listeners. It may also be [pending] or [keepAlive].
586 : ///
587 : /// An [untracked] image is not being cached.
588 : ///
589 : /// To obtain an [ImageCacheStatus], use [ImageCache.statusForKey] or
590 : /// [ImageProvider.obtainCacheStatus].
591 : @immutable
592 : class ImageCacheStatus {
593 0 : const ImageCacheStatus._({
594 : this.pending = false,
595 : this.keepAlive = false,
596 : this.live = false,
597 0 : }) : assert(!pending || !keepAlive);
598 :
599 : /// An image that has been submitted to [ImageCache.putIfAbsent], but
600 : /// not yet completed.
601 : final bool pending;
602 :
603 : /// An image that has been submitted to [ImageCache.putIfAbsent], has
604 : /// completed, fits based on the sizing rules of the cache, and has not been
605 : /// evicted.
606 : ///
607 : /// Such images will be kept alive even if [live] is false, as long
608 : /// as they have not been evicted from the cache based on its sizing rules.
609 : final bool keepAlive;
610 :
611 : /// An image that has been submitted to [ImageCache.putIfAbsent] and has at
612 : /// least one listener on its [ImageStreamCompleter].
613 : ///
614 : /// Such images may also be [keepAlive] if they fit in the cache based on its
615 : /// sizing rules. They may also be [pending] if they have not yet resolved.
616 : final bool live;
617 :
618 : /// An image that is tracked in some way by the [ImageCache], whether
619 : /// [pending], [keepAlive], or [live].
620 0 : bool get tracked => pending || keepAlive || live;
621 :
622 : /// An image that either has not been submitted to
623 : /// [ImageCache.putIfAbsent] or has otherwise been evicted from the
624 : /// [keepAlive] and [live] caches.
625 0 : bool get untracked => !pending && !keepAlive && !live;
626 :
627 0 : @override
628 : bool operator ==(Object other) {
629 0 : if (other.runtimeType != runtimeType) {
630 : return false;
631 : }
632 0 : return other is ImageCacheStatus
633 0 : && other.pending == pending
634 0 : && other.keepAlive == keepAlive
635 0 : && other.live == live;
636 : }
637 :
638 0 : @override
639 0 : int get hashCode => hashValues(pending, keepAlive, live);
640 :
641 0 : @override
642 0 : String toString() => '${objectRuntimeType(this, 'ImageCacheStatus')}(pending: $pending, live: $live, keepAlive: $keepAlive)';
643 : }
644 :
645 : /// Base class for [_CachedImage] and [_LiveImage].
646 : ///
647 : /// Exists primarily so that a [_LiveImage] cannot be added to the
648 : /// [ImageCache._cache].
649 : abstract class _CachedImageBase {
650 1 : _CachedImageBase(
651 : this.completer, {
652 : this.sizeBytes,
653 0 : }) : assert(completer != null),
654 1 : handle = completer.keepAlive();
655 :
656 : final ImageStreamCompleter completer;
657 : int? sizeBytes;
658 : ImageStreamCompleterHandle? handle;
659 :
660 1 : @mustCallSuper
661 : void dispose() {
662 1 : assert(handle != null);
663 : // Give any interested parties a chance to listen to the stream before we
664 : // potentially dispose it.
665 2 : SchedulerBinding.instance!.addPostFrameCallback((Duration timeStamp) {
666 0 : assert(handle != null);
667 0 : handle?.dispose();
668 0 : handle = null;
669 : });
670 : }
671 : }
672 :
673 : class _CachedImage extends _CachedImageBase {
674 0 : _CachedImage(ImageStreamCompleter completer, {int? sizeBytes})
675 0 : : super(completer, sizeBytes: sizeBytes);
676 : }
677 :
678 : class _LiveImage extends _CachedImageBase {
679 1 : _LiveImage(ImageStreamCompleter completer, VoidCallback handleRemove, {int? sizeBytes})
680 1 : : super(completer, sizeBytes: sizeBytes) {
681 1 : _handleRemove = () {
682 : handleRemove();
683 0 : dispose();
684 : };
685 2 : completer.addOnLastListenerRemovedCallback(_handleRemove);
686 : }
687 :
688 : late VoidCallback _handleRemove;
689 :
690 1 : @override
691 : void dispose() {
692 3 : completer.removeOnLastListenerRemovedCallback(_handleRemove);
693 1 : super.dispose();
694 : }
695 :
696 0 : @override
697 0 : String toString() => describeIdentity(this);
698 : }
699 :
700 : class _PendingImage {
701 1 : _PendingImage(this.completer, this.listener);
702 :
703 : final ImageStreamCompleter completer;
704 : final ImageStreamListener listener;
705 :
706 1 : void removeListener() {
707 3 : completer.removeListener(listener);
708 : }
709 : }
|