Flutter图片缓存 | Image.network源码分析
原创: 郭海生 京东技术
原文地址:https://mp.weixin.qq.com/s/W5iu3VsNuvIygFbKM-giqA
随着手机设备硬件水平的飞速发展,用户对于图片的显示要求也越来越高,稍微处理不好就会容易造成内存溢出等问题。所以我们在使用Image的时候,建立一个图片缓存机制已经是一个常态。Android目前提供了很丰富的图片框架,像ImageLoader、Glide、Fresco等。对于Flutter而言,为了探其缓存机制或者定制自己的缓存框架,特从其Image入手进行突破。
>>>>
Image 的用法
Image是Flutter里提供的显示图片的控件,类似Android里ImageView,不过其用法有点类似Glide等图片框架。
我们先看Image的用法。Flutter对Image控件提供了多种构造函数:
new Image 用于从ImageProvider获取图像
new Image.asset 用于使用key从AssetBundle获取图像
new Image.network 用于从URL地址获取图像
new Image.file 用于从File获取图像
我们只分析Image.network源码,分析理解完这个之后,其他的也是一样的思路。
我们先从Image.network的用法入手:显示一个网络图片很简单,直接通过Image.network携带一个url参数即可。
范例:
return new Scaffold(
appBar: new AppBar(
title: new Text("Image from Network"),
),
body: new Container(
child: new Column(
children: <Widget>[
// Load image from network
new Image.network(
'https://flutter.io/images/flutter-mark-square-100.png'),
],
)),
);
>>>>
Image结构UML类图
我们首先看一下Image的UML类图:

