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

2021-03-09 14:42 更新

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

#14.5.1 ImageProvider

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

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

我們看看ImageProvider抽象類的詳細(xì)定義:

abstract class ImageProvider<T> {


  ImageStream resolve(ImageConfiguration configuration) {
    // 實(shí)現(xiàn)代碼省略
  }
  Future<bool> evict({ ImageCache cache,
                      ImageConfiguration configuration = ImageConfiguration.empty }) async {
    // 實(shí)現(xiàn)代碼省略
  }


  Future<T> obtainKey(ImageConfiguration configuration); 
  @protected
  ImageStreamCompleter load(T key); // 需子類實(shí)現(xiàn)
}

#load(T key)方法

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

@override
ImageStreamCompleter load(image_provider.NetworkImage key) {


  final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();

  
  return MultiFrameImageStreamCompleter(
    codec: _loadAsync(key, chunkEvents), //調(diào)用
    chunkEvents: chunkEvents.stream,
    scale: key.scale,
    ... //省略無關(guān)代碼
  );
}

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

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

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

@pragma('vm:entry-point')
class Codec extends NativeFieldWrapperClass2 {
  // 此類由flutter engine創(chuàng)建,不應(yīng)該手動(dòng)實(shí)例化此類或直接繼承此類。
  @pragma('vm:entry-point')
  Codec._();


  /// 圖片中的幀數(shù)(動(dòng)態(tài)圖會(huì)有多幀)
  int get frameCount native 'Codec_frameCount';


  /// 動(dòng)畫重復(fù)的次數(shù)
  /// * 0 表示只執(zhí)行一次
  /// * -1 表示循環(huán)執(zhí)行
  int get repetitionCount native 'Codec_repetitionCount';


  /// 獲取下一個(gè)動(dòng)畫幀
  Future<FrameInfo> getNextFrame() {
    return _futurize(_getNextFrame);
  }


  String _getNextFrame(_Callback<FrameInfo> callback) native 'Codec_getNextFrame';

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

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

 Future<ui.Codec> _loadAsync(
    NetworkImage key,
    StreamController<ImageChunkEvent> chunkEvents,
  ) async {
    try {
      //下載圖片
      final Uri resolved = Uri.base.resolve(key.url);
      final HttpClientRequest request = await _httpClient.getUrl(resolved);
      headers?.forEach((String name, String value) {
        request.headers.add(name, value);
      });
      final HttpClientResponse response = await request.close();
      if (response.statusCode != HttpStatus.ok)
        throw Exception(...);
      // 接收?qǐng)D片數(shù)據(jù) 
      final Uint8List bytes = await consolidateHttpClientResponseBytes(
        response,
        onBytesReceived: (int cumulative, int total) {
          chunkEvents.add(ImageChunkEvent(
            cumulativeBytesLoaded: cumulative,
            expectedTotalBytes: total,
          ));
        },
      );
      if (bytes.lengthInBytes == 0)
        throw Exception('NetworkImage is an empty file: $resolved');
      // 對(duì)圖片數(shù)據(jù)進(jìn)行解碼
      return PaintingBinding.instance.instantiateImageCodec(bytes);
    } finally {
      chunkEvents.close();
    }
  }

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

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

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

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

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

#obtainKey(ImageConfiguration)方法

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

#resolve(ImageConfiguration) 方法

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

ImageStream resolve(ImageConfiguration configuration) {
  ... //省略無關(guān)代碼
  final ImageStream stream = ImageStream();
  T obtainedKey; //
  //定義錯(cuò)誤處理函數(shù)
  Future<void> handleError(dynamic exception, StackTrace stack) async {
    ... //省略無關(guān)代碼
    stream.setCompleter(imageCompleter);
    imageCompleter.setError(...);
  }


  // 創(chuàng)建一個(gè)新Zone,主要是為了當(dāng)發(fā)生錯(cuò)誤時(shí)不會(huì)干擾MainZone
  final Zone dangerZone = Zone.current.fork(...);

  
  dangerZone.runGuarded(() {
    Future<T> key;
    // 先驗(yàn)證是否已經(jīng)有緩存
    try {
      // 生成緩存key,后面會(huì)根據(jù)此key來檢測(cè)是否有緩存
      key = obtainKey(configuration);
    } catch (error, stackTrace) {
      handleError(error, stackTrace);
      return;
    }
    key.then<void>((T key) {
      obtainedKey = key;
      // 緩存的處理邏輯在這里,記為A,下面詳細(xì)介紹
      final ImageStreamCompleter completer = PaintingBinding.instance
          .imageCache.putIfAbsent(key, () => load(key), onError: handleError);
      if (completer != null) {
        stream.setCompleter(completer);
      }
    }).catchError(handleError);
  });
  return stream;
}

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

ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size size }) {
  return ImageConfiguration(
    bundle: DefaultAssetBundle.of(context),
    devicePixelRatio: MediaQuery.of(context, nullOk: true)?.devicePixelRatio ?? 1.0,
    locale: Localizations.localeOf(context, nullOk: true),
    textDirection: Directionality.of(context),
    size: size,
    platform: defaultTargetPlatform,
  );
}

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

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

