Flutter 图片加载框架详解《一》FadeInImage

刚上手Flutter时会碰到几个图片加载的插件,刚开始可能无所谓性能好坏,能满足我们的需求就是好的插件 ;当我们遇到图片加载慢、加载图片失败时,我们就会去分析到底哪个图片插件是最适合我们,或者说为了满足我们的需求,我们要自己写一个图片插件。不多说,就常用的几个图片加载插件去分析一下看看到底哪个适合你。

FadeInImage

//assetNetwork 用于加载网络图片
FadeInImage.assetNetwork(placeholder: '', image: '')
//调用后会生成一个ImageProvider 类型的 image;
image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale)),

可以看出FadeInImage.assetNetwork() 通过NetworkImage() 去加载图片;但是这个ResizeImage类是干嘛用的?assetNetwork又起了什么作用呢?稍后再去解释这个类,我们先走完主要流程。(文章顺序是按照代码流程走,先介绍图像绘制后介绍图片网络加载

// build会用到这个方法,先了解一下
 Image _image({
// 这个参数就是刚才生成的image
    @required ImageProvider image,
//图片加载错误的errorBuilder
    ImageErrorWidgetBuilder errorBuilder,
//加载第一帧时的效果(如loadingBuild,淡入淡出效果的Build)
    ImageFrameBuilder frameBuilder,
  }) 
    return Image(
      image: image,
      errorBuilder: errorBuilder,
      frameBuilder: frameBuilder,
       ...
    );
  }
//build 才是重点
  @override
  Widget build(BuildContext context) {
//通过_image 方法将 ImageProvider 类型的 image 转换成 Widget 
    Widget result = _image(
      image: image,
      errorBuilder: imageErrorBuilder,
      frameBuilder: (BuildContext context, Widget child, int frame, bool wasSynchronouslyLoaded) {
//是否同步加载,默认是异步的,不会走这个
        if (wasSynchronouslyLoaded)
          return child;
//_AnimatedFadeOutFadeIn()方法是渐入渐出动画效果
        return _AnimatedFadeOutFadeIn(
          target: child,
          placeholder: _image(image: placeholder, errorBuilder: placeholderErrorBuilder),
          isTargetLoaded: frame != null,
          fadeInDuration: fadeInDuration,
          fadeOutDuration: fadeOutDuration,
          fadeInCurve: fadeInCurve,
          fadeOutCurve: fadeOutCurve,
        );
      },
    );
//excludeFromSemantics 默认是false,肯定对走下面的Semantics()方法,这个Semantics不做过多解释,是个辅助工具
    if (!excludeFromSemantics) {
      result = Semantics(
        container: imageSemanticLabel != null,
        image: true,
        label: imageSemanticLabel ?? '',
        child: result,
      );
    }

    return result;
  }

Semantics(语义) 用于描述Widget的含义最终达到描述应用程序的UI。这些描述可以通过辅助工具、搜索引擎和其他语义分析软件使用。它有点像HTML5的语义元素,在Android、iOS上更多是用于读屏,帮助一些有视力障碍的人使用我们的软件(Android TalkBack 和 iOS VoiceOver)。

1.图像绘制

走到这里我们并没有看到图片的网络请求是在哪里调用的?图片是如何绘制的?只是看到最终返回一个result,so 问题肯定出在result身上了,继续深扒_image()方法里的Image,看它是怎么作妖的。

class Image extends StatefulWidget {
//...省略部分代码
  @override
  Widget build(BuildContext context) {
    Widget result = RawImage(
      image: _imageInfo?.image,
      width: widget.width,
      height: widget.height,
      scale: _imageInfo?.scale ?? 1.0
      filterQuality: widget.filterQuality,//默认低质量
      //...
    );
//图片没有加载完成会执行frameBuilder 或者loadingBuilder, 会多次刷新这个build
    if (widget.frameBuilder != null)
      result = widget.frameBuilder(context, result, _frameNumber, _wasSynchronouslyLoaded);

    if (widget.loadingBuilder != null)
      result = widget.loadingBuilder(context, result, _loadingProgress);

    return result;
  }
}


