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'; |
9 | library; |
10 | |
11 | import 'dart:developer'; |
12 | |
13 | import 'package:flutter/foundation.dart'; |
14 | import 'package:flutter/scheduler.dart'; |
15 | |
16 | import 'image_stream.dart'; |
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({super.key}); |
78 | /// |
79 | /// @override |
80 | /// Widget build(BuildContext context) { |
81 | /// return Container(); |
82 | /// } |
83 | /// } |
84 | /// ``` |
85 | /// {@end-tool} |
86 | class 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 |
541 | class 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]. |
596 | abstract 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 | |
619 | class _CachedImage extends _CachedImageBase { |
620 | _CachedImage(super.completer, {super.sizeBytes}); |
621 | } |
622 | |
623 | class _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 | |
645 | class _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 |
Definitions
- _kDefaultSize
- _kDefaultSizeBytes
- ImageCache
- maximumSize
- maximumSize
- currentSize
- maximumSizeBytes
- maximumSizeBytes
- currentSizeBytes
- clear
- evict
- _touch
- _trackLiveImage
- putIfAbsent
- listener
- statusForKey
- containsKey
- liveImageCount
- pendingImageCount
- clearLiveImages
- _checkCacheSize
- ImageCacheStatus
- _
- tracked
- untracked
- ==
- hashCode
- toString
- _CachedImageBase
- _CachedImageBase
- dispose
- _CachedImage
- _CachedImage
- _LiveImage
- _LiveImage
- dispose
- toString
- _PendingImage
- _PendingImage
Learn more about Flutter for embedded and desktop on industrialflutter.com