Flutter图片缓存 | Image.network源码分析

作 者 简 介

郭海生

Android高级工程师,6年以上开发经验,有丰富的代码重构和架构设计经验,负责京东商城我的京东的开发工作,热衷于学习和研究新技术。

随着手机设备硬件水平的飞速发展,用户对于图片的显示要求也越来越高,稍微处理不好就会容易造成内存溢出等问题。所以我们在使用Image的时候,建立一个图片缓存机制已经是一个常态。Android目前提供了很丰富的图片框架,像ImageLoader、Glide、Fresco等。对于Flutter而言,为了探其缓存机制或者定制自己的缓存框架,特从其Image入手进行突破。

>>>>

Image 的用法

Image是Flutter里提供的显示图片的控件,类似Android里ImageView,不过其用法有点类似Glide等图片框架。

我们先看Image的用法。Flutter对Image控件提供了多种构造函数:

new Image         用于从ImageProvider获取图像

  new Image.asset   用于使用key从AssetBundle获取图像

  new Image.network 用于从URL地址获取图像

  new Image.file    用于从File获取图像

我们只分析Image.network源码,分析理解完这个之后,其他的也是一样的思路。

我们先从Image.network的用法入手:显示一个网络图片很简单,直接通过Image.network携带一个url参数即可。

范例:

return new Scaffold(
  appBar: new AppBar(
    title: new Text("Image from Network"),
  ),
  body: new Container(
      child: new Column(
        children: <Widget>[
          // Load image from network
          new Image.network(
              'https://flutter.io/images/flutter-mark-square-100.png'),
        ],
      )),
);

>>>>

Image结构UML类图

我们首先看一下Image的UML类图:

可以看到Image的框架结构还是有点儿复杂的,在你只调用一行代码的情况下,其实Flutter为你做了很多工作。

初步梳理下每个类概念

  1. StatefulWidget就是有状态的Widget,是展示在页面上的元素。
  2. Image继承于StatefulWidget,是来显示和加载图片。
  3. State控制着StatefulWidget状态改变的生命周期,当Widget被创建、Widget配置信息改变或者Widget被销毁等等,State的一系列方法会被调用。
  4. _ImageState继承于State,处理State生命周期变化以及生成Widget。
  5. ImageProvider提供加载图片的入口,不同的图片资源加载方式不一样,只要重写其load方法即可。同样,缓存图片的key值也有其生成。
  6. NetWorkImage负责下载网络图片的,将下载完成的图片转化成ui.Codec对象交给ImageStreamCompleter去处理解析。
  7. ImageStreamCompleter就是逐帧解析图片的。
  8. ImageStream是处理Image Resource的,ImageState通过ImageStream与ImageStreamCompleter建立联系。ImageStream里也存储着图片加载完毕的监听回调。
  9. MultiFrameImageStreamCompleter就是多帧图片解析器。

先把Image的框架结构了解一下,有助于下面我们更加清晰地分析代码。

>>>>

源码分析

我们看下Image.network都做了什么:

class Image extends StatefulWidget {
    Image.network(String src, {
    Key key,
    double scale = 1.0,
    this.width,
    this.height,
    this.color,
    this.colorBlendMode,
    this.fit,
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.centerSlice,
    this.matchTextDirection = false,
    this.gaplessPlayback = false,
    Map<String, String> headers,
      }) : image = new NetworkImage(src, scale: scale, headers: headers),
       assert(alignment != null),
       assert(repeat != null),
       assert(matchTextDirection != null),
       super(key: key);
   ......

我们看到Image是一个StatefulWidget对象,可以直接放到Container或者Column等容器里,其属性解释如下:

  • width:widget的宽度
  • height:widget的高度
  • color:与colorBlendMode配合使用,将此颜色用BlendMode方式混合图片
  • colorBlendMode:混合模式算法
  • fit:与android:scaletype一样,控制图片如何resized/moved来匹对Widget的size
  • alignment:widget对齐方式
  • repeat:如何绘制未被图像覆盖的部分
  • centerSlice:支持9patch,拉伸的中间的区域
  • matchTextDirection:绘制图片的方向:是否从左到右
  • gaplessPlayback:图片变化的时候是否展示老图片或者什么都不展示
  • headers:http请求头
  • image:一个ImageProvide对象,在调用的时候已经实例化,这个类主要承担了从网络加载图片的功能。它是加载图片的最重要的方法,不同的图片加载方式(assert文件加载、网络加载等等)也就是重写ImageProvider加载图片的方法(load())。

Image是一个StatefulWidget对象,所以我们看它的State对象:

class _ImageState extends State<Image> {
  ImageStream _imageStream;
  ImageInfo _imageInfo;
  bool _isListeningToStream = false;
}

class ImageStream extends Diagnosticable {
  ImageStreamCompleter get completer => _completer;
  ImageStreamCompleter _completer;