class RawImage extends LeafRenderObjectWidget {

///The image is painted using [paintImage],paintImage绘制图片
/// Creates a widget that displays an image.创建一个widget显示图片
   const RawImage({
    Key key,
    this.image,
    this.width,
    this.height,
    this.scale = 1.0,
    this.color,
    this.colorBlendMode,
    this.fit,
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.centerSlice,
    this.matchTextDirection = false,
    this.invertColors = false,
    this.filterQuality = FilterQuality.low,
  })
//这里有个createRenderObject方法
 RenderImage createRenderObject(BuildContext context) {
    assert((!matchTextDirection && alignment is Alignment) || debugCheckHasDirectionality(context));
    return RenderImage(
      image: image,
     //...
    );
  }
//继续看这个RenderImage类
/// An image in the render tree.渲染树中的一幅图像。
///The render image attempts to find a size for itself that fits in the given
/// constraints and preserves the image's intrinsic aspect ratio.
///渲染图像试图为自己找到一个适合给定约束的大小,并保持图像固有的高宽比。
class RenderImage extends RenderBox {
  //这个类就是绘制图像了
 @override
  void paint(PaintingContext context, Offset offset) {
///...绘制方法
    paintImage(
      canvas: context.canvas,//画布
      rect: offset & size,//布局限制
      image: _image,
      scale: _scale,
      colorFilter: _colorFilter,
      fit: _fit,
      alignment: _resolvedAlignment,
      centerSlice: _centerSlice,
      repeat: _repeat,
      flipHorizontally: _flipHorizontally,
      invertColors: invertColors,
      filterQuality: _filterQuality,
    );
///...
}
}

到此这是图片绘制过程,但是这个绘制的Image 到底是啥?因为一开始参数传递时是ImageProvider,最终怎么会变成了Image 类型呢?推测这个应该是个流,但绘制时直接传了Image,有点懵了,可能是将流封装成了Image吧。继续深扒

void paintImage({
  @required Canvas canvas,
  @required Rect rect,
  @required ui.Image image,
  double scale = 1.0,
...
}) {
//...
  canvas.drawImageRect(image, sourceRect, destinationRect, paint);
//...
}

void drawImageRect(Image image, Rect src, Rect dst, Paint paint) {
}
//还是传了一个Image 类型

class Image extends NativeFieldWrapperClass2 {
// ...
//原来是封装成了Image了
//Converts the [Image] object into a byte array.将image对象封装成流
  Future<ByteData> toByteData({ImageByteFormat format = ImageByteFormat.rawRgba}) {
    return _futurize((_Callback<ByteData> callback) {
      return _toByteData(format.index, (Uint8List encoded) {
        callback(encoded?.buffer?.asByteData());
      });
    });
  }

绘制有了,就差网络请求了,只要能请求到数据流,然后封装成Image对象,就可以绘制图片了,让我们继续看它是如何从网络获取数据的。

2.网络加载

image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale)),

上面提到这个方法 NetworkImage(),图片的获取就是从这个方法开始的,看看这个方法到底怎么实现的。
1.可以看到这个NetworkImage是个抽象类,继承了ImageProvider

abstract class NetworkImage extends ImageProvider<NetworkImage> {
  /// Creates an object that fetches the image at the given URL.
  ///
  /// The arguments [url] and [scale] must not be null.
  const factory NetworkImage(String url, { double scale, Map<String, String> headers }) = network_image.NetworkImage;
  //...省略部分代码
}

2.所有的图片类都继承了这个ImageProvider,这个类至关重要,弄明白这个类就能懂个90%了
ImageProvider是个抽象类,定义了图片数据获取和加载的相关接口。它的主要职责有两个:
(1) 提供图片数据源
(2) 缓存图片

abstract class ImageProvider<T> {

  ImageStream resolve(ImageConfiguration configuration) {
    // 实现代码省略
  }
  Future<bool> evict({ ImageCache cache,
                      ImageConfiguration configuration = ImageConfiguration.empty }) async {
    // 实现代码省略
  }

  //缓存key
  Future<T> obtainKey(ImageConfiguration configuration); 
  @protected
  ImageStreamCompleter load(T key); // 需子类实现,加载load
}

