探索Flutter Image顯示Webp邏輯

KoonChaoSo 發表於 2019-10-09

簡介

最近探索了一下新增Flutter的Image widget對webp做一個stopAnimation的擴充的Api,順便了解一下Image整個結構和對一些多幀圖片的處理。 我們先看看Image的一個類圖結構。

探索Flutter Image顯示Webp邏輯
其中:

  • ImageProvider 提供載入圖片的入口,不同的圖片資源載入方式不一樣,只要重寫其load方法即可。同樣,快取圖片的key值也有其生成。
  • FileImage 負責讀取檔案圖片的資料,讀取到的檔案資料轉化成ui.Codec物件交給ImageStreamCompleter去處理解析。
  • ImageStreamCompleter就是逐幀解析圖片的類,生成之後會加入ImageCache,下載可以從快取中得到。
  • ImageStream是處理Image Resource的,ImageState通過ImageStream與ImageStreamCompleter建立聯絡。ImageStream裡也儲存著圖片載入完畢的監聽回撥。
  • MultiFrameImageStreamCompleter就是多幀圖片解析器。 Flutter imgae支援的圖片格式為:JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, and WBMP。Flutter Image是顯示圖片的一個Widget。 Flutter Image的幾個構造方法:
方法 釋義
Image() 從ImageProvider中獲取圖片,從本質上看,下面的幾個方法都是他的具體實現。
Image.asset(String name) 從AssetBundler中獲取圖片
Image.network(String src) 顯示網路圖片
Image.file(File file) 從檔案中獲取圖片
Image.memory(Uint8List bytes) 從Uint8List獲取資料顯示圖片

Image

從Image的構造體上看,ImageProvider才是圖片提供方,所以我們後面會看看ImageProvider究竟是要做點什麼的。 其他的引數是一些圖片的屬性和一些builder。

ImageState

關鍵程式碼:

void didUpdateWidget(Image oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (_isListeningToStream &&
        (widget.loadingBuilder == null) != (oldWidget.loadingBuilder == null)) {
      _imageStream.removeListener(_getListener(oldWidget.loadingBuilder));
      _imageStream.addListener(_getListener());
    }
    if (widget.image != oldWidget.image)
      _resolveImage();
  }
複製程式碼

ImageProvider

其實ImageProvider是一個抽象類,讓需要定製的子類去做一些實現。

比如:FileImage、MemoryImage、ExactAssetImage等等。其中對FileImage的程式碼進行了一些分析。

class FileImage extends ImageProvider<FileImage> {
  /// Creates an object that decodes a [File] as an image.
  ///
  /// The arguments must not be null.
  const FileImage(this.file, { this.scale: 1.0 })
      : assert(file != null),
        assert(scale != null);

  /// The file to decode into an image.
  final File file;

  /// The scale to place in the [ImageInfo] object of the image.
  final double scale;

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

  @override
  ImageStreamCompleter load(FileImage key) {
    return new MultiFrameImageStreamCompleter(
      codec: _loadAsync(key),
      scale: key.scale,
      informationCollector: (StringBuffer information) {
        information.writeln('Path: ${file?.path}');
      }
    );
  }

  Future<ui.Codec> _loadAsync(FileImage key) async {
    assert(key == this);

    final Uint8List bytes = await file.readAsBytes();
    if (bytes.lengthInBytes == 0)
      return null;

    return await ui.instantiateImageCodec(bytes);
  }

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
    final FileImage typedOther = other;
    return file?.path == typedOther.file?.path
        && scale == typedOther.scale;
  }

  @override
  int get hashCode => hashValues(file?.path, scale);

  @override
  String toString() => '$runtimeType("${file?.path}", scale: $scale)';
}
複製程式碼

FileImage重寫了 obtainKey、load的方法。但是在什麼地方會呼叫這兩個重寫的方法呢?那肯定是ImageProvider這個父類了。

@optionalTypeArgs
abstract class ImageProvider<T> {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const ImageProvider();

