Flutter 默认提供了Image 用于从网络、文件等加载图片,并且使用ImageCache 统一管理图片缓存,但有时候并不能满足使用需求(比如网络图片没有磁盘缓存,导致每次 ImageCache 清除缓存之后又要从网络下载),所以又出现了flutter_cached_network_image 、extended_image 等基于 Flutter 原生的解决方案,以及power_image 等基于混合开发的解决方案。
本文对 extended_image 加载过程、原理做一简单分析。
extended_image extended_image 是基于官方 Image 的拓展组件,支持加载以及失败显示,缓存网络图片,缩放拖拽图片,图片浏览(微信掘金效果),滑动退出页面(微信掘金效果),编辑图片(裁剪旋转翻转),保存,绘制自定义效果等功能。
本文主要对其加载缓存网络图片 的流程做一分析,因为这个库是官方 Image 的拓展,所以我们会在之前对 Image 的分析 基础上进行对比分析。
extended_image 的架构图如下:
分析 因为 extended_image 的定位是官方 Image 的拓展版,所以大部分使用方式和官方类似。
ExtendedImage 他的构造函数分别是:
ExtendedImage
ExtendedImage.asset
ExtendedImage.file
ExtendedImage.memory
ExtendedImage.network
同样是在构造函数中指定并创建 ImageProvider,不过 extented_image 库的 ImageProvider 都是继承自官方 ImageProvider 并且混入了ExtendedImageProvider 的子类。以ExtendedImage.network
为例,创建的 ImageProvider 类型是ExtendedNetworkImageProvider 。
其余的步骤和我们之前分析的官方 Image 组件类似,在 _ExtendedImageState 中使用 ImageProvider 获取并监听 ImageStream ,当成功加载图片之后获得ImageInfo? _imageInfo
并刷新页面,在_ExtendedImageState.build
方法中,虽然 extended_image 增加了一些特有的加载中、加载失败、手势等封装,但最后还是使用ImageInfo.image
创建ExtendedRawImage 以展示图片内容。
如此可见,在从网络加载图片这部分内容来看,ExtendedImage 和 Image 的主要不同在于ExtendedNetworkImageProvider 的实现:
ExtendedNetworkImageProvider
这部分内容的代码在extended_image_library 中。
ExtendedNetworkImageProvider 继承自ImageProvider ,混入了ExtendedImageProvider ,后者提供了get imageCache
/instantiateImageCodec
/resolveStreamForKey
等一系列通用方法。
下面是 ExtendedNetworkImageProvider 的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 abstract class ExtendedNetworkImageProvider extends ImageProvider <ExtendedNetworkImageProvider > with ExtendedImageProvider <ExtendedNetworkImageProvider > { factory ExtendedNetworkImageProvider( String url, { double scale, Map <String , String >? headers, bool cache, int retries, Duration? timeLimit, Duration timeRetry, CancellationToken? cancelToken, String? cacheKey, bool printError, bool cacheRawData, String? imageCacheName, Duration? cacheMaxAge, }) = network_image.ExtendedNetworkImageProvider }
ExtendedNetworkImageProvider 是个抽象类,他的逻辑在network_image.ExtendedNetworkImageProvider
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 import 'extended_network_image_provider.dart' as image_provider;class ExtendedNetworkImageProvider extends ImageProvider <image_provider .ExtendedNetworkImageProvider > with ExtendedImageProvider <image_provider .ExtendedNetworkImageProvider > implements image_provider .ExtendedNetworkImageProvider { @override ImageStreamCompleter load( image_provider.ExtendedNetworkImageProvider key, DecoderCallback decode) { final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>(); return MultiFrameImageStreamCompleter( codec: _loadAsync( key as ExtendedNetworkImageProvider, chunkEvents, decode, ), scale: key.scale, chunkEvents: chunkEvents.stream, informationCollector: () { ... }, ); } Future<ui.Codec> _loadAsync( ExtendedNetworkImageProvider key, StreamController<ImageChunkEvent> chunkEvents, DecoderCallback decode, ) async { assert (key == this ); final String md5Key = cacheKey ?? keyToMd5(key.url); ui.Codec? result; if (cache) { try { final Uint8List? data = await _loadCache( key, chunkEvents, md5Key, ); if (data != null ) { result = await instantiateImageCodec(data, decode); } } catch (e) { if (printError) { print (e); } } } if (result == null ) { try { final Uint8List? data = await _loadNetwork( key, chunkEvents, ); if (data != null ) { result = await instantiateImageCodec(data, decode); } } catch (e) { if (printError) { print (e); } } } if (result == null ) { return Future<ui.Codec>.error(StateError('Failed to load $url .' )); } return result; } }
从上述代码可以看到,如果需要缓存时,除了 ImageCache 本身的缓存外,ExtendedNetworkImageProvider 还会执行_loadCache
尝试从本地文件中读取缓存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 Future<Uint8List?> _loadCache( ExtendedNetworkImageProvider key, StreamController<ImageChunkEvent>? chunkEvents, String md5Key, ) async { final Directory _cacheImagesDirectory = Directory( join((await getTemporaryDirectory()).path, cacheImageFolderName)); Uint8List? data; if (_cacheImagesDirectory.existsSync()) { final File cacheFlie = File(join(_cacheImagesDirectory.path, md5Key)); if (cacheFlie.existsSync()) { if (key.cacheMaxAge != null ) { final DateTime now = DateTime .now(); final FileStat fs = cacheFlie.statSync(); if (now.subtract(key.cacheMaxAge!).isAfter(fs.changed)) { cacheFlie.deleteSync(recursive: true ); } else { data = await cacheFlie.readAsBytes(); } } else { data = await cacheFlie.readAsBytes(); } } } else { await _cacheImagesDirectory.create(); } if (data == null ) { data = await _loadNetwork( key, chunkEvents, ); if (data != null ) { await File(join(_cacheImagesDirectory.path, md5Key)).writeAsBytes(data); } } return data; }
上述代码中执行到的ExtendedNetworkImageProvider._loadNetwork()
方法只会使用HttpClient
从网络中下载图片并返回。
ExtendedImageProvider 此外,之前提到的ExtendedImageProvider
为 extended_image 库中的 ImageProvider 提供了一些通用的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Map <ExtendedImageProvider<dynamic >, Uint8List> rawImageDataMap = <ExtendedImageProvider<dynamic >, Uint8List>{}; Map <String , ImageCache> imageCaches = <String , ImageCache>{};mixin ExtendedImageProvider<T extends Object > on ImageProvider<T> { bool get cacheRawData; String? get imageCacheName; ImageCache get imageCache { if (imageCacheName != null ) { return imageCaches.putIfAbsent(imageCacheName!, () => ImageCache()); } else { return PaintingBinding.instance.imageCache; } } }
此外,还改动了ExtendedImageProvider.resolveStreamForKey
方法以使用指定的 ImageCache。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void resolveStreamForKey( ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError, ) { if (stream.completer != null ) { final ImageStreamCompleter? completer = imageCache.putIfAbsent( key, () => stream.completer!, onError: handleError, ); assert (identical(completer, stream.completer)); return ; } final ImageStreamCompleter? completer = imageCache.putIfAbsent( key, () => load(key, PaintingBinding.instance.instantiateImageCodec), onError: handleError, ); if (completer != null ) { stream.setCompleter(completer); } }
综上所见,ExtendedImageProvider 的主要作用是借助rawImageDataMap
提供了缓存图片原始数据的功能,此外还提供了一个 ImageCache 分组的方法,以便对一部分图片缓存统一处理。
总结 仅就从网络加载图片而言,extended_image 和 Flutter 官方 Image 组件的主要区别在于:在 ImageCache 之外,多了一层本地磁盘缓存 ,如果这二者都未命中缓存则从网络下载图片。
除此之外,extended_image 本身还提供了诸如图片缩放拖拽、滑动退出等图片操作常用的“大而全”的功能。这部分见仁见智,如果 APP 需求刚好需要用到这些功能的话,extended_image 是个不错的选择,但是如果只是想解决图片缓存问题的话,可能会显得有些臃肿。
另外一个常用的图片库flutter_cached_network_image 则是借助flutter_cache_manager 实现缓存网络图片的功能,相对比较轻量。
上述两种库都是基于 Flutter Image 组件实现图片加载、缓存,阿里巴巴出品的power_image 则是一款为 Flutter-Native 混合项目开发的图片加载库,借助 Texture 和 ffi 通过 Native 端已有的图片加载库完成图片加载、缓存的功能,Flutter 端只负责展示(以及 ImageCache 缓存)。
参考资料 extended_image
extended_image_library