可以看到Image的框架结构还是有点儿复杂的,在你只调用一行代码的情况下,其实Flutter为你做了很多工作。
初步梳理下每个类概念:
-
StatefulWidget就是有状态的Widget,是展示在页面上的元素。
-
Image继承于StatefulWidget,是来显示和加载图片。
-
State控制着StatefulWidget状态改变的生命周期,当Widget被创建、Widget配置信息改变或者Widget被销毁等等,State的一系列方法会被调用。
-
_ImageState继承于State,处理State生命周期变化以及生成Widget。
-
ImageProvider提供加载图片的入口,不同的图片资源加载方式不一样,只要重写其load方法即可。同样,缓存图片的key值也有其生成。
-
NetWorkImage负责下载网络图片的,将下载完成的图片转化成ui.Codec对象交给ImageStreamCompleter去处理解析。
-
ImageStreamCompleter就是逐帧解析图片的。
-
ImageStream是处理Image Resource的,ImageState通过ImageStream与ImageStreamCompleter建立联系。ImageStream里也存储着图片加载完毕的监听回调。
-
MultiFrameImageStreamCompleter就是多帧图片解析器。
先把Image的框架结构了解一下,有助于下面我们更加清晰地分析代码。
>>>>
源码分析
我们看下Image.network都做了什么:
-
class Image extends StatefulWidget { -
Image.network(String src, { -
Key key, -
double scale = 1.0, -
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, -
Map<String, String> headers, -
}) : image = new NetworkImage(src, scale: scale, headers: headers), -
assert(alignment != null), -
assert(repeat != null), -
assert(matchTextDirection != null), -
super(key: key); -
......
我们看到Image是一个StatefulWidget对象,可以直接放到Container或者Column等容器里,其属性解释如下:
-
width:widget的宽度
-
height:widget的高度
-
color:与colorBlendMode配合使用,将此颜色用BlendMode方式混合图片
-
colorBlendMode:混合模式算法
-
fit:与android:scaletype一样,控制图片如何resized/moved来匹对Widget的size
-
alignment:widget对齐方式
-
repeat:如何绘制未被图像覆盖的部分
-
centerSlice:支持9patch,拉伸的中间的区域
-
matchTextDirection:绘制图片的方向:是否从左到右
-
gaplessPlayback:图片变化的时候是否展示老图片或者什么都不展示
-
headers:http请求头
-
image:一个ImageProvide对象,在调用的时候已经实例化,这个类主要承担了从网络加载图片的功能。它是加载图片的最重要的方法,不同的图片加载方式(assert文件加载、网络加载等等)也就是重写ImageProvider加载图片的方法(load())。
Image是一个StatefulWidget对象,所以我们看它的State对象:
-
class _ImageState extends State<Image> { -
ImageStream _imageStream; -
ImageInfo _imageInfo; -
bool _isListeningToStream = false; -
} -
-
class ImageStream extends Diagnosticable { -
ImageStreamCompleter get completer => _completer; -
ImageStreamCompleter _completer; -
-
List<ImageListener> _listeners; -
-
/// Assigns a particular [ImageStreamCompleter] to this [ImageStream]. -
void setCompleter(ImageStreamCompleter value) { -
assert(_completer == null); -
_completer = value; -
print("setCompleter:::"+(_listeners==null).toString()); -
if (_listeners != null) { -
final List<ImageListener> initialListeners = _listeners; -
_listeners = null; -
initialListeners.forEach(_completer.addListener); -
} -
} -
-
/// Adds a listener callback that is called whenever a new concrete [ImageInfo] -
void addListener(ImageListener listener) { -
if (_completer != null) -
return _completer.addListener(listener); -
_listeners ??= <ImageListener>[]; -
_listeners.add(listener); -
} -
-
/// Stop listening for new concrete [ImageInfo] objects. -
void removeListener(ImageListener listener) { -
if (_completer != null) -
return _completer.removeListener(listener); -
assert(_listeners != null); -
_listeners.remove(listener); -
} -
}
我们对_ImageState的两个属性对象解释一下:
-
ImageStream是处理Image Resource的,ImageStream里存储着图片加载完毕的监听回调,ImageStreamCompleter也是其成员,这样ImageStream将图片的解析流程交给了ImageStreamCompleter去处理。
-
ImageInfo包含了Image的数据源信息:width和height以及ui.Image。 将ImageInfo里的ui.Image设置给RawImage就可以展示了。RawImage就是我们真正渲染的对象,是显示ui.Image的一个控件,接下来我们会看到。
我们知道State的生命周期,首先State的initState执行,然后didChangeDependencies会执行,我们看到ImageState里没有重写父类的initState,那我们看其didChangeDependencies():
-
@override -
void didChangeDependencies() { -
_resolveImage(); -
-
if (TickerMode.of(context)) -
_listenToStream(); -
else -
_stopListeningToStream(); -
-
super.didChangeDependencies(); -
}
>>>>
_resolveImage方法解析
我们看到首先调用了resolveImage(),我们看下resolveImage方法:
-
void _resolveImage() { -
final ImageStream newStream = -
widget.image.resolve(createLocalImageConfiguration( -
context, -
size: widget.width != null && widget.height != null ? new Size(widget.width, widget.height) : null -
)); -
assert(newStream != null); -
_updateSourceStream(newStream); -
}
这个方法是处理图片的入口。widget.image这个就是上面的创建的NetworkImage对象,是个ImageProvider对象,调用它的resolve并且传进去默认的ImageConfiguration。 我们看下resolve方法,发现NetworkImage没有,果不其然,我们在其父类ImageProvider找到了:
-
ImageStream resolve(ImageConfiguration configuration) { -
assert(configuration != null); -
final ImageStream stream = new ImageStream(); -
T obtainedKey; -
obtainKey(configuration).then<void>((T key) { -
obtainedKey = key; -
stream.setCompleter(PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key))); -
}).catchError( -
(dynamic exception, StackTrace stack) async { -
FlutterError.reportError(new FlutterErrorDetails( -
exception: exception, -
stack: stack, -
library: 'services library', -
context: 'while resolving an image', -
silent: true, // could be a network error or whatnot -
informationCollector: (StringBuffer information) { -
information.writeln('Image provider: $this'); -
information.writeln('Image configuration: $configuration'); -
if (obtainedKey != null) -
information.writeln('Image key: $obtainedKey'); -
} -
)); -
return null; -
} -
); -
return stream; -
}
我们看到这个方法创建了ImageStream并返回,调用obtainKey返回一个携带NetworkImage的future,以后会作为缓存的key使用,并且调用ImageStream的setCompleter的方法:
-
void setCompleter(ImageStreamCompleter value) { -
assert(_completer == null); -
_completer = value; -
if (_listeners != null) { -
final List<ImageListener> initialListeners = _listeners; -
_listeners = null; -
initialListeners.forEach(_completer.addListener); -
} -
}
这个方法就是给ImageStream设置一个ImageStreamCompleter对象,每一个ImageStream对象只能设置一次,ImageStreamCompleter是为了辅助ImageStream解析和管理Image图片帧的,并且判断是否有初始化监听器,可以做一些初始化回调工作。 我们继续看下PaintingBinding.instance.imageCache.putIfAbsent方法:
-
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader()) { -
assert(key != null); -
assert(loader != null); -
ImageStreamCompleter result = _pendingImages[key]; -
// 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; -
} -
result = loader(); -
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 = new _CachedImage(result, imageSize); -
_currentSizeBytes += imageSize; -
_pendingImages.remove(key); -
_cache[key] = image; -
result.removeListener(listener); -
_checkCacheSize(); -
} -
if (maximumSize > 0 && maximumSizeBytes > 0) { -
_pendingImages[key] = result; -
result.addListener(listener); -
} -
return result; -
}
这个是Flutter默认提供的内存缓存api的入口方法,这个方法会先通过key获取之前的ImageStreamCompleter对象,这个key就是NetworkImage对象,当然我们也可以重写obtainKey方法自定义key,如果存在则直接返回,如果不存在则执行load方法加载ImageStreamCompleter对象,并将其放到首位(最少最近使用算法)。
也就是说ImageProvider已经实现了内存缓存:默认缓存图片的最大个数是1000,默认缓存图片的最大空间是10MiB。 第一次加载图片肯定是没有缓存的,所以我们看下loader方法,我们看到ImageProvider是空方法,我们去看NetWorkImage,按照我们的预期确实在这里:
-
@override -
ImageStreamCompleter load(NetworkImage key) { -
return new MultiFrameImageStreamCompleter( -
codec: _loadAsync(key), -
scale: key.scale, -
informationCollector: (StringBuffer information) { -
information.writeln('Image provider: $this'); -
information.write('Image key: $key'); -
} -
); -
} -
//网络请求加载图片的方法 -
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 new Exception('HTTP request failed, statusCode: ${response?.statusCode}, $resolved'); -
-
final Uint8List bytes = await consolidateHttpClientResponseBytes(response); -
if (bytes.lengthInBytes == 0) -
throw new Exception('NetworkImage is an empty file: $resolved'); -
-
return await ui.instantiateImageCodec(bytes); -
}
这个方法为我们创建了一个MultiFrameImageStreamCompleter对象,根据名字我们也能知道它继承于ImageStreamCompleter。还记得ImageStreamCompleter是做什么的吗,就是辅助ImageStream管理解析Image的。
参数解析:
-
_loadAsync()是请求网络加载图片的方法
-
scale是缩放系数
-
informationCollector是信息收集对象的,提供错误或者其他日志用
MultiFrameImageStreamCompleter是多帧的图片处理加载器,我们知道Flutter的Image支持加载gif,通过MultiFrameImageStreamCompleter可以对gif文件进行解析:
-
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) { -
FlutterError.reportError(new FlutterErrorDetails( -
exception: error, -
stack: stack, -
library: 'services', -
context: 'resolving an image codec', -
informationCollector: informationCollector, -
silent: true, -
)); -
}); -
} -
-
ui.Codec _codec; -
final double _scale; -
final InformationCollector _informationCollector; -
ui.FrameInfo _nextFrame;
我们看到MultiFrameImageStreamCompleter拿到loadAsync返回的codec数据对象,通过handleCodecReady来处理数据,然后会调用_decodeNextFrameAndSchedule方法:
-
Future<Null> _decodeNextFrameAndSchedule() async { -
try { -
_nextFrame = await _codec.getNextFrame(); -
} catch (exception, stack) { -
FlutterError.reportError(new FlutterErrorDetails( -
exception: exception, -
stack: stack, -
library: 'services', -
context: 'resolving an image frame', -
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(new ImageInfo(image: _nextFrame.image, scale: _scale)); -
return; -
} -
SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame); -
}
通过codec.getNextFrame()去拿下一帧,对于静态的图片frameCount是1,直接用ImageInfo组装image,交给emitFrame方法,这个方法里会调用setImage,如下:
-
@protected -
void setImage(ImageInfo image) { -
_current = image; -
if (_listeners.isEmpty) -
return; -
final List<ImageListener> localListeners = new List<ImageListener>.from(_listeners); -
for (ImageListener listener in localListeners) { -
try { -
listener(image, false); -
} catch (exception, stack) { -
_handleImageError('by an image listener', exception, stack); -
} -
} -
}
setImage方法就是设置当前的ImageInfo并检查监听器列表,通知监听器图片已经加载完毕可以刷新UI了。
对于动图来说就是就是交给SchedulerBinding逐帧的去调用setImage,通知UI刷新,代码就不贴了,有兴趣的可以自行查看下。 至此resolveImage调用流程我们算是讲完了,接下来我们看listenToStream。
>>>>
_listenToStream方法解析
我们继续分析didChangeDependencies方法,这个方法里会判断TickerMode.of(context)的值,这个值默认是true,和AnimationConrol有关,后续可以深入研究。然后调用_listenToStream()。 我们看下这个方法:
-
void _listenToStream() { -
if (_isListeningToStream) -
return; -
_imageStream.addListener(_handleImageChanged); -
_isListeningToStream = true; -
}
这个就是添加图片加载完毕的回调器。还记得吗,当图片加载并解析完毕的时候,MultiFrameImageStreamCompleter的setImage方法会调用这里传过去的回调方法。我们看下这里回调方法里做了什么:
-
void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) { -
setState(() { -
_imageInfo = imageInfo; -
}); -
}
很显然就是拿到上层传过来ImageInfo,调用setState更新UI 我们看下build方法:
-
Widget build(BuildContext context) { -
return new RawImage( -
image: _imageInfo?.image, -
width: widget.width, -
height: widget.height, -
scale: _imageInfo?.scale ?? 1.0, -
color: widget.color, -
colorBlendMode: widget.colorBlendMode, -
fit: widget.fit, -
alignment: widget.alignment, -
repeat: widget.repeat, -
centerSlice: widget.centerSlice, -
matchTextDirection: widget.matchTextDirection, -
); -
}
就是用imageInfo和widget的信息来封装RawImage,RawImage是RenderObjectWidget对象,是应用程序真正渲染的对象,将咱们的图片显示到界面上。
>>>>
总结
梳理下流程:
-
从入口开始,Image是继承于StatefulWidget,它为咱们实现好了State:_ImageState,并且提供了一个已经实例化的NetWorkImage对象,它是继承于ImageProvider对象的。
-
ImageState创建完之后,ImageState通过调用resolveImage(),resolveImage()又会调用ImageProvider的resolve()方法返回一个ImageStream对象。_ImageState也注册了监听器给ImageStream,当图片下载完毕后会执行回调方法。
-
然后在ImageProvider的resolve()方法里不仅创建了ImageStream还设置了ImageStream的setComplete方法去设置ImageStreamCompleter,在这里去判断是否有缓存,没有缓存就调用load方法去创建ImageStreamCompleter并且添加监听器为了执行加载完图片之后的缓存工作。ImageStreamCompleter是为了解析已经加载完成的Image的。
-
NetWorkImage实现了ImageProvider的load方法,是真正下载图片的地方,创建了MultiFrameImageStreamCompleter对象,并且调用_loadAsync去下载图片。当图片下载完成后就调用UI的回调方法,通知UI刷新。
>>>>
最后
至此,对Image.network的源码分析到这里也结束了,你也可以返回去看下Image的结构图了。怎么样,分析完之后是不是对Flutter加载网络图片的流程已经很了解了,也找到了Flutter缓存的突破口,Flutter自身已经提供了内存缓存(虽然不太完美),接下来你就可以添加你的硬盘缓存或者定制你的图片框架了。

本文深入分析了Flutter中Image.network的源码,详细解析了图片加载、缓存及UI更新的流程,帮助读者理解Flutter的图片处理机制。
464

被折叠的 条评论
为什么被折叠?