1.load(T key)方法
加载图片数据源的接口,不同的数据源的加载方法不同,每个ImageProvider的子类必须实现它。比如NetworkImage类和AssetImage类,它们都是ImageProvider的子类,但它们需要从不同的数据源来加载图片数据:NetworkImage是从网络来加载图片数据,而AssetImage则是从最终的应用包里来加载(加载打到应用安装包里的资源图片)。 我们以NetworkImage为例,看看其load方法的实现:

// _network_image_io.dart
 @override
  ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
    // Ownership of this controller is handed off to [_loadAsync]; it is that
    // method's responsibility to close the controller's stream when the image
    // has been loaded or an error is thrown.
    final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();

    return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key as NetworkImage, chunkEvents, decode),
      chunkEvents: chunkEvents.stream,
      scale: key.scale,
      informationCollector: () {
        return <DiagnosticsNode>[
          DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
          DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
        ];
      },
    );
  }

我们看到,load方法的返回值类型是ImageStreamCompleter ,它是一个抽象类,定义了管理图片加载过程的一些接口,Image Widget中正是通过它来监听图片加载状态的。

MultiFrameImageStreamCompleterImageStreamCompleter的一个子类,是flutter sdk预置的类,通过该类,我们以方便、轻松地创建出一个ImageStreamCompleter实例来做为load方法的返回值。

我们可以看到,MultiFrameImageStreamCompleter 需要一个codec参数,该参数类型为Future<ui.Codec>Codec是处理图片编解码的类的一个handler,实际上,它只是一个flutter engine API的包装类,也就是说图片的编解码逻辑不是在Dart 代码部分实现,而是在flutter engine中实现的。Codec类部分定义如下:

class Codec extends NativeFieldWrapperClass2 {
  // 此类由flutter engine创建,不应该手动实例化此类或直接继承此类。
  // This class is created by the engine, and should not be instantiated
  // or extended directly.
  //
  //注意看这个里,_()需要看instantiateImageCodec()方法,等会会用到
  // To obtain an instance of the [Codec] interface, see
  // [instantiateImageCodec].
  @pragma('vm:entry-point')
  Codec._();
 /// 图片中的帧数(动态图会有多帧)
  int get frameCount native 'Codec_frameCount';
 /// 动画重复的次数
 /// * 0 表示只执行一次
 /// * -1 表示循环执行
  int get repetitionCount native 'Codec_repetitionCount';

 /// 获取下一个动画帧
  Future<FrameInfo> getNextFrame() {
    return _futurize(_getNextFrame);
  }

  /// Returns an error message on failure, null on success.
  String _getNextFrame(_Callback<FrameInfo> callback) native 'Codec_getNextFrame';
}

我们可以看到Codec最终的结果是一个或多个(动图)帧,而这些帧最终会绘制到屏幕上。

MultiFrameImageStreamCompletercodec参数值为_loadAsync方法的返回值,我们继续看_loadAsync方法的实现:

  Future<ui.Codec> _loadAsync(
    NetworkImage key,
    StreamController<ImageChunkEvent> chunkEvents,
    image_provider.DecoderCallback decode,
  ) 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) {
        PaintingBinding.instance.imageCache.evict(key);
        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();
    }
  }

可以看到_loadAsync方法主要做了两件事:
1.下载图片
2.对下载的图片数据进行解码

下载逻辑比较简单:通过HttpClient从网上下载图片,另外下载请求会设置一些自定义的header,开发者可以通过NetworkImageheaders命名参数来传递。

在图片下载完成后调用了decode(bytes)对数据进行回调;

typedef DecoderCallback = Future<ui.Codec> Function(Uint8List bytes, {int cacheWidth, int cacheHeight});

会封装成一个ui.Codec类型;

  // To obtain an instance of the [Codec] interface, see
  // [instantiateImageCodec].
 Codec._();
//_()需要看instantiateImageCodec();方法

Future<Codec> instantiateImageCodec(Uint8List list, {
  int targetWidth,
  int targetHeight,
}) {
  return _futurize(
    (_Callback<Codec> callback) => _instantiateImageCodec(list, callback, null, targetWidth ?? _kDoNotResizeDimension, targetHeight ?? _kDoNotResizeDimension)
  );
}
//最终会调用一个native方法
String _instantiateImageCodec(Uint8List list, _Callback<Codec> callback, _ImageInfo imageInfo, int targetWidth, int targetHeight)
  native 'instantiateImageCodec';

