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