查看原文
其他

Flutter系列之图片加载详解

jzman 躬行之 2022-08-26
PS:自律实践起来真的没有想象的那么简单。
Flutter 支持加载的图片类型:JPEG、PNG、GIF、WebP、BMP 和 WBMP,Flutter Image 的组件的必须参数是一个 ImageProviderImageProvider 是一个抽象类,具体获取图片由子类实现,本文将从如下几个方面学习 Flutter 中的图片加载:
  1. 图片加载

  2. 图片预加载

  3. 图片缓存

  4. 清除图片缓存

  5. 图片加载进度监听

  6. 加载图片案例

图片加载

Flutter 本身实现了图片加载,可以加载网络、SD卡、Asset、内存里面的图片,可以通过如下方式生成图片资源对应的 Image:
Image.network(String src,{...});
Image.file(File file,{...});
Image.asset(String name,{...});
Image.memeory(Uint8List bytes,{...});
下面以加载网络图片为例介绍 Flutter 中图片加载的流程,查看 Image.network() 源代码如下:
Image.network(
// ...
}) : image = NetworkImage(src, scale: scale, headers: headers),
   assert(alignment != null),
   assert(repeat != null),
   assert(matchTextDirection != null),
   super(key: key);
使用 Image.network 生成 Image 的时候创建了 NetworkImageNetworkImage 类是 ImageProvider 的子类,ImageProvider 是一个抽象类,里面提供了解析图片资源的 resolve 方法、将图片缓存移除去的 evict 方法以及加载图片的抽象方法 load 等,load 方法由子类具体实现,ImageProvider 源码分析如下:
/// ImageProvider是一个抽象类,具体加载由子类实现
abstract class ImageProvider<T> {
  const ImageProvider();

  /// 使用提供的ImageConfiguration对象生成ImageStream
  ImageStream resolve(ImageConfiguration configuration) {
    assert(configuration != null);
    final ImageStream stream = ImageStream();
    T obtainedKey;

    //...代码

    dangerZone.runGuarded(() {
      Future<T> key;
      try {
        // 获取图片资源对应的key
        key = obtainKey(configuration);
      } catch (error, stackTrace) {
        handleError(error, stackTrace);
        return;
      }
      key.then<void>((T key) {
        // 获取到图片资源对应的key
        obtainedKey = key;
        // 获取key对应的ImageStreamCompleter,如果缓存中没有则调用传入的loader回调
        // 去加载并将其添加到缓存中
        final ImageStreamCompleter completer = PaintingBinding
            .instance.imageCache
            .putIfAbsent(key, () => load(key), onError: handleError);
        if (completer != null) {
          stream.setCompleter(completer);
        }
      }).catchError(handleError);
    });
    return stream;
  }

  /// 将图片从缓存中移除,返回值为true表示移除成功
  Future<bool> evict(
      {ImageCache cache,
      ImageConfiguration configuration = ImageConfiguration.empty}) async {
    cache ??= imageCache;
    final T key = await obtainKey(configuration);
    return cache.evict(key);
  }

  /// 获取对应图片资源key,具体由子类实现
  Future<T> obtainKey(ImageConfiguration configuration);

  /// 根据key加载图片并将其转换为ImageStreamCompleter,具体由子类实现
  @protected
  ImageStreamCompleter load(T key);