  List<ImageListener> _listeners;

  /// Assigns a particular [ImageStreamCompleter] to this [ImageStream].
   void setCompleter(ImageStreamCompleter value) {
    assert(_completer == null);
    _completer = value;
    print("setCompleter:::"+(_listeners==null).toString());
    if (_listeners != null) {
      final List<ImageListener> initialListeners = _listeners;
      _listeners = null;
      initialListeners.forEach(_completer.addListener);
    }
  }

  /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
  void addListener(ImageListener listener) {
    if (_completer != null)
      return _completer.addListener(listener);
    _listeners ??= <ImageListener>[];
    _listeners.add(listener);
  }

  /// Stop listening for new concrete [ImageInfo] objects.
  void removeListener(ImageListener listener) {
    if (_completer != null)
      return _completer.removeListener(listener);
    assert(_listeners != null);
    _listeners.remove(listener);
  }
 }

我们对_ImageState的两个属性对象解释一下:

  • ImageStream是处理Image Resource的,ImageStream里存储着图片加载完毕的监听回调,ImageStreamCompleter也是其成员,这样ImageStream将图片的解析流程交给了ImageStreamCompleter去处理。
  • ImageInfo包含了Image的数据源信息:width和height以及ui.Image。 将ImageInfo里的ui.Image设置给RawImage就可以展示了。RawImage就是我们真正渲染的对象,是显示ui.Image的一个控件,接下来我们会看到。

我们知道State的生命周期,首先State的initState执行,然后didChangeDependencies会执行,我们看到ImageState里没有重写父类的initState,那我们看其didChangeDependencies():

@override
void didChangeDependencies() {
    _resolveImage();

    if (TickerMode.of(context))
      _listenToStream();
    else
      _stopListeningToStream();

    super.didChangeDependencies();
}

>>>>

_resolveImage方法解析

我们看到首先调用了resolveImage(),我们看下resolveImage方法:

void _resolveImage() {
    final ImageStream newStream =
      widget.image.resolve(createLocalImageConfiguration(
          context,
          size: widget.width != null && widget.height != null ? new Size(widget.width, widget.height) : null
      ));
    assert(newStream != null);
    _updateSourceStream(newStream);
  }

这个方法是处理图片的入口。widget.image这个就是上面的创建的NetworkImage对象,是个ImageProvider对象,调用它的resolve并且传进去默认的ImageConfiguration。 我们看下resolve方法,发现NetworkImage没有,果不其然,我们在其父类ImageProvider找到了:

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;
  }

我们看到这个方法创建了ImageStream并返回,调用obtainKey返回一个携带NetworkImage的future,以后会作为缓存的key使用,并且调用ImageStream的setCompleter的方法:

void setCompleter(ImageStreamCompleter value) {
    assert(_completer == null);
    _completer = value;
    if (_listeners != null) {
      final List<ImageListener> initialListeners = _listeners;
      _listeners = null;
      initialListeners.forEach(_completer.addListener);
    }
  }

这个方法就是给ImageStream设置一个ImageStreamCompleter对象,每一个ImageStream对象只能设置一次,ImageStreamCompleter是为了辅助ImageStream解析和管理Image图片帧的,并且判断是否有初始化监听器,可以做一些初始化回调工作。 我们继续看下PaintingBinding.instance.imageCache.putIfAbsent方法:

ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader()) {
    assert(key != null);
    assert(loader != null);
    ImageStreamCompleter result = _pendingImages[key];
    // Nothing needs to be done because the image hasn't loaded yet.
    if (result != null)
      return result;
    // Remove the provider from the list so that we can move it to the
    // recently used position below.
    final _CachedImage image = _cache.remove(key);
    if (image != null) {
      _cache[key] = image;
      return image.completer;
    }
    result = loader();
    void listener(ImageInfo info, bool syncCall) {
      // Images that fail to load don't contribute to cache size.
      final int imageSize = info.image == null ? 0 : info.image.height * info.image.width * 4;
      final _CachedImage image = new _CachedImage(result, imageSize);
      _currentSizeBytes += imageSize;
      _pendingImages.remove(key);
      _cache[key] = image;
      result.removeListener(listener);
      _checkCacheSize();
    }
    if (maximumSize > 0 && maximumSizeBytes > 0) {
      _pendingImages[key] = result;
      result.addListener(listener);
    }
    return result;
  }

