Flutter實戰(zhàn) 圖片加載原理與緩存

2021-03-09 14:42 更新

在本書前面章節(jié)已經(jīng)介紹過Image 組件,并提到 Flutter 框架對加載過的圖片是有緩存的(內(nèi)存),默認最大緩存數(shù)量是1000,最大緩存空間為100M。本節(jié)便詳細介紹 Image 的原理及圖片緩存機制,下面我們先看看ImageProvider 類。

#14.5.1 ImageProvider

我們已經(jīng)知道Image 組件的image 參數(shù)是一個必選參數(shù),它是ImageProvider類型。下面我們便詳細介紹一下ImageProvider,ImageProvider是一個抽象類,定義了圖片數(shù)據(jù)獲取和加載的相關(guān)接口。它的主要職責有兩個:

  1. 提供圖片數(shù)據(jù)源
  2. 緩存圖片

我們看看ImageProvider抽象類的詳細定義:

  1. abstract class ImageProvider<T> {
  2. ImageStream resolve(ImageConfiguration configuration) {
  3. // 實現(xiàn)代碼省略
  4. }
  5. Future<bool> evict({ ImageCache cache,
  6. ImageConfiguration configuration = ImageConfiguration.empty }) async {
  7. // 實現(xiàn)代碼省略
  8. }
  9. Future<T> obtainKey(ImageConfiguration configuration);
  10. @protected
  11. ImageStreamCompleter load(T key); // 需子類實現(xiàn)
  12. }

#load(T key)方法

加載圖片數(shù)據(jù)源的接口,不同的數(shù)據(jù)源的加載方法不同,每個ImageProvider的子類必須實現(xiàn)它。比如NetworkImage類和AssetImage類,它們都是ImageProvider的子類,但它們需要從不同的數(shù)據(jù)源來加載圖片數(shù)據(jù):NetworkImage是從網(wǎng)絡(luò)來加載圖片數(shù)據(jù),而AssetImage則是從最終的應用包里來加載(加載打到應用安裝包里的資源圖片)。 我們以NetworkImage為例,看看其 load 方法的實現(xiàn):

  1. @override
  2. ImageStreamCompleter load(image_provider.NetworkImage key) {
  3. final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
  4. return MultiFrameImageStreamCompleter(
  5. codec: _loadAsync(key, chunkEvents), //調(diào)用
  6. chunkEvents: chunkEvents.stream,
  7. scale: key.scale,
  8. ... //省略無關(guān)代碼
  9. );
  10. }

我們看到,load方法的返回值類型是ImageStreamCompleter ,它是一個抽象類,定義了管理圖片加載過程的一些接口,Image Widget 中正是通過它來監(jiān)聽圖片加載狀態(tài)的(我們將在下面介紹Image 原理時詳細介紹)。

MultiFrameImageStreamCompleterImageStreamCompleter的一個子類,是 flutter sdk 預置的類,通過該類,我們以方便、輕松地創(chuàng)建出一個ImageStreamCompleter實例來做為load方法的返回值。