  @override
  String toString() => '$runtimeType()';
}
resolve 方法中解析图片资源使用 PaintingBinding 的单例获取图片缓存 imageCache 并调用 putIfAbsent 方法,里面实现了 LRU 缓存基本逻辑,根据是否有缓存进行处理,如果有缓存则从缓存中获取与之对应的图片资源,反之则调用传入的 loader 进行图片加载并将加载的图片添加到缓存 ImageCache 中。
继续查看最终加载网络图片的 ImageProvider 的实现类 NetworkImageload 方法实现如下:
@override
ImageStreamCompleter load(image_provider.NetworkImage key) {

    final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
    // _loadAsync方法
    return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key, chunkEvents),
      chunkEvents: chunkEvents.stream,
      scale: key.scale,
      informationCollector: () {
        return <DiagnosticsNode>[
          DiagnosticsProperty<image_provider.ImageProvider>('Image provider'this),
          DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
        ];
      },
    );
}
load 方法中调用了 _loadAsync,这也是真正的下载图片的方法,同时要对图片进行解码返回,_loadAsync 方法源码如下:
/// 下载图片并解码图片
Future<ui.Codec> _loadAsync(
    NetworkImage key,
    StreamController<ImageChunkEvent> chunkEvents,
    ) async {
  try {
    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 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');
    // 将图片解码成二进制Codec对象
    return PaintingBinding.instance.instantiateImageCodec(bytes);
  } finally {
    chunkEvents.close();
  }
}
下载完图片会将图片解码成二进制对应的 Codec 对象,该 Codec 对象具体由 Flutter-engine 中原生方法进行解码,如下:
String _instantiateImageCodec(Uint8List list, _Callback<Codec> callback, _ImageInfo imageInfo, int targetWidth, int targetHeight)
  native 'instantiateImageCodec';
上面这个过程我们知道了图片是由 Flutter-engine 中原生方法进行解码最终返回一个 ImageStreamCompleter,这个 ImageStreamCompleterresolve 方法中被设置给 ImageStream,resolve 方法返回这个 ImageStream,我们可以通过这个 ImageStream 进行图片加载进度的监听,ImageStream 源码如下:
/// ImageStream用来处理图片资源,表示该图片资源还未加载完成,图片资源一旦加载完成,
/// ImageStream的真正数据对象就是由dart:ui.Image和scale构成的ImageInfo,
class ImageStream extends Diagnosticable {
  ImageStream();

  /// 管理正在加载中的图片资源,监听图片资源加载,如加载成功、加载中、加载失败
  ImageStreamCompleter get completer => _completer;
  ImageStreamCompleter _completer;

  List<ImageStreamListener> _listeners;

  /// 设置一个图片加载监听器,通常由创建ImageStream的ImageProvider自动设置,且每个ImageStream中只能设置一次
  void setCompleter(ImageStreamCompleter value) {
    assert(_completer == null);
    _completer = value;
    if (_listeners != null) {
      final List<ImageStreamListener> initialListeners = _listeners;
      _listeners = null;
      initialListeners.forEach(_completer.addListener);
    }
  }

  /// 添加图片加载监听器
  void addListener(ImageStreamListener listener) {
    if (_completer != nullreturn _completer.addListener(listener);
    _listeners ??= <ImageStreamListener>[];
    _listeners.add(listener);
  }

  /// 移除图片加载监听器
  void removeListener(ImageStreamListener listener) {
    if (_completer != nullreturn _completer.removeListener(listener);
    assert(_listeners != null);
    for (int i = 0; i < _listeners.length; i += 1) {
      if (_listeners[i] == listener) {
        _listeners.removeAt(i);
        break;
      }
    }
  }

  Object get key => _completer ?? this;

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    // ...
  }
}
至此,我们知道了图片资源最终会被转换成 ImageStreamresolve 方法会在 Image 组件的对应生命周期方法中被调用,如 didChangeDependenciesdidUpdateWidget等方法,在组件构建的时候会创建 RawImage,继续跟踪源码则是 RenderImage,在其 paint 方法中调用了 paintImage 方法,里面通过 canvas 完成图片配置信息的绘制。

图片预加载

在 Flutter 中可以通过 precacheImage 方法来对图片进行预加载,也就是将图片提前添加到缓存中,当需要加载图片的时候就直接从缓存中获取,precacheImage 方法还是通过 ImageProvider 的  resolve 方法解析图片资源并将其添加到图片缓存中,其方法源码如下:
/// precacheImage
Future<void> precacheImage(
  ImageProvider provider,
  BuildContext context, {
  Size size,
  ImageErrorListener onError,
}) {
  final ImageConfiguration config = createLocalImageConfiguration(context, size: size);
  final Completer<void> completer = Completer<void>();
  // 解析图片资源将其添加到缓存中
  final ImageStream stream = provider.resolve(config);
  ImageStreamListener listener;
  listener = ImageStreamListener(
    // 省略...
    },
  );
  stream.addListener(listener);
  return completer.future;
}
使用时根据图片来源选择不同的 ImageProvider 来缓存对应的图片,使用方式如下:
// 预缓存图片
precacheImage(new AssetImage("images/cat.jpg"), context);

