京东技术:Flutter图片缓存 | Image.network源码分析

轻松写代码 2018-07-20 14:07:43 ⋅ 793 阅读

来这里找志同道合的小伙伴!


作  者  简  介

郭海生

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


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


>>>>

Image 的用法


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


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

 
  1.  new Image         用于从ImageProvider获取图像

  2.  new Image.asset  用于使用keyAssetBundle获取图像

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

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


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


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


范例:

 
  1. return new Scaffold(

  2.  appBar: new AppBar(

  3.    title: new Text("Image from Network"),

  4.  ),

  5.  body: new Container(

  6.      child: new Column(

  7.        children: <Widget>[

  8.          // Load image from network

  9.          new Image.network(

  10.              'https://flutter.io/images/flutter-mark-square-100.png'),

  11.        ],

  12.      )),

  13. );


>>>>

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都做了什么:

 
  1. class Image extends StatefulWidget {

  2.    Image.network(String src, {

  3.    Key key,

  4.    double scale = 1.0,

  5.    this.width,

  6.    this.height,

  7.    this.color,

  8.    this.colorBlendMode,

  9.    this.fit,

  10.    this.alignment = Alignment.center,

  11.    this.repeat = ImageRepeat.noRepeat,

  12.    this.centerSlice,

  13.    this.matchTextDirection = false,

  14.    this.gaplessPlayback = false,

  15.    Map<String, String> headers,

  16.      }) : image = new NetworkImage(src, scale: scale, headers: headers),

  17.       assert(alignment != null),

  18.       assert(repeat != null),

  19.       assert(matchTextDirection != null),

  20.       super(key: key);

  21.   ......


我们看到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对象:

 
  1. class _ImageState extends State<Image> {

  2.  ImageStream _imageStream;

  3.  ImageInfo _imageInfo;

  4.  bool _isListeningToStream = false;

  5. }

  6. class ImageStream extends Diagnosticable {

  7.  ImageStreamCompleter get completer => _completer;

  8.  ImageStreamCompleter _completer;

  9.  List<ImageListener> _listeners;

  10.  /// Assigns a particular [ImageStreamCompleter] to this [ImageStream].

  11.   void setCompleter(ImageStreamCompleter value) {

  12.    assert(_completer == null);

  13.    _completer = value;

  14.    print("setCompleter:::"+(_listeners==null).toString());

  15.    if (_listeners != null) {

  16.      final List<ImageListener> initialListeners = _listeners;

  17.      _listeners = null;

  18.      initialListeners.forEach(_completer.addListener);

  19.    }

  20.  }

  21.  /// Adds a listener callback that is called whenever a new concrete [ImageInfo]

  22.  void addListener(ImageListener listener) {

  23.    if (_completer != null)

  24.      return _completer.addListener(listener);

  25.    _listeners ??= <ImageListener>[];

  26.    _listeners.add(listener);

  27.  }

  28.  /// Stop listening for new concrete [ImageInfo] objects.

  29.  void removeListener(ImageListener listener) {

  30.    if (_completer != null)

  31.      return _completer.removeListener(listener);

  32.    assert(_listeners != null);

  33.    _listeners.remove(listener);

  34.  }

  35. }


我们对_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():

 
  1. @override

  2. void didChangeDependencies() {

  3.    _resolveImage();

  4.    if (TickerMode.of(context))

  5.      _listenToStream();

  6.    else

  7.      _stopListeningToStream();

  8.    super.didChangeDependencies();

  9. }


>>>>

_resolveImage方法解析


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

 
  1. void _resolveImage() {

  2.    final ImageStream newStream =

  3.      widget.image.resolve(createLocalImageConfiguration(

  4.          context,

  5.          size: widget.width != null && widget.height != null ? new Size(widget.width, widget.height) : null

  6.      ));

  7.    assert(newStream != null);

  8.    _updateSourceStream(newStream);

  9.  }


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

 
  1. ImageStream resolve(ImageConfiguration configuration) {

  2.    assert(configuration != null);

  3.    final ImageStream stream = new ImageStream();

  4.    T obtainedKey;

  5.    obtainKey(configuration).then<void>((T key) {

  6.      obtainedKey = key;

  7.      stream.setCompleter(PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key)));

  8.    }).catchError(

  9.      (dynamic exception, StackTrace stack) async {

  10.        FlutterError.reportError(new FlutterErrorDetails(

  11.          exception: exception,

  12.          stack: stack,

  13.          library: 'services library',

  14.          context: 'while resolving an image',

  15.          silent: true, // could be a network error or whatnot

  16.          informationCollector: (StringBuffer information) {

  17.            information.writeln('Image provider: $this');

  18.            information.writeln('Image configuration: $configuration');

  19.            if (obtainedKey != null)

  20.              information.writeln('Image key: $obtainedKey');

  21.          }

  22.        ));

  23.        return null;

  24.      }

  25.    );

  26.    return stream;

  27.  }    


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

 
  1. void setCompleter(ImageStreamCompleter value) {

  2.    assert(_completer == null);

  3.    _completer = value;

  4.    if (_listeners != null) {

  5.      final List<ImageListener> initialListeners = _listeners;

  6.      _listeners = null;

  7.      initialListeners.forEach(_completer.addListener);

  8.    }

  9.  }


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

 
  1. ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader()) {

  2.    assert(key != null);

  3.    assert(loader != null);

  4.    ImageStreamCompleter result = _pendingImages[key];

  5.    // Nothing needs to be done because the image hasn't loaded yet.

  6.    if (result != null)

  7.      return result;

  8.    // Remove the provider from the list so that we can move it to the

  9.    // recently used position below.

  10.    final _CachedImage image = _cache.remove(key);

  11.    if (image != null) {

  12.      _cache[key] = image;

  13.      return image.completer;

  14.    }

  15.    result = loader();

  16.    void listener(ImageInfo info, bool syncCall) {

  17.      // Images that fail to load don't contribute to cache size.

  18.      final int imageSize = info.image == null ? 0 : info.image.height * info.image.width * 4;

  19.      final _CachedImage image = new _CachedImage(result, imageSize);

  20.      _currentSizeBytes += imageSize;

  21.      _pendingImages.remove(key);

  22.      _cache[key] = image;

  23.      result.removeListener(listener);

  24.      _checkCacheSize();

  25.    }

  26.    if (maximumSize > 0 && maximumSizeBytes > 0) {

  27.      _pendingImages[key] = result;

  28.      result.addListener(listener);

  29.    }

  30.    return result;

  31.  }


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


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

 
  1. @override

  2.  ImageStreamCompleter load(NetworkImage key) {

  3.    return new MultiFrameImageStreamCompleter(

  4.      codec: _loadAsync(key),

  5.      scale: key.scale,

  6.      informationCollector: (StringBuffer information) {

  7.        information.writeln('Image provider: $this');

  8.        information.write('Image key: $key');

  9.      }

  10.    );

  11.  }

  12.  //网络请求加载图片的方法

  13.  Future<ui.Codec> _loadAsync(NetworkImage key) async {

  14.    assert(key == this);

  15.    final Uri resolved = Uri.base.resolve(key.url);

  16.    final HttpClientRequest request = await _httpClient.getUrl(resolved);

  17.    headers?.forEach((String name, String value) {

  18.      request.headers.add(name, value);

  19.    });

  20.    final HttpClientResponse response = await request.close();

  21.    if (response.statusCode != HttpStatus.ok)

  22.      throw new Exception('HTTP request failed, statusCode: ${response?.statusCode}, $resolved');

  23.    final Uint8List bytes = await consolidateHttpClientResponseBytes(response);

  24.    if (bytes.lengthInBytes == 0)

  25.      throw new Exception('NetworkImage is an empty file: $resolved');

  26.    return await ui.instantiateImageCodec(bytes);

  27.  }


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


