同名公众号:小武码码码
在 Flutter 项目开发中,我经常需要处理大量的图片加载需求。曾在开发一个电商 App 时,发现性能问题非常突出,商品列表滑动卡顿,内存占用高。我意识到,高效地加载和缓存图片是优化用户体验的关键。
在对比多个图片加载库后,我选择了 cached_network_image ^3.2.3 这个插件。下面我将从源码角度,分析它是如何帮助解决图片加载难题的。
CachedNetworkImage 与 Image 的关系
CachedNetworkImage 可以看做是 Image 的升级版,它在 Image 的基础上增加了网络图片的缓存能力。我们先看下它的 build 方法:
Widget build(BuildContext context) {
return OctoImage(
image: _image,
// ... 省略其他参数
);
}
可以看到,CachedNetworkImage 内部是通过 OctoImage 来加载图片的。继续追踪 OctoImage 的实现,最终发现它调用了 ImageHandler:
Widget build(BuildContext context) {
return Image(
key: ValueKey(image),
image: image,
// ... 省略其他参数
);
}
所以,整个调用链是:
CachedNetworkImage -> OctoImage -> ImageHandler -> Image
图片加载流程解析
梳理清楚了 CachedNetworkImage 与 Image 的关系,接下来我们重点分析图片加载的完整流程。
1. 注册图片流监听
在 Image 中,有个关键的方法 _resolveImage
:
void _resolveImage() {
final ImageStream newStream = provider.resolve(createLocalImageConfiguration(
context,
size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null,
));
_updateSourceStream(newStream);
}
它主要做了两件事:
- 将 ImageProvider 和 ImageStream 关联起来。
- 注册 ImageStreamListener 监听图片加载结果。
_updateSourceStream 方法中,会创建 ImageStreamListener 对象:
ImageStreamListener _getListener() {
_imageStreamListener = ImageStreamListener(
_handleImageFrame,
onChunk: _handleImageChunk,
onError: (Object error, StackTrace? stackTrace) {
// 错误处理
},
);
return _imageStreamListener!;
}
当图片加载完成后,会回调 _handleImageFrame,此时就可以拿到 ImageInfo 对象,然后 setState 触发 Image 重建,完成图片渲染。
2. 获取和缓存图片数据
CachedNetworkImage 内部使用了自定义的 ImageProvider - CachedNetworkImageProvider。它覆写了 ImageProvider 的 resolve 方法:
ImageStream resolve(ImageConfiguration configuration) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key),
scale: key.scale,
).asStream();
}
resolve 方法返回一个 ImageStream,然后交给 CachedNetworkImage 内部处理。
在 _loadAsync 中,首先会通过 CacheManager 查找缓存图片:
final cacheFile = await _cacheManager.getSingleFile(key.url);
if (cacheFile != null) {
// 缓存命中
final bytes = await cacheFile.readAsBytes();
return await _decode(bytes);
}
如果缓存未命中,才会发起网络请求,下载图片数据:
final response = await _httpClient.get(Uri.parse(key.url));
final bytes = await consolidateHttpClientResponseBytes(response);
if (bytes.lengthInBytes == 0) {
// 处理空响应
throw Exception('NetworkImage is an empty file: $url');
}
await _cacheManager.putFile(cacheFile, bytes);
return await _decode(bytes);
这里用到了 http 库获取图片的字节数据,然后写入文件缓存,并进行解码。解码的过程在 _decode 函数中:
Future<Codec> _decode(Uint8List bytes) async {
return await PaintingBinding.instance.instantiateImageCodec(bytes);
}
instantiateImageCodec 会对图片字节数据进行解码,生成 ui.Codec 对象。解码后的结果也会被缓存起来,提高下次加载的速度。
优化效果与思考
通过引入 cached_network_image,在加载网络图片时,我的 App 性能获得了显著提升。列表滑动不再卡顿,内存使用也下降了。
主要原因是 cached_network_image 采用了两级缓存:
- 内存中通过 ImageCache 缓存 ImageStreamCompleter 和 ui.Codec。
- 磁盘上通过文件缓存网络请求结果。
在加载图片时,会优先从内存缓存中查找,避免重复解码。如果内存缓存未命中,则读取磁盘文件,避免重复网络请求。只有在两级缓存都未命中的情况下,才会发起真正的网络请求,下载图片数据。
当然,cached_network_image 目前也还有一些可以优化的地方:
- 支持 WebP 等其他格式的图片,提高图片压缩率。
- 支持渐进式、交错式图片加载,不需要等整张图片加载完成再显示。
- 考虑 SVG 等矢量图的加载和缓存。
总的来说,通过解决图片加载问题,cached_network_image 为我的 App 提供了巨大的性能优化空间。结合智能的缓存管理和占位图展示,它极大地改善了用户体验。希望这篇源码分析能为你在实际项目中处理图片加载问题提供一些思路。