这个是Flutter默认提供的内存缓存api的入口方法,这个方法会先通过key获取之前的ImageStreamCompleter对象,这个key就是NetworkImage对象,当然我们也可以重写obtainKey方法自定义key,如果存在则直接返回,如果不存在则执行load方法加载ImageStreamCompleter对象,并将其放到首位(最少最近使用算法)。

也就是说ImageProvider已经实现了内存缓存:默认缓存图片的最大个数是1000,默认缓存图片的最大空间是10MiB。 第一次加载图片肯定是没有缓存的,所以我们看下loader方法,我们看到ImageProvider是空方法,我们去看NetWorkImage,按照我们的预期确实在这里:

@override
  ImageStreamCompleter load(NetworkImage key) {
    return new MultiFrameImageStreamCompleter(
      codec: _loadAsync(key),
      scale: key.scale,
      informationCollector: (StringBuffer information) {
        information.writeln('Image provider: $this');
        information.write('Image key: $key');
      }
    );
  }
  //网络请求加载图片的方法
  Future<ui.Codec> _loadAsync(NetworkImage key) async {
    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 new Exception('HTTP request failed, statusCode: ${response?.statusCode}, $resolved');

    final Uint8List bytes = await consolidateHttpClientResponseBytes(response);
    if (bytes.lengthInBytes == 0)
      throw new Exception('NetworkImage is an empty file: $resolved');

    return await ui.instantiateImageCodec(bytes);
  }

这个方法为我们创建了一个MultiFrameImageStreamCompleter对象,根据名字我们也能知道它继承于ImageStreamCompleter。还记得ImageStreamCompleter是做什么的吗,就是辅助ImageStream管理解析Image的。

参数解析

  • _loadAsync()是请求网络加载图片的方法
  • scale是缩放系数
  • informationCollector是信息收集对象的,提供错误或者其他日志用

MultiFrameImageStreamCompleter是多帧的图片处理加载器,我们知道Flutter的Image支持加载gif,通过MultiFrameImageStreamCompleter可以对gif文件进行解析:

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,
      ));
    });
  }

  ui.Codec _codec;
  final double _scale;
  final InformationCollector _informationCollector;
  ui.FrameInfo _nextFrame;

我们看到MultiFrameImageStreamCompleter拿到loadAsync返回的codec数据对象,通过handleCodecReady来处理数据,然后会调用_decodeNextFrameAndSchedule方法:

Future<Null> _decodeNextFrameAndSchedule() async {
    try {
      _nextFrame = await _codec.getNextFrame();
    } catch (exception, stack) {
      FlutterError.reportError(new FlutterErrorDetails(
          exception: exception,
          stack: stack,
          library: 'services',
          context: 'resolving an image frame',
          informationCollector: _informationCollector,
          silent: true,
      ));
      return;
    }
    if (_codec.frameCount == 1) {
      // This is not an animated image, just return it and don't schedule more
      // frames.
      _emitFrame(new ImageInfo(image: _nextFrame.image, scale: _scale));
      return;
    }
    SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
  }

通过codec.getNextFrame()去拿下一帧,对于静态的图片frameCount是1,直接用ImageInfo组装image,交给emitFrame方法,这个方法里会调用setImage,如下:

@protected
  void setImage(ImageInfo image) {
    _current = image;
    if (_listeners.isEmpty)
      return;
    final List<ImageListener> localListeners = new List<ImageListener>.from(_listeners);
    for (ImageListener listener in localListeners) {
      try {
        listener(image, false);
      } catch (exception, stack) {
        _handleImageError('by an image listener', exception, stack);
      }
    }
  }

setImage方法就是设置当前的ImageInfo并检查监听器列表,通知监听器图片已经加载完毕可以刷新UI了。

对于动图来说就是就是交给SchedulerBinding逐帧的去调用setImage,通知UI刷新,代码就不贴了,有兴趣的可以自行查看下。 至此resolveImage调用流程我们算是讲完了,接下来我们看listenToStream。

>>>>

_listenToStream方法解析

我们继续分析didChangeDependencies方法,这个方法里会判断TickerMode.of(context)的值,这个值默认是true,和AnimationConrol有关,后续可以深入研究。然后调用_listenToStream()。 我们看下这个方法:

void _listenToStream() {
    if (_isListeningToStream)
      return;
    _imageStream.addListener(_handleImageChanged);
    _isListeningToStream = true;
  }

这个就是添加图片加载完毕的回调器。还记得吗,当图片加载并解析完毕的时候,MultiFrameImageStreamCompleter的setImage方法会调用这里传过去的回调方法。我们看下这里回调方法里做了什么:

void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {
    setState(() {
      _imageInfo = imageInfo;
    });
  }