我們可以看到,MultiFrameImageStreamCompleter 需要一個codec參數(shù),該參數(shù)類型為Future<ui.Codec>。Codec 是處理圖片編解碼的類的一個 handler,實際上,它只是一個 flutter engine API 的包裝類,也就是說圖片的編解碼邏輯不是在 Dart 代碼部分實現(xiàn),而是在 flutter engine中實現(xiàn)的。Codec類部分定義如下:

  1. @pragma('vm:entry-point')
  2. class Codec extends NativeFieldWrapperClass2 {
  3. // 此類由flutter engine創(chuàng)建,不應該手動實例化此類或直接繼承此類。
  4. @pragma('vm:entry-point')
  5. Codec._();
  6. /// 圖片中的幀數(shù)(動態(tài)圖會有多幀)
  7. int get frameCount native 'Codec_frameCount';
  8. /// 動畫重復的次數(shù)
  9. /// * 0 表示只執(zhí)行一次
  10. /// * -1 表示循環(huán)執(zhí)行
  11. int get repetitionCount native 'Codec_repetitionCount';
  12. /// 獲取下一個動畫幀
  13. Future<FrameInfo> getNextFrame() {
  14. return _futurize(_getNextFrame);
  15. }
  16. String _getNextFrame(_Callback<FrameInfo> callback) native 'Codec_getNextFrame';

我們可以看到Codec最終的結(jié)果是一個或多個(動圖)幀,而這些幀最終會繪制到屏幕上。

MultiFrameImageStreamCompleter 的 codec參數(shù)值為_loadAsync方法的返回值,我們繼續(xù)看_loadAsync方法的實現(xiàn):

  1. Future<ui.Codec> _loadAsync(
  2. NetworkImage key,
  3. StreamController<ImageChunkEvent> chunkEvents,
  4. ) async {
  5. try {
  6. //下載圖片
  7. final Uri resolved = Uri.base.resolve(key.url);
  8. final HttpClientRequest request = await _httpClient.getUrl(resolved);
  9. headers?.forEach((String name, String value) {
  10. request.headers.add(name, value);
  11. });
  12. final HttpClientResponse response = await request.close();
  13. if (response.statusCode != HttpStatus.ok)
  14. throw Exception(...);
  15. // 接收圖片數(shù)據(jù)
  16. final Uint8List bytes = await consolidateHttpClientResponseBytes(
  17. response,
  18. onBytesReceived: (int cumulative, int total) {
  19. chunkEvents.add(ImageChunkEvent(
  20. cumulativeBytesLoaded: cumulative,
  21. expectedTotalBytes: total,
  22. ));
  23. },
  24. );
  25. if (bytes.lengthInBytes == 0)
  26. throw Exception('NetworkImage is an empty file: $resolved');
  27. // 對圖片數(shù)據(jù)進行解碼
  28. return PaintingBinding.instance.instantiateImageCodec(bytes);
  29. } finally {
  30. chunkEvents.close();
  31. }
  32. }

可以看到_loadAsync方法主要做了兩件事:

  1. 下載圖片。
  2. 對下載的圖片數(shù)據(jù)進行解碼。

下載邏輯比較簡單:通過HttpClient從網(wǎng)上下載圖片,另外下載請求會設(shè)置一些自定義的 header,開發(fā)者可以通過NetworkImageheaders命名參數(shù)來傳遞。

在圖片下載完成后調(diào)用了PaintingBinding.instance.instantiateImageCodec(bytes)對圖片進行解碼,值得注意的是instantiateImageCodec(...)也是一個 Native API 的包裝,實際上會調(diào)用 Flutter engine 的instantiateImageCodec方法,源碼如下:

  1. String _instantiateImageCodec(Uint8List list, _Callback<Codec> callback, _ImageInfo imageInfo, int targetWidth, int targetHeight)
  2. native 'instantiateImageCodec';

#obtainKey(ImageConfiguration)方法

該接口主要是為了配合實現(xiàn)圖片緩存,ImageProvider從數(shù)據(jù)源加載完數(shù)據(jù)后,會在全局的ImageCache中緩存圖片數(shù)據(jù),而圖片數(shù)據(jù)緩存是一個 Map,而 Map 的 key 便是調(diào)用此方法的返回值,不同的 key 代表不同的圖片數(shù)據(jù)緩存。

#resolve(ImageConfiguration) 方法

resolve方法是ImageProvider的暴露的給Image的主入口方法,它接受一個ImageConfiguration參數(shù),返回ImageStream,即圖片數(shù)據(jù)流。我們重點看一下resolve執(zhí)行流程:

  1. ImageStream resolve(ImageConfiguration configuration) {
  2. ... //省略無關(guān)代碼
  3. final ImageStream stream = ImageStream();
  4. T obtainedKey; //
  5. //定義錯誤處理函數(shù)
  6. Future<void> handleError(dynamic exception, StackTrace stack) async {
  7. ... //省略無關(guān)代碼
  8. stream.setCompleter(imageCompleter);
  9. imageCompleter.setError(...);
  10. }
  11. // 創(chuàng)建一個新Zone,主要是為了當發(fā)生錯誤時不會干擾MainZone
  12. final Zone dangerZone = Zone.current.fork(...);
  13. dangerZone.runGuarded(() {
  14. Future<T> key;
  15. // 先驗證是否已經(jīng)有緩存
  16. try {
  17. // 生成緩存key,后面會根據(jù)此key來檢測是否有緩存
  18. key = obtainKey(configuration);
  19. } catch (error, stackTrace) {
  20. handleError(error, stackTrace);
  21. return;
  22. }
  23. key.then<void>((T key) {
  24. obtainedKey = key;
  25. // 緩存的處理邏輯在這里,記為A,下面詳細介紹
  26. final ImageStreamCompleter completer = PaintingBinding.instance
  27. .imageCache.putIfAbsent(key, () => load(key), onError: handleError);
  28. if (completer != null) {
  29. stream.setCompleter(completer);
  30. }
  31. }).catchError(handleError);
  32. });
  33. return stream;
  34. }

ImageConfiguration 包含圖片和設(shè)備的相關(guān)信息,如圖片的大小、所在的AssetBundle(只有打到安裝包的圖片存在)以及當前的設(shè)備平臺、devicePixelRatio(設(shè)備像素比等)。Flutter SDK 提供了一個便捷函數(shù)createLocalImageConfiguration來創(chuàng)建ImageConfiguration 對象:

  1. ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size size }) {
  2. return ImageConfiguration(
  3. bundle: DefaultAssetBundle.of(context),
  4. devicePixelRatio: MediaQuery.of(context, nullOk: true)?.devicePixelRatio ?? 1.0,
  5. locale: Localizations.localeOf(context, nullOk: true),
  6. textDirection: Directionality.of(context),
  7. size: size,
  8. platform: defaultTargetPlatform,
  9. );
  10. }