图片缓存

ImageCache 是 flutter 提供的基于 LRU 算法的缓存实现,默认可以缓存 1000 张图片,缓存大小最大 100 M,当缓存超过任意一个缓存限制都会从缓存中移除最近最少使用的缓存项,当然,也可以根据项目需要设置最大缓存项 _maximumSize 的值和最大缓存大小 _maximumSizeBytes 的值,具体查看 ImageCache 源码相关注释,如下:
const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20// 100 MiB

/// 使用LRU实现的图片缓存。最多100张图片,缓存大小最大100M,缓存由ImageProvider及其子类实现
/// 其缓存实例由PaintingBinding单例持有
class ImageCache {
  // 正在加载的图片队列
  final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};

  // 缓存队列
  final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};

  /// 缓存项的最大数量,
  int get maximumSize => _maximumSize;
  int _maximumSize = _kDefaultSize;

  /// 设置缓存项最大数量
  set maximumSize(int value) {
    assert(value != null);
    assert(value >= 0);
    if (value == maximumSize) return;
    _maximumSize = value;
    if (maximumSize == 0) {
      clear();
    } else {
      _checkCacheSize();
    }
  }

  /// 当前缓存项的数量
  int get currentSize => _cache.length;

  /// 最大缓存大小(字节)
  int get maximumSizeBytes => _maximumSizeBytes;
  int _maximumSizeBytes = _kDefaultSizeBytes;

  /// 设置缓存大小
  set maximumSizeBytes(int value) {
    assert(value != null);
    assert(value >= 0);
    if (value == _maximumSizeBytes) return;
    _maximumSizeBytes = value;
    if (_maximumSizeBytes == 0) {
      clear();
    } else {
      _checkCacheSize();
    }
  }

  /// 缓存当前大小(字节)
  int get currentSizeBytes => _currentSizeBytes;
  int _currentSizeBytes = 0;

  /// 清除缓存
  void clear() {
    _cache.clear();
    _pendingImages.clear();
    _currentSizeBytes = 0;
  }

  /// 根据对应key移除缓存,移除成功返回true,反之,还在加载完成的图片也会被移出同时移除
  /// 对应图片加载监听器,避免将其添加到缓存中
  bool evict(Object key) {
    final _PendingImage pendingImage = _pendingImages.remove(key);
    if (pendingImage != null) {
      pendingImage.removeListener();
      return true;
    }
    final _CachedImage image = _cache.remove(key);
    if (image != null) {
      _currentSizeBytes -= image.sizeBytes;
      return true;
    }
    return false;
  }

  /// 缓存API的入口
  ///
  /// 如果缓存可用,从给定key从缓存中获取ImageStreamCompleter返回,反之则使用
  /// 提供的回调loader()获取ImageStreamCompleter并返回,都会将key移动到最近使用的位置
  ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(),
      {ImageErrorListener onError}) {
    assert(key != null);
    assert(loader != null);
    ImageStreamCompleter result = _pendingImages[key]?.completer;
    // 如果图片还未加载完成,直接返回
    if (result != nullreturn result;
    // 如果有对应缓存,先从缓存中移除,然后再添加最近使用的位置
    final _CachedImage image = _cache.remove(key);
    if (image != null) {
      _cache[key] = image;
      return image.completer;
    }
    // 如果获取不到对应缓存,直接使用对应ImageProvider中的load方法加载图片
    try {
      result = loader();
    } catch (error, stackTrace) {
      if (onError != null) {
        onError(error, stackTrace);
        return null;
      } else {
        rethrow;
      }
    }
    void listener(ImageInfo info, bool syncCall) {
      // 加载失败的图片不会占用缓存大小
      final int imageSize =
          info?.image == null ? 0 : info.image.height * info.image.width * 4;
      final _CachedImage image = _CachedImage(result, imageSize);
      // 如果图片的大小大于缓存大小,且缓存大小不为0,则增加缓存到小到图片缓存大小
      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();
    }

    // 添加正在加载的图片到_pendingImages中,并设置加载图片的监听
    if (maximumSize > 0 && maximumSizeBytes > 0) {
      final ImageStreamListener streamListener = ImageStreamListener(listener);
      _pendingImages[key] = _PendingImage(result, streamListener);
      // Listener is removed in [_PendingImage.removeListener].
      result.addListener(streamListener);
    }
    return result;
  }

  // 缓存检查,超过缓存限制则从缓存中移除最近最少使用的缓存项
  void _checkCacheSize() {
    while (
        _currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
      final Object key = _cache.keys.first;
      final _CachedImage image = _cache[key];
      _currentSizeBytes -= image.sizeBytes;
      _cache.remove(key);
    }
    assert(_currentSizeBytes >= 0);
    assert(_cache.length <= maximumSize);
    assert(_currentSizeBytes <= maximumSizeBytes);
  }
}
// 缓存图片类
class _CachedImage {
  _CachedImage(this.completer, this.sizeBytes);

