Flutter版本: Flutter (Channel master, v1.9.1, on Mac OS X 10.14.4 18E226, locale zh-Hans-CN)
基本用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const Image({ Key key, @required this.image, this.frameBuilder, this.loadingBuilder, this.semanticLabel, this.excludeFromSemantics = false, 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, this.filterQuality = FilterQuality.low, }) : assert(image != null), assert(alignment != null), assert(repeat != null), assert(filterQuality != null), assert(matchTextDirection != null), super(key: key);
上面是Image Widget的构造方法,其中四种属性是最常被使用的:
width和height表示的是控件的大小,如果为null则自适应图片本身的宽高或者由更外层的控件决定(Tight Layout Constraints)。如果设定的尺寸大小和图片本身的大小不一致,那就要根据fit属性来决定最终的显示。
fit表示的是图片伸缩类型,和Android中的scaleType类似,包含以下几种:
fill contain cover fitWidth fitHeight scaleDown none
image属性为ImageProvider 类型,官方提供了四种供开发者直接使用:
NetworkImage AssetImage MemoryImage FileImage
它们分别对应于Image的四种工厂方法:
+ `Image.network("")`,加载网络图片
Image.asset("",package:"")
,加载本地资源文件,需要注意的是图片文件必须在pubspec.yaml 中注册。如果希望Flutter在不同分辨率下使用不同大小的图片则需要在文件夹下创建2.0x,3.0x等子文件夹,在这些文件夹中放入同名文件并且在yaml中注册。至于package属性则是在lib下创建图片文件夹时才会用到,这些都可以在官网上看到(在Flutter中添加资源和图片 ),本文不再赘述。
Image.memory(Uint8List)
,平时开发用的较少,表示直接加载字节数组
Image.file("")
,表示直接加载本地图片,一般会配合path_provider使用
图片加载 在整个图片加载的过程中起决定性作用的是ImageProvider类
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 abstract class ImageProvider<T> { ImageStream resolve(ImageConfiguration configuration) { assert(configuration != null); //创建ImageStream final ImageStream stream = ImageStream(); T obtainedKey; ///省略了error handle的逻辑 dangerZone.runGuarded(() { Future<T> key; try { //获取ImageProvider对象 key = obtainKey(configuration); } catch (error, stackTrace) { handleError(error, stackTrace); return; } key.then<void>((T key) { obtainedKey = key; //首先从缓存中去取,如果取不到就调用load(key)方法加载图片 final ImageStreamCompleter completer = PaintingBinding.instance .imageCache.putIfAbsent(key, () => load(key), onError: handleError); if (completer != null) { stream.setCompleter(completer); } }).catchError(handleError); }); return stream; } Future<T> obtainKey(ImageConfiguration configuration); @protected ImageStreamCompleter load(T key); }
可以看到其中比较关键的几个方法:resolve, obtainKey, load
下面是上面代码块的拆解分析:
生成ImageStream
生成ImageProvider对象:key
以key为键去ImageCache中查看是否已经有缓存(ImageStreamCompleter)对象,如果有就直接返回,如果没有就调用load(key)方法生成对应的ImageStreamCompleter并缓存起来
把上一步获取的ImageStreamCompleter对象设置给ImageStream
1 2 3 4 5 6 7 8 9 10 11 12 ///ImageStream setCompleter方法: void setCompleter(ImageStreamCompleter value) { assert(_completer == null); _completer = value; if (_listeners != null) { final List<ImageStreamListener> initialListeners = _listeners; _listeners = null; initialListeners.forEach(_completer.addListener); } }
可以看到的是ImageStream把所有监听器都注册到ImageStreamCompleter,这样在图片加载成功后就可以回调给所有的监听者,这其中就包括_ImageState:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ///ImageStreamCompleter setImage方法: void setImage(ImageInfo image) { _currentImage = image; if (_listeners.isEmpty) return; // Make a copy to allow for concurrent modification. final List<ImageStreamListener> localListeners = List<ImageStreamListener>.from(_listeners); for (ImageStreamListener listener in localListeners) { try { listener.onImage(image, false); } catch (exception, stack) { reportError( context: ErrorDescription('by an image listener'), exception: exception, stack: stack, ); } } }
可以看到Flutter中Image的加载还是很简洁的,基本上可以简化为Image控件通过ImageStream注册监听器到真正的图片加载器ImageStreamCompleter,然后在图片成功获取后通过回调的方式返回给Image Widget 。
下面是笔者绘制的NetWorkImage图片加载UML图:(其他类型的ImageProvider加载过程都类似)
图片优化 Flutter中的Image组件使用十分简洁,但是也有一些缺陷。
Image没有提供error callback,如果开发者想定制错误布局需要自己自定义ImageProvider
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 class CustomNetworkImage extends ImageProvider<NetworkImageSSL> { const CustomNetworkImage(this.url, {this.scale = 1.0, this.headers, this.imageErrorListener}) : assert(url != null), assert(scale != null); final String url; final double scale; final Map<String, String> headers; final ImageErrorListener imageErrorListener; @override ImageStreamCompleter load(NetworkImageSSL key) { MultiFrameImageStreamCompleter multiFrameImageStreamCompleter = MultiFrameImageStreamCompleter( codec: _loadAsync(key), scale: key.scale, informationCollector: () { List<DiagnosticsNode> informationCollector; informationCollector.add(DiagnosticsProperty('Image provider', this)); informationCollector.add(DiagnosticsProperty('Image key', key)); return informationCollector; }); ///添加Error Callback ImageStreamListener listener; listener = ImageStreamListener((imageInfo, ret) { //移除监听器 multiFrameImageStreamCompleter.removeListener(listener); }, onError: (ex, stack) { multiFrameImageStreamCompleter.removeListener(listener); //回调给上层控件 imageErrorListener?.call(ex, stack); }); multiFrameImageStreamCompleter.addListener(listener); return multiFrameImageStreamCompleter; } }
如果需要添加占位图,可以考虑使用Image控件的loadingBuilder属性,Flutter还提供了下载进度的回调通知
1 2 3 4 5 typedef ImageLoadingBuilder = Widget Function( BuildContext context, Widget child, ImageChunkEvent loadingProgress, );
2、缓存不友好 Flutter中ImageCache只实现了内存缓存(默认最多缓存1000张图片和100M内存),并且缓存的是ImageStreamCompleter(不是ui.Image)对象。
如果想要自定义缓存数量或者最大内存可以在Root Widget里定义:
1 2 3 4 5 6 7 @override void initState() { super.initState(); //最多100张图片和150M内存 PaintingBinding.instance.imageCache.maximumSize = 100; PaintingBinding.instance.imageCache.maximumSizeBytes = 150<<20; }
ImageCache是没有磁盘缓存的,这就意味着可能会重复加载图片,从而造成流量浪费。要添加磁盘缓存,需要定制ImageProvider(下面是伪码)
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 Future<ui.Codec> _loadAsync(NetworkImageSSL key) async { assert(key == this); /////////////////在加载前判断本地是否有缓存///////////////// if(diskCached(key.url)){ return getBytes(key.url); } /////////////////在加载前判断本地是否有缓存///////////////// 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('NetworkImageSSL is an empty file: $resolved'); } /////////////////缓存到本地///////////////// saveBytesToDisk(key.url,bytes); /////////////////缓存到本地///////////////// return await ui.instantiateImageCodec(bytes); }
注意到上述代码最后一个过程:return await ui.instantiateImageCodec(bytes);
这段代码的含义是解析图片字节数组获取ui.Codec对象,但是可惜的是Flutter不会根据实际显示图片的大小进行解码。举个栗子,假如我要加载一张1000x1000大小的图片,而显示区域只有100x100,那解码所耗费的内存是按1000x1000算的,这显然会造成不必要的浪费。
笔者用一张图片做了测试:
优化的方案很简单:
1 2 3 4 5 6 7 8 Future<Codec> instantiateImageCodec(Uint8List list, { int targetWidth, int targetHeight, }) { return _futurize( (_Callback<Codec> callback) => _instantiateImageCodec(list, callback, null, targetWidth ?? _kDoNotResizeDimension, targetHeight ?? _kDoNotResizeDimension) ); }
只要在解码时传入targetWidth和targetHeight即可(Flutter 版本至少要大于1.5.4,低于此版本就没法使用该优化方案 )
3、无法降低的内存占用 Flutter中的Image内存似乎存在着内存泄漏的危险,笔者做了一个简单的实验,即一张空白页面加载一张网络图片,下面是实验过程(数据均来自Dart Observatory):
打开页面
可以看到新生代内存占用为2.9M,老年代为0
销毁页面
可以看到新生代内存占用为0,老年代为2.9M,至此都是正常的,因为Dart VM的老年代就是存储较大内存对象的,然后笔者进行了手动GC操作,发现内存占用没发生变化,即老年代里的Image对象没有被回收。
销毁页面+手动清缓存 1 2 3 4 5 6 7 @override void initState() { super.initState(); //禁止缓存 PaintingBinding.instance.imageCache.maximumSize = 0; PaintingBinding.instance.imageCache.maximumSizeBytes = 0; }
起初笔者考虑到问题可能出在图片缓存上,因为缓存持有了图片对象可能导致内存无法被彻底回收,但是结果是 一样的,Image对象依旧无法被回收。
调用native方法回收 ImageStreamCompleter->ImageInfo->ui.Image->dispose->native 'Image_dispose'
笔者尝试了在页面销毁的时候销毁ui.Image对象,并且手动GC ,结果是
可以看到已经看不到图片对象了,但是需要注意的是该方法会彻底回收ui.Image对象,所以如果应用到生产项目需要考虑到其他可能引用到ui.Image的地方 ,毕竟这个方案有点过于简单粗暴了。下面是参考伪码
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 class CacheableNetworkImage extends ImageProvider<NetworkImageSSL> { CacheableNetworkImage(this.url, {this.scale = 1.0, this.headers}) : assert(url != null), assert(scale != null); final String url; final double scale; final Map<String, String> headers; ///////////保存imageInfo对象//////////// ImageInfo imageInfo; ///////////保存imageInfo对象//////////// ////////////页面销毁时调用/////////////// void dispose() { imageInfo?.image?.dispose(); } ////////////页面销毁时调用/////////////// @override ImageStreamCompleter load(NetworkImageSSL key) { var multiFrameImageStreamCompleter = new MultiFrameImageStreamCompleter( codec: _loadAsync(key), scale: key.scale, informationCollector: () { List<DiagnosticsNode> informationCollector; informationCollector.add(DiagnosticsProperty('Image provider', this)); informationCollector.add(DiagnosticsProperty('Image key', key)); return informationCollector; }); ///添加Error Callback ImageStreamListener listener; listener = ImageStreamListener((imageInfo, ret) { /////////保存ImageInfo对象///////// this.imageInfo = imageInfo; /////////保存ImageInfo对象///////// multiFrameImageStreamCompleter.removeListener(listener); }, onError: (ex, stack) { multiFrameImageStreamCompleter.removeListener(listener); imageErrorListener?.call(ex, stack); }); multiFrameImageStreamCompleter.addListener(listener); return multiFrameImageStreamCompleter; } ...................... }