flutter源碼學習筆記-圖片加載流程

本文基於1.12.13+hotfix.8版本源碼分析。

0、大綱

  1. Image
  2. ImageProvider
  3. 圖片數據加載 ImageStream、ImageStreamCompleter
  4. 緩存池 PaintingBinding#imageCache
  5. 網絡圖片加載

1、Image

點擊進入源碼,可以看到Image繼承自StatefulWidget,那麼重點自然在State裏面。跟着生命周期走,可以發現在didUpdateWidget中調用了這個方法:

  void _resolveImage() {      // 在這裡獲取到一個流對象      final ImageStream newStream =        widget.image.resolve(createLocalImageConfiguration(          context,          size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,        ));      assert(newStream != null);      _updateSourceStream(newStream);    }      void _updateSourceStream(ImageStream newStream) {      // ... 省略部分源碼      if (_isListeningToStream)        _imageStream.addListener(_getListener());    }      ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) {      loadingBuilder ??= widget.loadingBuilder;      return ImageStreamListener(        _handleImageFrame,        onChunk: loadingBuilder == null ? null : _handleImageChunk,      );    }  

在這裡調用了image(ImageProvider)的resolve方法獲取到一個ImageStream,並給這個流設置了監聽器。從名字上,不難猜出這是個圖片數據流,在listener拿到數據後會調用setState(() {})方法進行rebuild,這裡不再貼代碼。

2、ImageProvider

在上面我們看到了Image是需要接收圖片數據進行繪製的,那麼,這個數據是在哪裡解碼的?又是哪裡發送過來的?

帶着疑問,我們先進到ImageProvider的源碼,可以發現其實這個類非常簡單,代碼量也不多,可以看看resolve方法的核心部分:

  Future<T> key;    try {      key = obtainKey(configuration);    } catch (error, stackTrace) {      handleError(error, stackTrace);      return;    }    key.then<void>((T key) {      obtainedKey = key;      final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(        key,        () => load(key, PaintingBinding.instance.instantiateImageCodec),        onError: handleError,      );      if (completer != null) {        stream.setCompleter(completer);      }    }).catchError(handleError);  

可以看到,這裡會異步獲取到一個key,然後從管理在PaintingBinding中的緩存池查找圖片流。繼續看關鍵的obtainKey和load方法,去到定義的地方,可以發現這兩個都是子類實現的。從注釋中可以看到,obtainKey的功能就是根據傳入的ImageConfiguration生成一個獨一無二的key(廢話),而load方法則是將key轉換成為一個ImageStreamCompleter對象並開始加載圖片。

那麼,我們從最簡單的MemoryImage入手,首先看看obtainKey:

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

可以看到,就只是把自己包了一層再返回,並沒有什麼特殊。接着看load:

  @override    ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) {      return MultiFrameImageStreamCompleter(        codec: _loadAsync(key, decode),        scale: key.scale,      );    }      Future<ui.Codec> _loadAsync(MemoryImage key, DecoderCallback decode) {      assert(key == this);      return decode(bytes);    }  

同樣非常簡單,就是創建了一個ImageStreamCompleter的子類對象,同時傳入了一個包裝了解碼器的Future(這個解碼器是PaintingBinding.instance.instantiateImageCodec,內部調用native方法進行圖片解碼)。

看到這裡,相信基本有猜想了,數據和解碼器都提供了,看來ImageStreamCompleter就是我們要看的數據源提供者。

3、圖片數據加載ImageStream、ImageStreamCompleter

廢話不多說,直接看MultiFrameImageStreamCompleter,可以看到直接在構造函數中獲取codec對象,在獲取到以後就會去獲取解碼數據,下面是簡化的代碼片段:

  // 構造函數中獲取codec    codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {// 略});      void _handleCodecReady(ui.Codec codec) {      _codec = codec;      assert(_codec != null);        if (hasListeners) {        // 拿到codec之後解碼數據        _decodeNextFrameAndSchedule();      }    }      Future<void> _decodeNextFrameAndSchedule() async {      try {        _nextFrame = await _codec.getNextFrame();      } catch (exception, stack) {        // 略        return;      }      if (_codec.frameCount == 1) {        // 發送數據        _emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));        return;      }      _scheduleAppFrame();    }  