通过多个回调最终调用Flutter engineinstantiateImageCodec的方法去解析图片。

obtainKey(ImageConfiguration configuration)方法

  @override
  Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
      //this指代NetworkImage对象
    return SynchronousFuture<NetworkImage>(this);
  }

//缓存的key是根据 url 和 scale
 @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    return other is NetworkImage
        && other.url == url  
        && other.scale == scale;
  }

key是根据图片的urlscale生成的
该接口主要是为了配合实现图片缓存,ImageProvider从数据源加载完数据后,会在全局的ImageCache中缓存图片数据,而图片数据缓存是一个Map,而Mapkey便是调用此方法的返回值,不同的key代表不同的图片数据缓存。

resolve(ImageConfiguration configuration)方法
resolve方法是ImageProvider的暴露的给·Image·的主入口方法,它接受一个ImageConfiguration参数,返回ImageStream,即图片数据流。我们重点看一下resolve执行流程:

  @nonVirtual
  ImageStream resolve(ImageConfiguration configuration) {
    assert(configuration != null);
    final ImageStream stream = createStream(configuration);
    _createErrorHandlerAndKey(
      configuration,
      (T key, ImageErrorListener errorHandler) {
        resolveStreamForKey(configuration, stream, key, errorHandler);
      },
      (T key, dynamic exception, StackTrace stack) async {
        await null; // wait an event turn in case a listener has been added to the image stream.
        final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter();
        stream.setCompleter(imageCompleter);
        InformationCollector collector;
       //...省略部分代码
        },
      );
    return stream;
  }

 void _createErrorHandlerAndKey(
    ImageConfiguration configuration,
    _KeyAndErrorHandlerCallback<T> successCallback,///缓存会在这里执行
    _AsyncKeyErrorHandler<T> errorCallback,
  ) {
  /// 创建一个新Zone,主要是为了当发生错误时不会干扰MainZone
    final Zone dangerZone = Zone.current.fork(
      specification: ZoneSpecification(
        handleUncaughtError: (Zone zone, ZoneDelegate delegate, Zone parent, Object error, StackTrace stackTrace) {
          handleError(error, stackTrace);
        }
      )
    );
    dangerZone.runGuarded(() {
 // 先验证是否已经有缓存
      Future<T> key;
      try {
// 生成缓存key,后面会根据此key来检测是否有缓存
        key = obtainKey(configuration);
      } catch (error, stackTrace) {
        handleError(error, stackTrace);
        return;
      }
      key.then<void>((T key) {
        obtainedKey = key;
        try {
//成功回调
          successCallback(key, handleError);
        } catch (error, stackTrace) {
          handleError(error, stackTrace);
        }
      }).catchError(handleError);
    });
  }

ImageConfiguration包含图片和设备的相关信息,如图片的大小、所在的AssetBundle(只有打到安装包的图片存在)以及当前的设备平台、devicePixelRatio(设备像素比等)。Flutter SDK提供了一个便捷函数createLocalImageConfiguration来创建ImageConfiguration 对象:
通过ResizeImage名字可以联想到这是调整图片大小,看官方给出的解释

class ImageConfiguration {

  const ImageConfiguration({
    this.bundle,
    this.devicePixelRatio,
    this.locale,
    this.textDirection,
    this.size,
    this.platform,
  });
}

上面提到的缓存现在来看看:

 @protected
  void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {

    if (stream.completer != null) {
      final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
        key,
        () => stream.completer,
        onError: handleError,
      );
      assert(identical(completer, stream.completer));
      return;
    }
    final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
      key,
      () => load(key, PaintingBinding.instance.instantiateImageCodec),
      onError: handleError,
    );
    if (completer != null) {
      stream.setCompleter(completer);
    }
  }

PaintingBinding.instance.imageCache 是 ImageCache的一个实例,它是PaintingBinding的一个属性,而Flutter框架中的PaintingBinding.instance是一个单例,imageCache事实上也是一个单例,也就是说图片缓存是全局的,统一由PaintingBinding.instance.imageCache 来管理。
篇幅太长还没写完......

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339