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

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

0、大綱

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

一、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,這裏再也不貼代碼。異步

二、ImageProvider

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

帶着疑問,咱們先進到ImageProvider的源碼,能夠發現其實這個類很是簡單,代碼量也很少,能夠看看resolve方法的核心部分:ide

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:ui

@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方法進行圖片解碼)。this

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

三、圖片數據加載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註冊的監聽器。

四、緩存池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);
}

五、網絡圖片加載

看完最基本的圖片數據加載,接下來看看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狀態的方法。

相關文章
相關標籤/搜索