其他
Flutter系列之图片加载详解
PS:自律实践起来真的没有想象的那么简单。
ImageProvider
,ImageProvider
是一个抽象类,具体获取图片由子类实现,本文将从如下几个方面学习 Flutter 中的图片加载:图片加载
图片预加载
图片缓存
清除图片缓存
图片加载进度监听
加载图片案例
图片加载
Image.file(File file,{...});
Image.asset(String name,{...});
Image.memeory(Uint8List bytes,{...});
Image.network()
源代码如下:// ...
}) : image = NetworkImage(src, scale: scale, headers: headers),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
super(key: key);
Image.network
生成 Image 的时候创建了 NetworkImage
,NetworkImage
类是 ImageProvider
的子类,ImageProvider
是一个抽象类,里面提供了解析图片资源的 resolve
方法、将图片缓存移除去的 evict
方法以及加载图片的抽象方法 load
等,load
方法由子类具体实现,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
的实现类 NetworkImage
的 load
方法实现如下: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();
}
}
native 'instantiateImageCodec';
ImageStreamCompleter
,这个 ImageStreamCompleter
在 resolve
方法中被设置给 ImageStream
,resolve
方法返回这个 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 != null) return _completer.addListener(listener);
_listeners ??= <ImageStreamListener>[];
_listeners.add(listener);
}
/// 移除图片加载监听器
void removeListener(ImageStreamListener listener) {
if (_completer != null) return _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);
// ...
}
}
ImageStream
,resolve
方法会在 Image 组件的对应生命周期方法中被调用,如 didChangeDependencies
、didUpdateWidget
等方法,在组件构建的时候会创建 RawImage
,继续跟踪源码则是 RenderImage
,在其 paint
方法中调用了 paintImage
方法,里面通过 canvas 完成图片配置信息的绘制。图片预加载
precacheImage
方法来对图片进行预加载,也就是将图片提前添加到缓存中,当需要加载图片的时候就直接从缓存中获取,precacheImage
方法还是通过 ImageProvider
的 resolve
方法解析图片资源并将其添加到图片缓存中,其方法源码如下: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 _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 != null) return 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
方法,该方法是缓存的入口,如果已经有缓存则从缓存中获取,否则调用对应 ImageProvider
的 load
方法去加载图片,并将其添加到缓存中。清除图片缓存
PaintingBinding
的单例获取到 ImageCache
调用其 ckear
方法,如下:_clearCache(BuildContext context) {
PaintingBinding.instance.imageCache.clear();
Toast.show("缓存已清除", context);
}
图片加载进度监听
resolve
方法返回对应图片的 ImageStream
,我们可以通过这个 ImageStream
设置图片加载监听,实际上添加的就是 ImageStreamListener
,如下: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 == null) return 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,
);
},
),
);
}
}
加载图片案例
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);
}
}