我們可以發(fā)現(xiàn)這些信息基本都是通過Context來獲取。

上面代碼 A 處就是處理緩存的主要代碼,這里的PaintingBinding.instance.imageCacheImageCache的一個實例,它是PaintingBinding的一個屬性,而 Flutter 框架中的PaintingBinding.instance是一個單例,imageCache事實上也是一個單例,也就是說圖片緩存是全局的,統(tǒng)一由PaintingBinding.instance.imageCache 來管理。

下面我們看看ImageCache類定義:

  1. const int _kDefaultSize = 1000;
  2. const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
  3. class ImageCache {
  4. // 正在加載中的圖片隊列
  5. final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
  6. // 緩存隊列
  7. final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
  8. // 緩存數(shù)量上限(1000)
  9. int _maximumSize = _kDefaultSize;
  10. // 緩存容量上限 (100 MB)
  11. int _maximumSizeBytes = _kDefaultSizeBytes;
  12. // 緩存上限設(shè)置的setter
  13. set maximumSize(int value) {...}
  14. set maximumSizeBytes(int value) {...}
  15. ... // 省略部分定義
  16. // 清除所有緩存
  17. void clear() {
  18. // ...省略具體實現(xiàn)代碼
  19. }
  20. // 清除指定key對應的圖片緩存
  21. bool evict(Object key) {
  22. // ...省略具體實現(xiàn)代碼
  23. }
  24. ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
  25. assert(key != null);
  26. assert(loader != null);
  27. ImageStreamCompleter result = _pendingImages[key]?.completer;
  28. // 圖片還未加載成功,直接返回
  29. if (result != null)
  30. return result;
  31. // 有緩存,繼續(xù)往下走
  32. // 先移除緩存,后再添加,可以讓最新使用過的緩存在_map中的位置更近一些,清理時會LRU來清除
  33. final _CachedImage image = _cache.remove(key);
  34. if (image != null) {
  35. _cache[key] = image;
  36. return image.completer;
  37. }
  38. try {
  39. result = loader();
  40. } catch (error, stackTrace) {
  41. if (onError != null) {
  42. onError(error, stackTrace);
  43. return null;
  44. } else {
  45. rethrow;
  46. }
  47. }
  48. void listener(ImageInfo info, bool syncCall) {
  49. final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
  50. final _CachedImage image = _CachedImage(result, imageSize);
  51. // 下面是緩存處理的邏輯
  52. if (maximumSizeBytes > 0 && imageSize > maximumSizeBytes) {
  53. _maximumSizeBytes = imageSize + 1000;
  54. }
  55. _currentSizeBytes += imageSize;
  56. final _PendingImage pendingImage = _pendingImages.remove(key);
  57. if (pendingImage != null) {
  58. pendingImage.removeListener();
  59. }
  60. _cache[key] = image;
  61. _checkCacheSize();
  62. }
  63. if (maximumSize > 0 && maximumSizeBytes > 0) {
  64. final ImageStreamListener streamListener = ImageStreamListener(listener);
  65. _pendingImages[key] = _PendingImage(result, streamListener);
  66. // Listener is removed in [_PendingImage.removeListener].
  67. result.addListener(streamListener);
  68. }
  69. return result;
  70. }
  71. // 當緩存數(shù)量超過最大值或緩存的大小超過最大緩存容量,會調(diào)用此方法清理到緩存上限以內(nèi)
  72. void _checkCacheSize() {
  73. while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
  74. final Object key = _cache.keys.first;
  75. final _CachedImage image = _cache[key];
  76. _currentSizeBytes -= image.sizeBytes;
  77. _cache.remove(key);
  78. }
  79. ... //省略無關(guān)代碼
  80. }
  81. }