  final ImageStreamCompleter completer;
  final int sizeBytes;
}

// 正在加载的图片类
class _PendingImage {
  _PendingImage(this.completer, this.listener);

  final ImageStreamCompleter completer;
  final ImageStreamListener listener;

  void removeListener() {
    completer.removeListener(listener);
  }
}
上述代码是整个缓存逻辑,当 resolve 方法调用的时候会调用 putIfAbsent 方法,该方法是缓存的入口,如果已经有缓存则从缓存中获取,否则调用对应 ImageProviderload 方法去加载图片,并将其添加到缓存中。

清除图片缓存

清除图片缓存直接通过 PaintingBinding 的单例获取到 ImageCache 调用其 ckear 方法,如下:
/// 清理缓存
_clearCache(BuildContext context) {
  PaintingBinding.instance.imageCache.clear();
  Toast.show("缓存已清除", context);
}

图片加载进度监听

从前面我们知道 resolve 方法返回对应图片的 ImageStream,我们可以通过这个 ImageStream 设置图片加载监听,实际上添加的就是 ImageStreamListener,如下:
/// Image
Image image = Image.network(
  "https://cdn.nlark.com/yuque/0/2019/jpeg/644330/1576812507787-bdaeaf42-8317-4e06-a489-251686bf7b91.jpeg",
  width: 100,
  height: 100,
  alignment: Alignment.topLeft,
);

// 图片加载监听
image.image.resolve(ImageConfiguration()).addListener(
        ImageStreamListener((ImageInfo imageInfo, bool synchronousCall) {
      completer.complete(imageInfo.image);
    }, onChunk: (event) {
      int currentLength = event.cumulativeBytesLoaded;
      int totalLength = event.expectedTotalBytes;
      print("$currentLength/$totalLength from network");
    }, onError: (e, trace) {
      print(e.toString());
    }));
开发中最常用的是下面这种方式,通过 loadingBuilder 属性添加对图片加载进度的监听,实际上最终设置的也是 ImageStreamListener,如下:
/// 图片进度监听
class ImageLoadListenerSamplePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _ImageState();
  }
}
/// _ImageState
class _ImageState extends State<ImageLoadListenerSamplePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Image Load Listener"),
        centerTitle: true,
      ),
      body: Image.network(
        "https://cdn.nlark.com/yuque/0/2019/jpeg/644330/1576812507787-bdaeaf42-8317-4e06-a489-251686bf7b91.jpeg",
        width: 100,
        height: 100,
        loadingBuilder: (BuildContext context, Widget child,
            ImageChunkEvent loadingProgress) {
          if (loadingProgress == nullreturn child;
          int currentLength = loadingProgress.cumulativeBytesLoaded;
          int totalLength = loadingProgress.expectedTotalBytes;
          print("$currentLength/$totalLength from network");
          return CircularProgressIndicator(
            value: loadingProgress.expectedTotalBytes != null
                ? loadingProgress.cumulativeBytesLoaded /
                    loadingProgress.expectedTotalBytes
                : null,
          );
        },
      ),
    );
  }
}
显示效果如下:

