Flutter中网络图片加载和缓存

前言

应用开发中常常会碰到网络图片的加载,一般咱们会对图片进行缓存,以便下次加载同一张图片时不用再从新下载,在包含有大量图片的应用中,会大幅提升图片展示速度、提高用户体验且为用户节省流量。Flutter自己提供的Image Widget已经实现了加载网络图片的功能,且具有内存缓存的机制,接下来一块儿看一下Image的网络图片加载的实现。android

重温小部件Image

经常使用小部件Image中实现了几种构造函数,已经足够咱们平常开发中各类场景下建立Image对象使用了。编程

  • 有参构造函数:缓存

    Image(Key key, @required this.image, ...)bash

    开发者可根据自定义的ImageProvider来建立Image。微信

  • 命名构造函数:网络

    • Image.network(String src, ...)框架

      src便是根据网络获取的图片url地址。异步

    • Image.file(File file, ...)async

      file指本地一个图片文件对象,安卓中须要android.permission.READ_EXTERNAL_STORAGE权限。ide

    • Image.asset(String name, ...)

      name指项目中添加的图片资源名,事先在pubspec.yaml文件中有声明。

    • Image.memory(Uint8List bytes, ...)

      bytes指内存中的图片数据,将其转化为图片对象。

其中Image.network就是咱们本篇分享的重点 -- 加载网络图片。

Image.network源码分析

下面经过源码咱们来看下Image.network加载网络图片的具体实现。

Image.network(String src, {
    Key key,
    double scale = 1.0,
    .
    .
  }) : image = NetworkImage(src, scale: scale, headers: headers),
       assert(alignment != null),
       assert(repeat != null),
       assert(matchTextDirection != null),
       super(key: key);

  /// The image to display.
  final ImageProvider image;
复制代码

首先,使用Image.network命名构造函数建立Image对象时,会同时初始化实例变量image,image是一个ImageProvider对象,该ImageProvider就是咱们所须要的图片的提供者,它自己是一个抽象类,子类包括NetworkImageFileImageExactAssetImageAssetImageMemoryImage等,网络加载图片使用的就是NetworkImage

Image做为一个StatefulWidget其状态由_ImageState控制,_ImageState继承自State类,其生命周期方法包括initState()didChangeDependencies()build()deactivate()dispose()didUpdateWidget()等。咱们重点来_ImageState中函数的执行。

因为插入渲染树时会先调用initState()函数,而后调用didChangeDependencies()函数,_ImageState中并无重写initState()函数,因此didChangeDependencies()函数会执行,看下didChangeDependencies()里的内容

@override
  void didChangeDependencies() {
    _invertColors = MediaQuery.of(context, nullOk: true)?.invertColors
      ?? SemanticsBinding.instance.accessibilityFeatures.invertColors;
    _resolveImage();

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

    super.didChangeDependencies();
  }
复制代码

_resolveImage()会被调用,函数内容以下

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);
  }
复制代码

函数中先建立了一个ImageStream对象,该对象是一个图片资源的句柄,其持有着图片资源加载完毕后的监听回调和图片资源的管理者。而其中的ImageStreamCompleter对象就是图片资源的一个管理类,也就是说,_ImageState经过ImageStreamImageStreamCompleter管理类创建了联系。

再回头看一下ImageStream对象是经过widget.image.resolve方法建立的,也就是对应NetworkImageresolve方法,咱们查看NetworkImage类的源码发现并无resolve方法,因而查找其父类,在ImageProvider类中找到了。

ImageStream resolve(ImageConfiguration configuration) {
    assert(configuration != null);
    final ImageStream stream = ImageStream();
    T obtainedKey;
    Future<void> handleError(dynamic exception, StackTrace stack) async {
      .
      .
    }
    obtainKey(configuration).then<void>((T key) {
      obtainedKey = key;
      final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key), onError: handleError);
      if (completer != null) {
        stream.setCompleter(completer);
      }
    }).catchError(handleError);
    return stream;
  }
复制代码

