奇技指南
本篇文章你将获得?
1、Flutter 图片加载方式
2、Flutter 图片加载源码实现流程
3、Flutter 图片加载优化点有什么
本文转载自奇舞移动技术。
Flutter Image
在 Flutter 中 Image 是展示图片的 widget ,用于从 ImageProvider 获取图像。 Image 支持的图片格式有 JPEG、WebP、GIF、animated WebP/GIF 、PNG 、BMP、 and WBMP。 Image 结构如下:可以看到图片上部有多个加载方式。
Flutter图片加载方式
1、Image.asset 使用 key 从AssetBundle获得的图片; 两种方式如下: Image(height: 100, width: 100, image: AssetImage(happy.png), ) Image.asset( happy.png, width: 100, height: 100,)当然这一方式,需要在 pubspec.yaml 文件中配置图片路径。
2、Image.network 从网络URL中获取图片; Image.network('https://p0.ssl.qhimg.com/t0183421f63f84fccaf.gif',fit: BoxFit.fill); 3、Image.file 从本地文件中获取图片; Image.file(File('/sdcard/happy.png')), 4、Image.memory 用于从 Uint8List 获取图片; new Image.memory(Uint8List bytes),bytes指内存中的图片数据,将其转化为图片对象。
Unit8List 与其他语言数据结构类比:
其他相关常用的加载图片的方式5、CacheNetworkImage
缓存的网络图片,此类属于 cached_network_image 库; new CachedNetworkImage( fit:BoxFit.fill, width:200, height:100, imageUrl:'https://p0.ssl.qhimg.com/t0183421f63f84fccaf.gif', placeholder:(context, url) => new ProgressView(), errorWidget:(context, url, error) => new Icon(Icons.error),); 6、FadeInImage.memoryNetwork 默认占位图和淡入效果 import 'package:transparent_image/transparent_image.dart';FadeInImage.memoryNetwork( placeholder: kTransparentImage, //kTransparentImage 属于 transparent_image 库 image: 'https://p0.ssl.qhimg.com/t0183421f63f84fccaf.gif',);7、Icon Icons 图片参考URL
new Icon(Icons.android,size: 200,);Flutter 加载 images 分辨率
Flutter 可以为当前设备加载适合其分辨率的图像。指定不同设备像素比例的图片可以这样分配asset文件夹:
…icon/happy.png
…/2.0x/happy.png…/3.0x/happy.png
主资源默认对应于 1.0 倍的分辨率图片;在设备像素比率为 1.8 的设备上会选用
.../2.0x/happy.png;对于在像素比率 2.7 的设备上 ,会选用.../3.0x/happy.png。
pubspec.yaml 文件中的 asset 声明中每一项都标识与实际文件对应。但是主资源缺少时,会按分辨率从低到高的顺序寻找加载。这里的加载方案,可以参考 Android 系统中图片加载的逻辑作对比。
Flutter 打包应用时,资源会按照 key-value的形式存入apk 的 assets/flutter_assets/AssetManifest.json文件中,加载资源时先解析 json 文件,选择最适合的图片进行加载显示,其中 AssetManifest.json 的具体内容简介如:
{ "assets/happy.png":[ "assets/2.0x/happy.png", "assets/3.0x/happy.png" ]}Android
android 上可以通过 AssetManager 获取 asset, 根据 key 查找到 openFd 。
key 是由 PluginRegistry.Registrar的 lookupKeyForAsset与 FlutterView 的getLookupKeyForAsset 得到;
PluginRegistry.Registrar用于开发插件,而FlutterView则用于开发平台 app的view。
pubspec.yaml
flutter:assets: - icons/happy.pngJava plugin code
AssetManager assetManager = registrar.context().getAssets();String key = registrar.lookupKeyForAsset("icons/happy.png");AssetFileDescriptor fd = assetManager.openFd(key);iOS
iOS 开发使用 mainbundle 获取 assets。使用FlutterPluginRegistrar的 lookupKeyForAsset 和
lookupKeyForAsset:fromPackage: 方法获取文件路径 ;
FlutterViewController 的 lookupKeyForAsset 和
lookupKeyForAsset:fromPackage: 方法获取文件路径 ;
然后 FlutterPluginRegistrar 用于开发插件,而 FlutterViewController 则用于开发平台 app 的 view 。
Objective-C plugin
NSString* key = [registrar lookupKeyForAsset:@"icons/happy.png"];NSString* path = [[NSBundle mainBundle] pathForResource:key ofType:nil];当然 pubspec.yaml 配置都是一致的。
源码分析
图片加载方式中有四种方式,接下来我们一起看看 framework 层加载图片是如何实现的。我们就以 Image.network 为例,跟进一下相关源码实现。
Image.network 的方法如下:
Image.network( String src, { Key key, double scale = 1.0, 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, Map<String, String> headers,}) : image = NetworkImage(src, scale: scale, headers: headers), assert(alignment != null), assert(repeat != null), assert(matchTextDirection != null), super(key: key);这方法的作用就是创建一个 用于显示从网络得到的ImageStream的 image 小部件,加载网络图片的 image 是由 NetworkImage 创建出来的,其中参数 src, scale, headers是不能为空的,其他的参数可以不做要求。NetworkImage 又是继承自 ImageProvider
所以image 就是 ImageProvider 。ImageProvider 是个抽象类,它的实现类包括:
NetworkImage,FileImage,ExactAssetImage,AssetImage,MemoryImage,AssetBundleImageProvider。
Image 源码部分如下
class Image extends StatefulWidget {/// 用于显示的 image final ImageProvider image; .......... @override _ImageState createState() => _ImageState();}_ImageState 类
class _ImageState extends State<Image> with WidgetsBindingObserver { ImageStream _imageStream; ImageInfo _imageInfo; .......@overridevoid initState() { super.initState(); WidgetsBinding.instance.addObserver(this);} @overridevoid didChangeDependencies() { _updateInvertColors(); _resolveImage();//解析图片从这里开始 //设置和移除监听图片变化的回调 if (TickerMode.of(context)) _listenToStream(); else _stopListeningToStream(); super.didChangeDependencies();} void _resolveImage() { //根据 ImageConfiguration 调用 ImageProvider 的 resolve 函数获得 ImageStream 对象 final ImageStream newStream =widget.image.resolve(createLocalImageConfiguration( context, size: widget.width != null && widget.height != null ? Size(widget.width,widget.height) : null, )); _updateSourceStream(newStream);}......}它的生命周期方法方法包括initState(),didChangeDependencies(),build()、deactivate()、dispose()、didUpdateWidget() 等等。当它插入到渲染树时,先调用initState()函数,再调用didChangeDependencies()。代码中可以看到调用了方法 _resolveImage(),这个方法中创建了 ImageStream 的新对象newStream 。widget.image就是 ImageProvider,调用resolve方法,代码如下:
ImageStream resolve(ImageConfiguration configuration) { final ImageStream stream = ImageStream(); T obtainedKey; bool didError = false; Future<void> handleError(dynamic exception, StackTrace stack) async { if (didError) { return; } didError = true; await null; // 等待事件轮询,以防侦听器被添加到图像流中。 final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter(); stream.setCompleter(imageCompleter); ......} ...... Future<T> key; try { key = obtainKey(configuration); } catch (error, stackTrace) { return; } key.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;ImageStreamCompleter 用于管理 dart:ui 加载的类的基类。ImageStreams 的对象很少直接构造,而是由 ImageStreamCompleter 自动配置它。ImageStream 中的图片管理者 ImageStreamCompleter 通过方法创建,imageCache 是 Flutter 框架中实现的用于图片缓存的单例,它在 Dart 虚拟机加载时就已经创建。imageCache 最多可缓存 1000 张图像和 100MB 内存空间。可以使用 [maximumSize] 和 [maximumSizeBytes]调整最大大小。
PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key), onError:handleError); 根据源码可以看到两个关键方法 :putIfAbsent 和 load。putIfAbsent
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), {ImageErrorListener onError }) { ImageStreamCompleter result = _pendingImages[key]?.completer; // 因为图像还没有加载,不需要做任何事情。 if (result != null) return result; // 从缓存列表中根据Key删除对应的 imageprovider,便于将它移动到下面最近使用位置。 final _CachedImage image = _cache.remove(key); if (image != null) { _cache[key] = image; return image.completer; } try { result = loader(); } catch (error, stackTrace) { ...... } 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); // 如果图像大于最大缓存大小,且缓存大小不为零,则将缓存大小增加到图像大小加上 1000。 // 思考点:一直这么加什么时候引起崩溃? 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) { final ImageStreamListener streamListener = ImageStreamListener(listener); _pendingImages[key] = _PendingImage(result, streamListener); // 移除 [_PendingImage.removeListener] 上的监听 result.addListener(streamListener); } return result;}load
/// 拉取网络图片的 image_provider.NetworkImage 具体实现.class NetworkImage extendsimage_provider.ImageProvider<image_provider.NetworkImage> implementsimage_provider.NetworkImage {...................... @override ImageStreamCompleter load(image_provider.NetworkImage key) { final StreamController<ImageChunkEvent> chunkEvents =StreamController<ImageChunkEvent>(); 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), ]; }, );}loadAsync
Future<ui.Codec> _loadAsync( NetworkImage key, StreamController<ImageChunkEvent> chunkEvents,) async { try { 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); //将网络返回的 response 信息,转换成内存中的 Uint8List bytes。这里面有解压 gzip 的逻辑。 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'); return PaintingBinding.instance.instantiateImageCodec(bytes); } finally { chunkEvents.close(); }}将网络返回的response信息,转换成内存中的 Uint8List bytes,最终返回一个实例化图像编解码器对象Codec,此处 Codec 可以移步到 painting.dart 文件的 _instantiateImageCodec 看出来它是调用了native方法去处理了。
MultiFrameImageStreamCompleter
这个对象就是 ImageStreamCompleter 的具体实现,见名知意,多帧图片流管理,作用管理图像帧的解码和调度。
这个类处理两种类型的帧:
图像帧 :动画图像的图像帧。
app 帧 :Flutter 引擎绘制到屏幕的帧,显示到应用程序 GUI。
这就不贴所有代码了,在 image_stream.dart 文件中 可见 class MultiFrameImageStreamCompleter。 MultiFrameImageStreamCompleter({ @required Future<ui.Codec> codec, @required double scale, Stream<ImageChunkEvent> chunkEvents, InformationCollector informationCollector,}) : assert(codec != null), _informationCollector = informationCollector, _scale = scale { codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) { .......... });_handelCodecReady
这里 codec 异步回调次方法 void _handleCodecReady(ui.Codec codec) { _codec = codec; if (hasListeners) { _decodeNextFrameAndSchedule(); }}_decodeNextFrameAndSchedule
codec 解码获取到图片的帧数,判断图片是只有一帧的话,就是png、jpg这样静态图片。 Future<void> _decodeNextFrameAndSchedule() async { try { _nextFrame = await _codec.getNextFrame(); } catch (exception, stack) { ........ return; } if (_codec.frameCount == 1) { // 此处判断图片是只有一帧的逻辑. _emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale)); return; } _scheduleAppFrame();} void _scheduleAppFrame() { if (_frameCallbackScheduled) { return; } _frameCallbackScheduled = true; SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);} _emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));void _emitFrame(ImageInfo imageInfo) { setImage(imageInfo); _framesEmitted += 1;} @protected void setImage(ImageInfo image) { _currentImage = image; if (_listeners.isEmpty) return; // 复制一份以允许并发修改。final List<ImageStreamListener> localListeners =List<ImageStreamListener>.from(_listeners); for (ImageStreamListener listener in localListeners) { try { listener.onImage(image, false); } catch (exception, stack) { .......... } }} setImage 核心逻辑就是通知所有注册上的监听,表示图片发生了变化可以更新啦。此时我们回到 开始提到的_ImageState 类中 didChangeDependencies 方法调用的 _listenToStream 方法,最终调用方法 _handleImageFrame ,改变 图片信息 _imageInfo 和 图片帧数变化 _frameNumber ,最终执行 setState(() {}) 来刷新了 UI。 void _listenToStream() { if (_isListeningToStream) return; _imageStream.addListener(_getListener()); _isListeningToStream = true;} ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) { loadingBuilder ??= widget.loadingBuilder; return ImageStreamListener( _handleImageFrame, onChunk: loadingBuilder == null ? null : _handleImageChunk, );} void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) { setState(() { _imageInfo = imageInfo; _loadingProgress = null; _frameNumber = _frameNumber == null ? 0 : _frameNumber + 1; _wasSynchronouslyLoaded |= synchronousCall; });} 这样就结束了一个网络图片的加载过程。 此处应该有流程图就更加简洁明了的表达啦。总结
图片加载显示的方式 framework 提供了多种方式,我们就图片网络加载进行了分析。 从源码角度对网络图片加载过程有了大致的了解。 发现的可以优化点,这里先提出来优化的点:1、看到网络图片只是在 ImageCache 管理类中进行了内存缓存,当应用进程重新启动后还是要重新下载图片,此处是可以优化的,比如保存到本地磁盘外存。
2、拿到图片加载到内存里面的时候,是否有对图片进行压缩处理,这种处理最好既适应当前平台又不过分地改变图片的清晰度。
期待下一篇的迭代优化点。推荐阅读