参数解析


  • _loadAsync()是请求网络加载图片的方法

  • scale是缩放系数

  • informationCollector是信息收集对象的,提供错误或者其他日志用


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

 
  1. MultiFrameImageStreamCompleter({

  2.    @required Future<ui.Codec> codec,

  3.    @required double scale,

  4.    InformationCollector informationCollector

  5.  }) : assert(codec != null),

  6.       _informationCollector = informationCollector,

  7.       _scale = scale,

  8.       _framesEmitted = 0,

  9.       _timer = null {

  10.    codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {

  11.      FlutterError.reportError(new FlutterErrorDetails(

  12.        exception: error,

  13.        stack: stack,

  14.        library: 'services',

  15.        context: 'resolving an image codec',

  16.        informationCollector: informationCollector,

  17.        silent: true,

  18.      ));

  19.    });

  20.  }

  21.  ui.Codec _codec;

  22.  final double _scale;

  23.  final InformationCollector _informationCollector;

  24.  ui.FrameInfo _nextFrame;


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

 
  1. Future<Null> _decodeNextFrameAndSchedule() async {

  2.    try {

  3.      _nextFrame = await _codec.getNextFrame();

  4.    } catch (exception, stack) {

  5.      FlutterError.reportError(new FlutterErrorDetails(

  6.          exception: exception,

  7.          stack: stack,

  8.          library: 'services',

  9.          context: 'resolving an image frame',

  10.          informationCollector: _informationCollector,

  11.          silent: true,

  12.      ));

  13.      return;

  14.    }

  15.    if (_codec.frameCount == 1) {

  16.      // This is not an animated image, just return it and don't schedule more

  17.      // frames.

  18.      _emitFrame(new ImageInfo(image: _nextFrame.image, scale: _scale));

  19.      return;

  20.    }

  21.    SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);

  22.  }


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

 
  1. @protected

  2.  void setImage(ImageInfo image) {

  3.    _current = image;

  4.    if (_listeners.isEmpty)

  5.      return;

  6.    final List<ImageListener> localListeners = new List<ImageListener>.from(_listeners);

  7.    for (ImageListener listener in localListeners) {

  8.      try {

  9.        listener(image, false);

  10.      } catch (exception, stack) {

  11.        _handleImageError('by an image listener', exception, stack);

  12.      }

  13.    }

  14.  }


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


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