ImageStream中的图片管理者ImageStreamCompleter经过PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key), onError: handleError);方法建立,imageCache是Flutter框架中实现的用于图片缓存的单例,查看其中的putIfAbsent方法

ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
    assert(key != null);
    assert(loader != null);
    ImageStreamCompleter result = _pendingImages[key]?.completer;
    // 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; } try { result = loader(); } catch (error, stackTrace) { if (onError != null) { onError(error, stackTrace); return null; } else { rethrow; } } 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 = _CachedImage(result, imageSize);
      // If the image is bigger than the maximum cache size, and the cache size
      // is not zero, then increase the cache size to the size of the image plus
      // some change.
      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();
    }
    if (maximumSize > 0 && maximumSizeBytes > 0) {
      _pendingImages[key] = _PendingImage(result, listener);
      result.addListener(listener);
    }
    return result;
  }
复制代码

经过以上代码能够看到会经过key来查找缓存中是否存在,若是存在则返回,若是不存在则会经过执行loader()方法建立图片资源管理者,然后再将缓存图片资源的监听方法注册到新建的图片管理者中以便图片加载完毕后作缓存处理。

根据上面的代码调用PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key), onError: handleError);看出load()方法由ImageProvider对象实现,这里就是NetworkImage对象,看下其具体实现代码

@override
  ImageStreamCompleter load(NetworkImage key) {
    return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key),
      scale: key.scale,
      informationCollector: (StringBuffer information) {
        information.writeln('Image provider: $this');
        information.write('Image key: $key');
      }
    );
  }
复制代码

代码中其就是建立一个MultiFrameImageStreamCompleter对象并返回,这是一个多帧图片管理器,代表Flutter是支持GIF图片的。建立对象时的codec变量由_loadAsync方法的返回值初始化,查看该方法内容

static final HttpClient _httpClient = HttpClient();

  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 Exception('HTTP request failed, statusCode: ${response?.statusCode}, $resolved');

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

    return PaintingBinding.instance.instantiateImageCodec(bytes);
  }
复制代码

这里才是关键,就是经过HttpClient对象对指定的url进行下载操做,下载完成后根据图片二进制数据实例化图像编解码器对象Codec,而后返回。

那么图片下载完成后是如何显示到界面上的呢,下面看下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) {
      reportError(
        context: 'resolving an image codec',
        exception: error,
        stack: stack,
        informationCollector: informationCollector,
        silent: true,
      );
    });
  }
复制代码

看,构造方法中的代码块,codec的异步方法执行完成后会调用_handleCodecReady函数,函数内容以下

void _handleCodecReady(ui.Codec codec) {
    _codec = codec;
    assert(_codec != null);

    _decodeNextFrameAndSchedule();
  }
复制代码

方法中会将codec对象保存起来,而后解码图片帧

Future<void> _decodeNextFrameAndSchedule() async {
    try {
      _nextFrame = await _codec.getNextFrame();
    } catch (exception, stack) {
      reportError(
        context: 'resolving an image frame',
        exception: exception,
        stack: stack,
        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(ImageInfo(image: _nextFrame.image, scale: _scale)); return; } SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame); } 复制代码

若是图片是png或jpg只有一帧,则执行_emitFrame函数,从帧数据中拿到图片帧对象根据缩放比例建立ImageInfo对象,而后设置显示的图片信息

void _emitFrame(ImageInfo imageInfo) {
    setImage(imageInfo);
    _framesEmitted += 1;
  }
  
  /// Calls all the registered listeners to notify them of a new image.
  @protected
  void setImage(ImageInfo image) {
    _currentImage = image;
    if (_listeners.isEmpty)
      return;
    final List<ImageListener> localListeners = _listeners.map<ImageListener>(
      (_ImageListenerPair listenerPair) => listenerPair.listener
    ).toList();
    for (ImageListener listener in localListeners) {
      try {
        listener(image, false);
      } catch (exception, stack) {
        reportError(
          context: 'by an image listener',
          exception: exception,
          stack: stack,
        );
      }
    }
  }
复制代码

这时就会根据添加的监听器来通知一个新的图片须要渲染。那么这个监听器是何时添加的呢,咱们回头看一下_ImageState类中的didChangeDependencies()方法内容,执行完_resolveImage();后会执行_listenToStream();方法

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

该方法就向ImageStream对象中添加了监听器_handleImageChanged,监听方法以下

void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {
    setState(() {
      _imageInfo = imageInfo;
    });
  }
复制代码

最终就是调用setState方法来通知界面刷新,将下载到的图片渲染到界面上来了。

实际问题