看到這裡,終於找到了發送數據的地方,_emitFrame裏面會調用setImage,而後在setImage中會找到listener並將數據發送,而listener就是widgets.Image註冊的監聽器。

4、緩存池PaintingBinding#imageCache

看完了加載流程,我們看看緩存池的緩存邏輯,回到ImageProvider的resolve方法,這裡有個關鍵點,傳給PaintingBinding的是個創建方法,而非實體。進入其源碼可以看到是先檢測cache中是否存在該對象,存在則直接返回,不存在才會調用load方法進行創建:

final _CachedImage image = _cache.remove(key);  if (image != null) {    // 有緩存就直接返回    _cache[key] = image;    return image.completer;  }  try {    // 沒找到緩存就調外面傳入的loader()進行創建    result = loader();  } // catch部分省略  

並且,在剛創建時緩存中的對象是個PendingImage,這東西可以理解為類似一個佔位符的作用,當圖片數據加載完畢後才替換成實際數據對象CacheImage:

  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();  }    // 這裡創建了一個PendingImage插入緩存  if (maximumSize > 0 && maximumSizeBytes > 0) {    final ImageStreamListener streamListener = ImageStreamListener(listener);    _pendingImages[key] = _PendingImage(result, streamListener);    // 監聽加載狀態,result就是ImageStreamCompleter    result.addListener(streamListener);  }  

5、網絡圖片加載

看完最基本的圖片數據加載,接下來看看NetworkImage如何加載網絡圖片。看核心的load方法:

  ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {      final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();      return MultiFrameImageStreamCompleter(        // 關鍵點1,加載、解析數據        codec: _loadAsync(key, chunkEvents, decode),        // 關鍵點2,分塊下載事件流傳給completer用        chunkEvents: chunkEvents.stream,        scale: key.scale,      );    }  

直接進入關鍵方法,看NetworkImage的_loadAsync方法:

  Future<ui.Codec> _loadAsync(      NetworkImage key,      StreamController<ImageChunkEvent> chunkEvents,      image_provider.DecoderCallback decode,    ) async {      try {        assert(key == this);          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 image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);          // 接收數據        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');          // 解碼數據        return decode(bytes);      } finally {        chunkEvents.close();      }    }  

這裡有2個點:

(1)通過HttpClient進行圖片下載,下載失敗或者數據為空都會拋異常,這裡要做好異常處理。另外,從上面的圖片緩存邏輯可以看到,flutter默認是只有內存緩存的,磁盤緩存需要自己處理,可以在這裡入手處理;

(2)通過consolidateHttpClientResponseBytes接收數據,並將下載進度轉成ImageChunkEvent發送出去。可以看看MultiFrameImageStreamCompleter對ImageChunkEvent的處理:

if (chunkEvents != null) {    chunkEvents.listen(      (ImageChunkEvent event) {        if (hasListeners) {          // 把這個事件傳遞給ImageStreamListener的onChunk方法          final List<ImageChunkListener> localListeners = _listeners              .map<ImageChunkListener>((ImageStreamListener listener) => listener.onChunk)              .where((ImageChunkListener chunkListener) => chunkListener != null)              .toList();          for (ImageChunkListener listener in localListeners) {            listener(event);          }        }      }    );  }  

順着_listeners的來源,一路往上找,最後可以看到onChunk方法是這裡傳進來的:

  ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) {      loadingBuilder ??= widget.loadingBuilder;      return ImageStreamListener(        _handleImageFrame,        onChunk: loadingBuilder == null ? null : _handleImageChunk,      );    }  

widget.loadingBuilder即自定義loading狀態的方法。