很显然就是拿到上层传过来ImageInfo,调用setState更新UI 我们看下build方法:

Widget build(BuildContext context) {
    return new RawImage(
      image: _imageInfo?.image,
      width: widget.width,
      height: widget.height,
      scale: _imageInfo?.scale ?? 1.0,
      color: widget.color,
      colorBlendMode: widget.colorBlendMode,
      fit: widget.fit,
      alignment: widget.alignment,
      repeat: widget.repeat,
      centerSlice: widget.centerSlice,
      matchTextDirection: widget.matchTextDirection,
    );
  }

就是用imageInfo和widget的信息来封装RawImage,RawImage是RenderObjectWidget对象,是应用程序真正渲染的对象,将咱们的图片显示到界面上。

>>>>

总结

梳理下流程:

  1. 从入口开始,Image是继承于StatefulWidget,它为咱们实现好了State:_ImageState,并且提供了一个已经实例化的NetWorkImage对象,它是继承于ImageProvider对象的。
  2. ImageState创建完之后,ImageState通过调用resolveImage(),resolveImage()又会调用ImageProvider的resolve()方法返回一个ImageStream对象。_ImageState也注册了监听器给ImageStream,当图片下载完毕后会执行回调方法。
  3. 然后在ImageProvider的resolve()方法里不仅创建了ImageStream还设置了ImageStream的setComplete方法去设置ImageStreamCompleter,在这里去判断是否有缓存,没有缓存就调用load方法去创建ImageStreamCompleter并且添加监听器为了执行加载完图片之后的缓存工作。ImageStreamCompleter是为了解析已经加载完成的Image的。
  4. NetWorkImage实现了ImageProvider的load方法,是真正下载图片的地方,创建了MultiFrameImageStreamCompleter对象,并且调用_loadAsync去下载图片。当图片下载完成后就调用UI的回调方法,通知UI刷新。

>>>>

最后

至此,对Image.network的源码分析到这里也结束了,你也可以返回去看下Image的结构图了。怎么样,分析完之后是不是对Flutter加载网络图片的流程已经很了解了,也找到了Flutter缓存的突破口,Flutter自身已经提供了内存缓存(虽然不太完美),接下来你就可以添加你的硬盘缓存或者定制你的图片框架了。

---------------------END---------------------

原文发布于微信公众号 - 京东技术(jingdongjishu)

原文发表时间:2018-07-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏SeanCheney的专栏

Python工程的文档结构

Python工程的文档结构,可以参考https://stackoverflow.com/questions/193161/what-is-the-best-pr...

2332
来自专栏求索之路

从零开始撸一个Fresco之gif和Webp动画

上一篇文章的链接:从零开始撸一个Fresco之硬盘缓存 转载请注明出处 Fresco源代码文档翻译项目请看这里:Fresco源代码翻译项目 这个项目会不断...

3639
来自专栏量化投资与机器学习

【精心解读】关于Jupyter Notebook的28个技巧

Jupyter具有很强的可扩展性,支持许多编程语言,可以很容易地托管在计算机上或几乎所有的服务器上,只需要拥有ssh或http访问权限。 最重要的是,它是完全免...

1.7K7
来自专栏令仔很忙

学生信息管理系统问题集锦

 1)、数据库的ODBC配置出现错误,没有配置好,与数据库的连接没有连接好,就会出现这样的问题

952
来自专栏有趣的Python

Scrapy分布式爬虫打造搜索引擎 -(二)伯乐在线爬取所有文章Python分布式爬虫打造搜索引擎

Python分布式爬虫打造搜索引擎 基于Scrapy、Redis、elasticsearch和django打造一个完整的搜索引擎网站 二、伯乐在线爬取所有文章 ...

4825
来自专栏Android群英传

Clipboard还能玩出花

1202
来自专栏学习力

《Java从入门到放弃》JavaSE入门篇:单元测试

1836
来自专栏红色石头的机器学习之路

Jupyter notebook入门教程(上)

本文将分上下两部分简单介绍Jupyter notebook的入门教程,英文原文出处: Getting started with the Jupyter note...

3930
来自专栏高性能服务器开发

libevent源码深度剖析十 支持I/O多路复用技术

(1)libevent源码深度剖析一 序 (2)libevent源码深度剖析二 Reactor模式 (3)libevent源码深度剖析三 libevent基本使...

1171
来自专栏逆向技术

16位汇编第三讲 分段存储管理思想

      内存分段 一丶分段(汇编指令分段) 1.为什么分段?   因为分段是为了更好的管理数据和代码,就好比C语言为什么会有内存4区一样,否则汇编代码都写...

2026

扫码关注云+社区

领取腾讯云代金券