加载图片案例

前面提到,Flutter 默认实现了加载网络、SD 卡、Asset、内存里面的图片,其中 SD 卡、内存获取图片使用 FutureBuilder 来处理异步任务返回 Image,不多说直接看代码:
/// 加载图片
class ImageLoadSamplePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _ImageSampleSate();
  }
}
/// _ImageSampleSate
class _ImageSampleSate extends State<ImageLoadSamplePage> {

  Future<Directory> _externalStorageDirectory;
  Future<Uint8List> _imageUint8List;

  /// 获取文件目录
  void _requestExternalStorageDirectory() {
    setState(() {
      _externalStorageDirectory = getExternalStorageDirectory();
    });
  }

  /// 把文件转换成字节
  void _requestBytes() {
    setState(() {
      File file = new File("/storage/emulated/0/owl.jpg");
      _imageUint8List = file.readAsBytes();
    });
  }

  @override
  Widget build(BuildContext context) {

    _requestExternalStorageDirectory();
    _requestBytes();

    return Scaffold(
      appBar: AppBar(
        title: Text("Image Sample"),
        centerTitle: true,
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _clearCache(context);
        },
        child: Icon(Icons.clear),
      ),
      body: ListView(
        scrollDirection: Axis.vertical,
        children: <Widget>[
          Text(
            "from network...",
            style: TextStyle(fontSize: 16),
          ),
          Image.network(
            "https://cdn.nlark.com/yuque/0/2019/jpeg/644330/1576812507787-bdaeaf42-8317-4e06-a489-251686bf7b91.jpeg",
            width: 100,
            height: 100,
            alignment: Alignment.topLeft,
          ),
          Text(
            "from file...",
            style: TextStyle(fontSize: 16),
          ),
          FutureBuilder<Directory>(
            future: _externalStorageDirectory,
            builder: _buildFileDirectory,
          ),
          Text(
            "from asset...",
            style: TextStyle(fontSize: 16),
          ),
          Image.asset(
            'images/cat.jpg',
            width: 100,
            height: 100,
            alignment: Alignment.topLeft,
          ),
          Text(
            "from memory...",
            style: TextStyle(fontSize: 16),
          ),
          FutureBuilder<Uint8List>(
            future: _imageUint8List,
            builder: _buildMemoryDirectory,
          ),
        ],
      ),
    );
  }

  /// 异步获取SD卡图片
  Widget _buildFileDirectory(
      BuildContext context, AsyncSnapshot<Directory> snapshot) {
    Text text = new Text("default");
    if (snapshot.connectionState == ConnectionState.done) {
      if (snapshot.hasData) {
        File file = new File("${snapshot.data.path}/owl.jpg");
        return Image.file(
          file,
          width: 100,
          height: 100,
          alignment: Alignment.topLeft,
        );
      } else if (snapshot.hasError) {
        text = new Text(snapshot.error);
      } else {
        text = const Text("unknow");
      }
    }
    print(text.data);
    return text;
  }


  /// 异步获取内存中图片
  Widget _buildMemoryDirectory(
      BuildContext context, AsyncSnapshot<Uint8List> snapshot) {
    Text text = new Text("default");
    if (snapshot.connectionState == ConnectionState.done) {
      if (snapshot.hasData) {
        return Image.memory(
          snapshot.data,
          width: 100,
          height: 100,
          alignment: Alignment.topLeft,
        );
      } else if (snapshot.hasError) {
        text = new Text(snapshot.error);
      } else {
        text = const Text("unknow");
      }
    }
    return text;
  }

  /// 清理缓存(为了测试缓存)
  _clearCache(BuildContext context) {
    PaintingBinding.instance.imageCache.clear();
    print("---_clearCache-->");
    Toast.show("缓存已清除", context);
  }
}
上述代码执行效果如下:
可以添加在公众号回复关键字【加群】邀你进微信交流群。
推荐阅读:

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存