有緩存則使用緩存,沒有緩存則調(diào)用 load 方法加載圖片,加載成功后:

  1. 先判斷圖片數(shù)據(jù)有沒有緩存,如果有,則直接返回ImageStream。
  2. 如果沒有緩存,則調(diào)用load(T key)方法從數(shù)據(jù)源加載圖片數(shù)據(jù),加載成功后先緩存,然后返回 ImageStream。

另外,我們可以看到ImageCache類中有設(shè)置緩存上限的setter,所以,如果我們可以自定義緩存上限:

  1. PaintingBinding.instance.imageCache.maximumSize=2000; //最多2000張
  2. PaintingBinding.instance.imageCache.maximumSizeBytes = 200 << 20; //最大200M

現(xiàn)在我們看一下緩存的 key,因為 Map 中相同 key 的值會被覆蓋,也就是說 key 是圖片緩存的一個唯一標識,只要是不同 key,那么圖片數(shù)據(jù)就會分別緩存(即使事實上是同一張圖片)。那么圖片的唯一標識是什么呢?跟蹤源碼,很容易發(fā)現(xiàn) key 正是ImageProvider.obtainKey()方法的返回值,而此方法需要ImageProvider子類去重寫,這也就意味著不同的ImageProvider對key的定義邏輯會不同。其實也很好理解,比如對于NetworkImage,將圖片的 url 作為 key 會很合適,而對于AssetImage,則應該將“包名+路徑”作為唯一的 key。下面我們以NetworkImage為例,看一下它的obtainKey()實現(xiàn):

  1. @override
  2. Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
  3. return SynchronousFuture<NetworkImage>(this);
  4. }

代碼很簡單,創(chuàng)建了一個同步的 future,然后直接將自身做為 key 返回。因為 Map 中在判斷 key(此時是NetworkImage對象)是否相等時會使用“==”運算符,那么定義 key 的邏輯就是NetworkImage的“==”運算符:

  1. @override
  2. bool operator ==(dynamic other) {
  3. ... //省略無關(guān)代碼
  4. final NetworkImage typedOther = other;
  5. return url == typedOther.url
  6. && scale == typedOther.scale;
  7. }

很清晰,對于網(wǎng)絡(luò)圖片來說,會將其“url+縮放比例”作為緩存的 key。也就是說如果兩張圖片的 url 或 scale 只要有一個不同,便會重新下載并分別緩存

另外,我們需要注意的是,圖片緩存是在內(nèi)存中,并沒有進行本地文件持久化存儲,這也是為什么網(wǎng)絡(luò)圖片在應用重啟后需要重新聯(lián)網(wǎng)下載的原因。

同時也意味著在應用生命周期內(nèi),如果緩存沒有超過上限,相同的圖片只會被下載一次。

#總結(jié)

上面主要結(jié)合源碼,探索了ImageProvider的主要功能和原理,如果要用一句話來總結(jié)ImageProvider功能,那么應該是:加載圖片數(shù)據(jù)并進行緩存、解碼。在此再次提醒讀者,F(xiàn)lutter 的源碼是非常好的第一手資料,建議讀者多多探索,另外,在閱讀源碼學習的同時一定要有總結(jié),這樣才不至于在源碼中迷失。

#14.5.2 Image組件原理

前面章節(jié)中我們介紹過Image的基礎(chǔ)用法,現(xiàn)在我們更深入一些,研究一下Image是如何和ImageProvider配合來獲取最終解碼后的數(shù)據(jù),然后又如何將圖片繪制到屏幕上的。