  /// Resolves this image provider using the given `configuration`, returning
  /// an [ImageStream].
  ///
  /// This is the public entry-point of the [ImageProvider] class hierarchy.
  ///
  /// Subclasses should implement [obtainKey] and [load], which are used by this
  /// method.
  ImageStream resolve(ImageConfiguration configuration) {
    assert(configuration != null);
    final ImageStream stream = new ImageStream();
    T obtainedKey;
    obtainKey(configuration).then<void>((T key) {
      obtainedKey = key;
      stream.setCompleter(PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key)));
    }).catchError(
      (dynamic exception, StackTrace stack) async {
        FlutterError.reportError(new FlutterErrorDetails(
          exception: exception,
          stack: stack,
          library: 'services library',
          context: 'while resolving an image',
          silent: true, // could be a network error or whatnot
          informationCollector: (StringBuffer information) {
            information.writeln('Image provider: $this');
            information.writeln('Image configuration: $configuration');
            if (obtainedKey != null)
              information.writeln('Image key: $obtainedKey');
          }
        ));
        return null;
      }
    );
    return stream;
  }

  /// Converts an ImageProvider's settings plus an ImageConfiguration to a key
  /// that describes the precise image to load.
  ///
  /// The type of the key is determined by the subclass. It is a value that
  /// unambiguously identifies the image (_including its scale_) that the [load]
  /// method will fetch. Different [ImageProvider]s given the same constructor
  /// arguments and [ImageConfiguration] objects should return keys that are
  /// '==' to each other (possibly by using a class for the key that itself
  /// implements [==]).
  @protected
  Future<T> obtainKey(ImageConfiguration configuration);

  /// Converts a key into an [ImageStreamCompleter], and begins fetching the
  /// image.
  @protected
  ImageStreamCompleter load(T key);

  @override
  String toString() => '$runtimeType()';
}
複製程式碼

該方法的作用就是建立一個ImageStream,並且ImageConfiguration作為key從ImageCache中獲取ImageCompleter,設定到ImageStream上面。而ImageCompleter是為了設定一些回撥和幫助ImageStream設定圖片的一個類。

ImageConfiguration是對於ImageCompleter的一些配置。

ImageCache是對於ImageCompleter的快取。 ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) 這個方法在resolve方法中是一個關鍵方法。

ImageStreamCompleter putIfAbsentImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader()) {
    assert(key != null);
    assert(loader != null);
    ImageStreamCompleter result = _cache[key];
    if (result != null) {
      // Remove the provider from the list so that we can put it back in below
      // and thus move it to the end of the list.
      _cache.remove(key);
    } else {
      if (_cache.length == maximumSize && maximumSize > 0)
        _cache.remove(_cache.keys.first);
      result = loader();
    }
    if (maximumSize > 0) {
      assert(_cache.length < maximumSize);
      _cache[key] = result;
    }
    assert(_cache.length <= maximumSize);
    return result;
 }
複製程式碼

這個方法是在imageCache裡面的,提供的是記憶體快取api的入口方法,putIfAbsent會先通過key獲取之前的ImageStreamCompleter物件,這個key就是NetworkImage物件,當然我們也可以重寫obtainKey方法自定義key,如果存在則直接返回,如果不存在則執行load方法載入ImageStreamCompleter物件,並將其放到首位(最少最近使用演算法)。 也就是說ImageProvider已經實現了記憶體快取:預設快取圖片的最大個數是1000,預設快取圖片的最大空間是10MiB。 第一次載入圖片肯定是沒有快取的,所以會呼叫loader方法,那就是方法外面傳進去的load()方法。

FileImage的load方法

@override
ImageStreamCompleter load(FileImage key) {
  return new MultiFrameImageStreamCompleter(
    codec: _loadAsync(key),
    scale: key.scale,
    informationCollector: (StringBuffer information) {
      information.writeln('Path: ${file?.path}');
    }
  );
}

Future<ui.Codec> _loadAsync(FileImage key) async {
  assert(key == this);

  final Uint8List bytes = await file.readAsBytes();
  if (bytes.lengthInBytes == 0)
    return null;

  return await ui.instantiateImageCodec(bytes);
}
複製程式碼

load方法中使用了一個叫MultiFrameImageStreamCompleter的類:

MultiFrameImageStreamCompleter({
  @required Future<ui.Codec> codec,
  @required double scale,
  InformationCollector informationCollector
}) : assert(codec != null),
     _informationCollector = informationCollector,
     _scale = scale,
     _framesEmitted = 0,
     _timer = null {
  codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
    FlutterError.reportError(new FlutterErrorDetails(
      exception: error,
      stack: stack,
      library: 'services',
      context: 'resolving an image codec',
      informationCollector: informationCollector,
      silent: true,
    ));
  });
}
複製程式碼

MultiFrameImageStreamCompleter是ImageStreamCompleter的子類,為了處理多幀的圖片載入,Flutter的Image支援載入webp,通過MultiFrameImageStreamCompleter可以對webp檔案進行解析,MultiFrameImageStreamCompleter拿到外面傳入的codec資料物件,通過handleCodecReady來儲存Codec,之後呼叫decodeNextFrameAndSchedule方法,從Codec獲取下一幀圖片資料和把資料通知回撥到Image,並且開啟定時解析下一幀圖片資料。

到此為止,基本dart流程就走完了,所以需要做stopAnimation和startAnimation的改造就應該這這個MultiFrameImageStreamCompleter入手了。

最後

整個在Dart層面Image解析webp的流程就這樣,下篇再介紹下現在Dart的一些Codec的工作流程。