下面我們看看ImageCache類定義:

const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100 MiB


class ImageCache {
  // 正在加載中的圖片隊(duì)列
  final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
  // 緩存隊(duì)列
  final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};


  // 緩存數(shù)量上限(1000)
  int _maximumSize = _kDefaultSize;
  // 緩存容量上限 (100 MB)
  int _maximumSizeBytes = _kDefaultSizeBytes;

  
  // 緩存上限設(shè)置的setter
  set maximumSize(int value) {...}
  set maximumSizeBytes(int value) {...}

 
  ... // 省略部分定義


  // 清除所有緩存
  void clear() {
    // ...省略具體實(shí)現(xiàn)代碼
  }


  // 清除指定key對(duì)應(yīng)的圖片緩存
  bool evict(Object key) {
   // ...省略具體實(shí)現(xiàn)代碼
  }



 
  ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
    assert(key != null);
    assert(loader != null);
    ImageStreamCompleter result = _pendingImages[key]?.completer;
    // 圖片還未加載成功,直接返回
    if (result != null)
      return result;

 
    // 有緩存,繼續(xù)往下走
    // 先移除緩存,后再添加,可以讓最新使用過的緩存在_map中的位置更近一些,清理時(shí)會(huì)LRU來清除
    final _CachedImage image = _cache.remove(key);
    if (image != null) {
      _cache[key] = image;
      return image.completer;
    }
    try {
      result = loader();
    } catch (error, stackTrace) {
      if (onError != null) {
        onError(error, stackTrace);
        return null;
      } else {
        rethrow;
      }
    }
    void listener(ImageInfo info, bool syncCall) {
      final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
      final _CachedImage image = _CachedImage(result, imageSize);
      // 下面是緩存處理的邏輯
      if (maximumSizeBytes > 0 && imageSize > maximumSizeBytes) {
        _maximumSizeBytes = imageSize + 1000;
      }
      _currentSizeBytes += imageSize;
      final _PendingImage pendingImage = _pendingImages.remove(key);
      if (pendingImage != null) {
        pendingImage.removeListener();
      }


      _cache[key] = image;
      _checkCacheSize();
    }
    if (maximumSize > 0 && maximumSizeBytes > 0) {
      final ImageStreamListener streamListener = ImageStreamListener(listener);
      _pendingImages[key] = _PendingImage(result, streamListener);
      // Listener is removed in [_PendingImage.removeListener].
      result.addListener(streamListener);
    }
    return result;
  }


  // 當(dāng)緩存數(shù)量超過最大值或緩存的大小超過最大緩存容量,會(huì)調(diào)用此方法清理到緩存上限以內(nèi)
  void _checkCacheSize() {
   while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
      final Object key = _cache.keys.first;
      final _CachedImage image = _cache[key];
      _currentSizeBytes -= image.sizeBytes;
      _cache.remove(key);
    }
    ... //省略無關(guān)代碼
  }
}

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

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

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

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

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

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

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

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

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

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

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

#總結(jié)

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

#14.5.2 Image組件原理

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

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

class MyImage extends StatefulWidget {
  const MyImage({
    Key key,
    @required this.imageProvider,
  })
      : assert(imageProvider != null),
        super(key: key);


  final ImageProvider imageProvider;


  @override
  _MyImageState createState() => _MyImageState();
}


class _MyImageState extends State<MyImage> {
  ImageStream _imageStream;
  ImageInfo _imageInfo;


  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // 依賴改變時(shí),圖片的配置信息可能會(huì)發(fā)生改變
    _getImage();
  }


  @override
  void didUpdateWidget(MyImage oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.imageProvider != oldWidget.imageProvider)
      _getImage();
  }


  void _getImage() {
    final ImageStream oldImageStream = _imageStream;
    // 調(diào)用imageProvider.resolve方法,獲得ImageStream。
    _imageStream =
        widget.imageProvider.resolve(createLocalImageConfiguration(context));
    //判斷新舊ImageStream是否相同,如果不同,則需要調(diào)整流的監(jiān)聽器
    if (_imageStream.key != oldImageStream?.key) {
      final ImageStreamListener listener = ImageStreamListener(_updateImage);
      oldImageStream?.removeListener(listener);
      _imageStream.addListener(listener);
    }
  }


  void _updateImage(ImageInfo imageInfo, bool synchronousCall) {
    setState(() {
      // Trigger a build whenever the image changes.
      _imageInfo = imageInfo;
    });
  }


  @override
  void dispose() {
    _imageStream.removeListener(ImageStreamListener(_updateImage));
    super.dispose();
  }


  @override
  Widget build(BuildContext context) {
    return RawImage(
      image: _imageInfo?.image, // this is a dart:ui Image object
      scale: _imageInfo?.scale ?? 1.0,
    );
  }
}

上面代碼流程如下:

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

下面測(cè)試一下MyImage

class ImageInternalTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        MyImage(
          imageProvider: NetworkImage(
            "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
          ),
        )
      ],
    );
  }
}

運(yùn)行效果如圖14-4所示:

圖14-4

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

#總結(jié)

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

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)