本節(jié)換一個思路,我們先不去直接看Image的源碼,而根據(jù)已經(jīng)掌握的知識來實現(xiàn)一個簡版的“Image組件” MyImage,代碼大致如下:

  1. class MyImage extends StatefulWidget {
  2. const MyImage({
  3. Key key,
  4. @required this.imageProvider,
  5. })
  6. : assert(imageProvider != null),
  7. super(key: key);
  8. final ImageProvider imageProvider;
  9. @override
  10. _MyImageState createState() => _MyImageState();
  11. }
  12. class _MyImageState extends State<MyImage> {
  13. ImageStream _imageStream;
  14. ImageInfo _imageInfo;
  15. @override
  16. void didChangeDependencies() {
  17. super.didChangeDependencies();
  18. // 依賴改變時,圖片的配置信息可能會發(fā)生改變
  19. _getImage();
  20. }
  21. @override
  22. void didUpdateWidget(MyImage oldWidget) {
  23. super.didUpdateWidget(oldWidget);
  24. if (widget.imageProvider != oldWidget.imageProvider)
  25. _getImage();
  26. }
  27. void _getImage() {
  28. final ImageStream oldImageStream = _imageStream;
  29. // 調(diào)用imageProvider.resolve方法,獲得ImageStream。
  30. _imageStream =
  31. widget.imageProvider.resolve(createLocalImageConfiguration(context));
  32. //判斷新舊ImageStream是否相同,如果不同,則需要調(diào)整流的監(jiān)聽器
  33. if (_imageStream.key != oldImageStream?.key) {
  34. final ImageStreamListener listener = ImageStreamListener(_updateImage);
  35. oldImageStream?.removeListener(listener);
  36. _imageStream.addListener(listener);
  37. }
  38. }
  39. void _updateImage(ImageInfo imageInfo, bool synchronousCall) {
  40. setState(() {
  41. // Trigger a build whenever the image changes.
  42. _imageInfo = imageInfo;
  43. });
  44. }
  45. @override
  46. void dispose() {
  47. _imageStream.removeListener(ImageStreamListener(_updateImage));
  48. super.dispose();
  49. }
  50. @override
  51. Widget build(BuildContext context) {
  52. return RawImage(
  53. image: _imageInfo?.image, // this is a dart:ui Image object
  54. scale: _imageInfo?.scale ?? 1.0,
  55. );
  56. }
  57. }

上面代碼流程如下:

  1. 通過imageProvider.resolve方法可以得到一個ImageStream(圖片數(shù)據(jù)流),然后監(jiān)聽ImageStream的變化。當圖片數(shù)據(jù)源發(fā)生變化時,ImageStream會觸發(fā)相應的事件,而本例中我們只設(shè)置了圖片成功的監(jiān)聽器_updateImage,而_updateImage中只更新了_imageInfo。值得注意的是,如果是靜態(tài)圖,ImageStream只會觸發(fā)一次時間,如果是動態(tài)圖,則會觸發(fā)多次事件,每一次都會有一個解碼后的圖片幀。
  2. _imageInfo 更新后會 rebuild,此時會創(chuàng)建一個RawImage Widget。RawImage最終會通過RenderImage來將圖片繪制在屏幕上。如果繼續(xù)跟進RenderImage類,我們會發(fā)現(xiàn)RenderImagepaint 方法中調(diào)用了paintImage方法,而paintImage方法中通過CanvasdrawImageRect(…)drawImageNine(...)等方法來完成最終的繪制。
  3. 最終的繪制由RawImage來完成。

下面測試一下MyImage

  1. class ImageInternalTestRoute extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return Column(
  5. children: <Widget>[
  6. MyImage(
  7. imageProvider: NetworkImage(
  8. "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
  9. ),
  10. )
  11. ],
  12. );
  13. }
  14. }

運行效果如圖14-4所示:

圖14-4

成功了! 現(xiàn)在,想必Image Widget 的源碼已經(jīng)沒必要在花費篇章去介紹了,讀者有興趣可以自行去閱讀。

#總結(jié)

本節(jié)主要介紹了 Flutter 圖片的加載、緩存和繪制流程。其中ImageProvider主要負責圖片數(shù)據(jù)的加載和緩存,而繪制部分邏輯主要是由RawImage來完成。 而Image正是連接起ImageProviderRawImage 的橋梁。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號