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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com