从以上源码分析,咱们应该清楚了整个网络图片从加载到显示的过程,不过使用这种原生的方式咱们发现网络图片只是进行了内存缓存,若是杀掉应用进程再从新打开后仍是要从新下载图片,这对于用户而言,每次打开应用仍是会消耗下载图片的流量,不过咱们能够从中学习到一些思路来本身设计网络图片加载框架,下面做者就简单的基于Image.network来进行一下改造,增长图片的磁盘缓存。

解决方案

咱们经过源码分析可知,图片在缓存中未找到时,会经过网络直接下载获取,而下载的方法是在NetworkImage类中,因而咱们能够参考NetworkImage来自定义一个ImageProvider。

代码实现

拷贝一份NetworkImage的代码到新建的network_image.dart文件中,在_loadAsync方法中咱们加入磁盘缓存的代码。

static final CacheFileImage _cacheFileImage = CacheFileImage();

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

/// 新增代码块start
/// 从缓存目录中查找图片是否存在
    final Uint8List cacheBytes = await _cacheFileImage.getFileBytes(key.url);
    if(cacheBytes != null) {
      return PaintingBinding.instance.instantiateImageCodec(cacheBytes);
    }
/// 新增代码块end

    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 Exception('HTTP request failed, statusCode: ${response?.statusCode}, $resolved');

/// 新增代码块start
/// 将下载的图片数据保存到指定缓存文件中
    await _cacheFileImage.saveBytesToFile(key.url, bytes);
/// 新增代码块end

    return PaintingBinding.instance.instantiateImageCodec(bytes);
  }
复制代码

代码中注释已经代表了基于原有代码新增的代码块,CacheFileImage是本身定义的文件缓存类,完整代码以下

import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:crypto/crypto.dart';
import 'package:path_provider/path_provider.dart';

class CacheFileImage {

  /// 获取url字符串的MD5值
  static String getUrlMd5(String url) {
    var content = new Utf8Encoder().convert(url);
    var digest = md5.convert(content);
    return digest.toString();
  }

  /// 获取图片缓存路径
  Future<String> getCachePath() async {
    Directory dir = await getApplicationDocumentsDirectory();
    Directory cachePath = Directory("${dir.path}/imagecache/");
    if(!cachePath.existsSync()) {
      cachePath.createSync();
    }
    return cachePath.path;
  }

  /// 判断是否有对应图片缓存文件存在
  Future<Uint8List> getFileBytes(String url) async {
    String cacheDirPath = await getCachePath();
    String urlMd5 = getUrlMd5(url);
    File file = File("$cacheDirPath/$urlMd5");
    print("读取文件:${file.path}");
    if(file.existsSync()) {
      return await file.readAsBytes();
    }

    return null;
  }

  /// 将下载的图片数据缓存到指定文件
  Future saveBytesToFile(String url, Uint8List bytes) async {
    String cacheDirPath = await getCachePath();
    String urlMd5 = getUrlMd5(url);
    File file = File("$cacheDirPath/$urlMd5");
    if(!file.existsSync()) {
      file.createSync();
      await file.writeAsBytes(bytes);
    }
  }
}
复制代码

这样就增长了文件缓存的功能,思路很简单,就是在获取网络图片以前先检查一下本地文件缓存目录中是否有缓存文件,若是有则不用再去下载,不然去下载图片,下载完成后当即将下载到的图片缓存到文件中供下次须要时使用。

工程的pubspec.yaml中须要增长如下依赖库

dependencies:
	path_provider: ^0.4.1
	crypto: ^2.0.6
复制代码

自定义ImageProvider使用

在建立图片Widget时使用带参数的非命名构造函数,指定image参数为自定义ImageProvider对象便可,代码示例以下

import 'imageloader/network_image.dart' as network;

  Widget getNetworkImage() {
    return Container(
      color: Colors.blue,
      width: 200,
      height: 200,
      child: Image(image: network.NetworkImage("https://flutter.dev/images/flutter-mono-81x100.png")),
    );
  }
复制代码

写在最后

以上对Flutter中自带的Image小部件的网络图片加载流程进行了源码分析,了解了源码的设计思路以后,咱们新增了简单的本地文件缓存功能,这使咱们的网络图片加载同时具有了内存缓存和文件缓存两种能力,大大提高了用户体验,若是其余同窗有更好的方案能够给做者留言交流。

说明:

文章转载自对应的“Flutter编程指南”微信公众号,更多Flutter相关技术文章打开微信扫描二维码关注微信公众号获取。

相关文章
相关标签/搜索