在Flutter中有个图片组件:Image
,通常会使用它的Image.network(src)
、Image.file(src)
、Image.asset(src)
来加载图片。
下面是Image
的普通构造方法:
const Image({
super.key,
required this.image,
this.frameBuilder,
this.loadingBuilder,
this.errorBuilder,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width,
this.height,
this.color,
this.opacity,
this.colorBlendMode,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.centerSlice,
this.matchTextDirection = false,
this.gaplessPlayback = false,
this.isAntiAlias = false,
this.filterQuality = FilterQuality.low,
})
从它的构造方法可以看出Image组件有个必传参数image,它是ImageProvider类型。ImageProvider是个抽象类,定义了图片数据获取和加载的相关接口。它的主要职责有两个:
- 1.提供图片数据源;
- 2.缓存图片;
ImageProvider
抽象类:
abstract class ImageProvider<T extends Object> {
const ImageProvider();
ImageStream resolve(ImageConfiguration configuration) {
...
}
ImageStream createStream(ImageConfiguration configuration) {
return ImageStream();
}
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
...
}
Future<bool> evict({ ImageCache? cache, ImageConfiguration configuration = ImageConfiguration.empty }) async {
...
}
Future<T> obtainKey(ImageConfiguration configuration);
}
从上面源码中可以发现图片的加载和解析是由ImageProvider
来完成的,具体说是由它的子类实现的。ImageProvider
派生了很多子类,例如NetworkImage
类和AssetImage
类,NetworkImage
是从网络来加载图片数据,而AssetImage
则是从安装包里的资源文件中加载。
图片的加载
在ImageProvider中提供了一个load()
方法,它是一个用于加载图片数据源的接口,不同的数据源的加载方式不同。
加载网络图片是用Image.network()
,对应的ImageProvider是NetworkImage
类,它实现了load()方法:
ImageStreamCompleter load(FileImage key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, null, decode),
scale: key.scale,
debugLabel: key.file.path,
informationCollector: () => <DiagnosticsNode>[
ErrorDescription('Path: ${file.path}'),
],
);
}
- load方法的返回值类型是ImageStreamCompleter,它是一个抽象类,定义了管理图片加载过程的一些接口,Image Widget中是通过它来监听图片加载状态的;
- MultiFrameImageStreamCompleter是ImageStreamCompleter的一个子类,实现该类,可以快速的创建出一个ImageStreamCompleter实例;
MultiFrameImageSteamCompleter有个codec参数,源码中是用调用了_loadAsync()
方法,方法的实现如下:
Future<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents,
image_provider.DecoderBufferCallback? decode,
image_provider.DecoderCallback? decodeDepreacted,
) 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) {
await response.drain<List<int>>(<int>[]);
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 (decode != null) {
final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
return decode(buffer);
} else {
assert(decodeDepreacted != null);
return decodeDepreacted!(bytes);
}
}
通过源码可以发现_loadAsync()
方法负责下载图片,并调用decode()
方法对下载的图片数据进行解码。
图片的缓存
图片的缓存的关键方法是:obtainKey(ImageConfiguration)
该方法主要是为了配合实现图片缓存,ImageProvider从数据源加载完数据后,会在全局的ImageCache中缓存图片数据,而图片数据缓存是一个Map,而Map的key便是调用此方法的返回值,不同的key代表不同的图片数据缓存。
resolve
方法是ImageProvider
暴露给Image的主入口方法,它接收一个ImageConfiguration参数,返回ImageStream。
ImageStream resolve(ImageConfiguration configuration) {
assert(configuration != null);
final ImageStream stream = createStream(configuration);
_createErrorHandlerAndKey(
configuration,
(T key, ImageErrorListener errorHandler) {
resolveStreamForKey(configuration, stream, key, errorHandler);
},
...
);
return stream;
}
ImageConfiguration:包含图片和设备的相关信息。内部会调用_createErrorHandlerAndKey
来加载key并创建错误处理函数。
resolveStreamForKey
方法:
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
if (stream.completer != null) {
//缓存逻辑
final ImageStreamCompleter? completer = PaintingBinding.instance.imageCache.putIfAbsent(
key,
() => stream.completer!,
onError: handleError,
);
assert(identical(completer, stream.completer));
return;
}
//缓存逻辑
final ImageStreamCompleter? completer = PaintingBinding.instance.imageCache.putIfAbsent(
key,
() => loadBuffer(key, PaintingBinding.instance.instantiateImageCodecFromBuffer),
onError: handleError,
);
if (completer != null) {
stream.setCompleter(completer);
}
}
在resolve方法中会调用resolveStreamForKey,其中PaintingBinding.instance.imageCache
是ImageCache的一个实例,它是PaintingBinding的一个属性,并且PaintingBinding.instance和imageCache都是单例,所以图片缓存是全局的,统一由PaintingBinding.instance.imageCache
来管理。
ImageCache缓存的具体实现
ImageCache的定义:
const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
class ImageCache {
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
final Map<Object, _LiveImage> _liveImages = <Object, _LiveImage>{};
int get maximumSize => _maximumSize;
int _maximumSize = _kDefaultSize;
...
}
ImageCache中有三个缓存池:
- _pendingImages:用来存放正在加载和解码的图片,当图片加载和解码完成后,ImageCache会自动移除_pendingImages相应的Entry;
- _cache:用来存储所有加载过的图片,如果图片缓存的数量和内存占用大小没有超过ImageCache的上限,_cache就会一直保留Cache Entry,如果超过了则会按LRU进行释放;
- _liveImages:用来存放使用中的图片,当Image Widget移除或者更换图片,或者Image Widget自身被移除,ImageCache会从_liveImages移除相应Entry;
只有ImageCache从所有缓存池都释放了同一张图片的Entry,该图片才算在内存中真正释放。
图片缓存的Key怎么生成
因为Map中相同key的值会被覆盖,也就是说key是图片缓存的唯一标识,只要key不同,那么图片数据就会分布缓存。那么图片的唯一标识是什么呢?从源码中可以看到ImageProvider.obtainKey()
方法,图片在缓存时所使用的key就是通过这个方法生成的,并且ImageProvider的子类对这个方法进行了重写。
这就意味着不同的ImageProvider对key的定义逻辑是不同的。NetworkImage的obtainKey()方法:
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
return SynchronousFuture<NetworkImage>(this);
}
创建了一个SynchronousFuture,然后将自身返回,所以对比key的时候,看操作符==
就可以了:
operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is NetworkImage && other.url == url && other.scale == scale;
}
bool
对于网络图片来说,key实际上是url+缩放比例。所以两张图片的url和scale都相同,那么他们在内存里面就只会缓存一份。
设置缓存大小
ImageCache类中有默认的缓存大小:
const int _kDefaultSize = 1000;//最多1000张
const int _kDefaultSizeBytes = 100 << 20; //最大 100 MiB
我们也可以通过如下代码去设置自定义的缓存上限:
PaintingBinding.instance.imageCache.maximumSize=500; //最多500张
PaintingBinding.instance.imageCache.maximumSizeBytes = 50 << 20; //最大50M