>>>>

_listenToStream方法解析


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

 
  1. void _listenToStream() {

  2.    if (_isListeningToStream)

  3.      return;

  4.    _imageStream.addListener(_handleImageChanged);

  5.    _isListeningToStream = true;

  6.  }


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

 
  1. void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {

  2.    setState(() {

  3.      _imageInfo = imageInfo;

  4.    });

  5.  }


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

 
  1. Widget build(BuildContext context) {

  2.    return new RawImage(

  3.      image: _imageInfo?.image,

  4.      width: widget.width,

  5.      height: widget.height,

  6.      scale: _imageInfo?.scale ?? 1.0,

  7.      color: widget.color,

  8.      colorBlendMode: widget.colorBlendMode,

  9.      fit: widget.fit,

  10.      alignment: widget.alignment,

  11.      repeat: widget.repeat,

  12.      centerSlice: widget.centerSlice,

  13.      matchTextDirection: widget.matchTextDirection,

  14.    );

  15.  }


就是用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----------------

后续的内容同样精彩

长按关注“IT实战联盟”哦




全部评论: 0

    我有话说:

    京东到家订单中心系统mysql到es的转化之路

    原文:https://www.toutiao.com/i6796507988602389006 京东到家订单中心系统业务中,无论是外部商家的订单生产,或是内部上下游系统的依赖,订单查询的调用量都非常

    京东技术:用最小的图片格式,打造最优的用户体验

    DPG图片压缩技术能够有效的减少图片大小50%,并且减少50%的CDN带宽流量!

    推荐一款前端数据管理工具 algeb

    方案不是很灵活,无法解决共享数据,数据没回来时怎...

    微商城小程序在哪里下载?

    博主在吗?问一下微商城小程序在哪里下载?

    Flutter 布局详解

    作为最近大火特火的Flutter,已经成为移动开发者必学的技术了。

    GitHub竟然有基于SpringCloud的“网约车”项目,附

    有人问小编有没有开的“网约车”项目,并且最好是采用微服务架构设计,这样可以投入技术团队进行二次开发。 小编在GitHub上还真找到了这个项目,接下来一起看一看吧! 项目介绍 该项目是一款标准且

    京东技术:多数据模型数据库 | 应用实例解析

    作 者 简 介吕信,京东商城技术架构部资深架构师,拥有多年数据产品研发及架构经验。

    精品推荐:JDFlutter | 京东技术中台新一代跨平台开发框架

    DFlutter 是商城共享技术部-多端融合技术部推出的新一代跨平台开发框架,可快速集成至现有 Android/iOS 工程,开发者可借助 JDFlutter 平台快速完成 Flutter 业务开发。

    阿里技术:自底向上构建知识图谱全过程

    知识图谱的构建技术主要有自顶向下和自底向上两种。其中自顶向下构建是指借助百科类网站等结构化数据,从高质量数据中提取本体和模式信息,加入到知识库里。

    VUE 开源库收藏版(一):史上最全面的学习资源 ,附GitHub地址

    VUE 开源库收藏版(一):史上最全面的学习资源 ,附GitHub地址

    京东技术:Hystrix 分布式系统限流、降级、熔断框架

    Hystrix是Netflix开的一款容错框架,包含常用的容错方法:线程隔离、信号量隔离、降级策略、熔断技术

    使用分支进行开发和部署

    原文:Developing and Deploying with Branches 分支能够使开发工作更有条理,可以将正在开发的代码跟那些已完成的代码、已测试的代码和稳定下来的代码隔离开。不但使

    精品推荐:微信平台反编译找回丢失的小程序

    这篇文章是总结了一下公司后台开发的经验,之前他的电脑系统突然就坏掉了,电脑里的小陈需也丢失了,怎么找回呢?

    SpringBoot+zk+dubbo架构实践(四):sb+zk+dubbo框架搭建(内附GitHub地址)

    本篇案例模拟了一个provider服务提供方和PC、Web两个服务消费方内附GitHub......

    京东技术:使用JDReact小程序双向转换

    JDReact是京东商城前台产品研发部推出的多端融合开发框架。

    京东技术:多级缓存设计详解 | 给数据库减负

    传统的cpu通过fsb直连内存的方式显然就会因为内存访问的等待,导致cpu吞吐量下降,内存成为性能瓶颈。

    转载:RocketMQ基础概念剖析&解析

    Topic Topic是一类消息的集合,是一种逻辑上的分区。为什么说是逻辑分区呢?因为最终数据是存储到Broker上的,而且为了满足高可用,采用了分布式的存储。 这和Kafka中的实现如出一辙

    ImageIO Unsupported Image Type

    : Unsupported Image Type at com.sun...