一、问题:
Flutter
原有的图片缓存机制,是通过PaintingBinding.instance!.imageCache
来管理缓存的,这个缓存缓存到的是内存中,每次重新打开APP
或者缓存被清理都会再次进行网络请求,大图片加载慢不友好,且增加服务器负担。
二、思路:
1、查看FadeInImage.assetNetwork
、Image.network
等几个网络请求的命名构造方法,初始化了ImageProvider
。
FadeInImage.assetNetwork({
Key key,
@required String placeholder,
this.placeholderErrorBuilder,
@required String image,
this.imageErrorBuilder,
AssetBundle bundle,
double placeholderScale,
double imageScale = 1.0,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700),
this.fadeInCurve = Curves.easeIn,
this.width,
this.height,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
int placeholderCacheWidth,
int placeholderCacheHeight,
int imageCacheWidth,
int imageCacheHeight,
}) : assert(placeholder != null),
assert(image != null),
placeholder = placeholderScale != null
? ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale))
: ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, AssetImage(placeholder, bundle: bundle)),
assert(imageScale != null),
assert(fadeOutDuration != null),
assert(fadeOutCurve != null),
assert(fadeInDuration != null),
assert(fadeInCurve != null),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale)),
super(key: key);
Image.network(
String src, {
Key key,
double scale = 1.0,
this.frameBuilder,
this.loadingBuilder,
this.errorBuilder,
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,
this.isAntiAlias = false,
Map headers,
int cacheWidth,
int cacheHeight,
}) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
assert(cacheWidth == null || cacheWidth > 0),
assert(cacheHeight == null || cacheHeight > 0),
assert(isAntiAlias != null),
super(key: key);
其中:
image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),
,使用ImageProvider
类型的NetworkImage
创建了ImageProvider
类型的ResizeImage
。
而NetworkImage
是一个继承ImageProvider
的抽象类。
abstract class NetworkImage extends ImageProvider {
/// Creates an object that fetches the image at the given URL.
///
/// The arguments [url] and [scale] must not be null.
const factory NetworkImage(String url, { double scale, Map? headers }) = network_image.NetworkImage;
/// The URL from which the image will be fetched.
String get url;
/// The scale to place in the [ImageInfo] object of the image.
double get scale;
/// The HTTP headers that will be used with [HttpClient.get] to fetch image from network.
///
/// When running flutter on the web, headers are not used.
Map? get headers;
@override
ImageStreamCompleter load(NetworkImage key, DecoderCallback decode);
}
其中工厂方法给了一个值,const factory NetworkImage(String url, { double scale, Map
进入network_image.NetworkImage
,到了_network_image_io.dart
文件。
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'binding.dart';
import 'debug.dart';
import 'image_provider.dart' as image_provider;
import 'image_stream.dart';
/// The dart:io implementation of [image_provider.NetworkImage].
@immutable
class NetworkImage extends image_provider.ImageProvider implements image_provider.NetworkImage {
/// Creates an object that fetches the image at the given URL.
///
/// The arguments [url] and [scale] must not be null.
const NetworkImage(this.url, { this.scale = 1.0, this.headers })
: assert(url != null),
assert(scale != null);
@override
final String url;
@override
final double scale;
@override
final Map? headers;
@override
Future obtainKey(image_provider.ImageConfiguration configuration) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
// Ownership of this controller is handed off to [_loadAsync]; it is that
// method's responsibility to close the controller's stream when the image
// has been loaded or an error is thrown.
final StreamController chunkEvents = StreamController();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key as NetworkImage, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
debugLabel: key.url,
informationCollector: () {
return [
DiagnosticsProperty('Image provider', this),
DiagnosticsProperty('Image key', key),
];
},
);
}
// Do not access this field directly; use [_httpClient] instead.
// We set `autoUncompress` to false to ensure that we can trust the value of
// the `Content-Length` HTTP header. We automatically uncompress the content
// in our call to [consolidateHttpClientResponseBytes].
static final HttpClient _sharedHttpClient = HttpClient()..autoUncompress = false;
static HttpClient get _httpClient {
HttpClient client = _sharedHttpClient;
assert(() {
if (debugNetworkImageHttpClientProvider != null)
client = debugNetworkImageHttpClientProvider!();
return true;
}());
return client;
}
Future _loadAsync(
NetworkImage key,
StreamController chunkEvents,
image_provider.DecoderCallback decode,
) 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) {
// The network may be only temporarily unavailable, or the file will be
// added on the server later. Avoid having future calls to resolve
// fail to check the network again.
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');
return decode(bytes);
} catch (e) {
// Depending on where the exception was thrown, the image cache may not
// have had a chance to track the key in the cache at all.
// Schedule a microtask to give the cache a chance to add the key.
scheduleMicrotask(() {
PaintingBinding.instance!.imageCache!.evict(key);
});
rethrow;
} finally {
chunkEvents.close();
}
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is NetworkImage
&& other.url == url
&& other.scale == scale;
}
@override
int get hashCode => ui.hashValues(url, scale);
@override
String toString() => '${objectRuntimeType(this, 'NetworkImage')}("$url", scale: $scale)';
}
对其中的_loadAsync
方法进行修改,实现图片的本地存储和获取,即可。
三、实现
1、新建一个文件my_local_cache_network_image.dart
,将_network_image_io.dart
内容复制过来,进行修改。
2、全部文件内容如下(非空安全版本):
import 'dart:async';
import 'dart:convert' as convert;
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
/// The dart:io implementation of [image_provider.NetworkImage].
@immutable
class MyLocalCacheNetworkImage extends ImageProvider implements NetworkImage {
/// Creates an object that fetches the image at the given URL.
///
/// The arguments [url] and [scale] must not be null.
const MyLocalCacheNetworkImage(
this.url, {
this.scale = 1.0,
this.headers,
this.isLocalCache = false,
}) : assert(url != null),
assert(scale != null);
@override
final String url;
@override
final double scale;
@override
final Map headers;
final bool isLocalCache;
@override
Future obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter load(NetworkImage key, DecoderCallback decode) {
// Ownership of this controller is handed off to [_loadAsync]; it is that
// method's responsibility to close the controller's stream when the image
// has been loaded or an error is thrown.
final StreamController chunkEvents = StreamController();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
debugLabel: key.url,
informationCollector: () {
return [
DiagnosticsProperty('Image provider', this),
DiagnosticsProperty('Image key', key),
];
},
);
}
// Do not access this field directly; use [_httpClient] instead.
// We set `autoUncompress` to false to ensure that we can trust the value of
// the `Content-Length` HTTP header. We automatically uncompress the content
// in our call to [consolidateHttpClientResponseBytes].
static final HttpClient _sharedHttpClient = HttpClient()..autoUncompress = false;
static HttpClient get _httpClient {
HttpClient client = _sharedHttpClient;
assert(() {
if (debugNetworkImageHttpClientProvider != null) client = debugNetworkImageHttpClientProvider();
return true;
}());
return client;
}
Future _loadAsync(
NetworkImage key,
StreamController chunkEvents,
DecoderCallback decode,
) async {
try {
assert(key == this);
/// 如果本地缓存过图片,直接返回图片
if (isLocalCache != null && isLocalCache == true) {
final Uint8List bytes = await _getImageFromLocal(key.url);
if (bytes != null && bytes.lengthInBytes != null && bytes.lengthInBytes != 0) {
return await PaintingBinding.instance.instantiateImageCodec(bytes);
}
}
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) {
// The network may be only temporarily unavailable, or the file will be
// added on the server later. Avoid having future calls to resolve
// fail to check the network again.
throw 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 (isLocalCache != null && isLocalCache == true && bytes.lengthInBytes != 0) {
_saveImageToLocal(bytes, key.url);
}
if (bytes.lengthInBytes == 0) throw Exception('NetworkImage is an empty file: $resolved');
return decode(bytes);
} catch (e) {
// Depending on where the exception was thrown, the image cache may not
// have had a chance to track the key in the cache at all.
// Schedule a microtask to give the cache a chance to add the key.
scheduleMicrotask(() {
PaintingBinding.instance.imageCache.evict(key);
});
rethrow;
} finally {
chunkEvents.close();
}
}
/// 图片路径通过MD5处理,然后缓存到本地
void _saveImageToLocal(Uint8List mUInt8List, String name) async {
String path = await _getCachePathString(name);
var file = File(path);
bool exist = await file.exists();
if (!exist) {
File(path).writeAsBytesSync(mUInt8List);
}
}
/// 从本地拿图片
Future _getImageFromLocal(String name) async {
String path = await _getCachePathString(name);
var file = File(path);
bool exist = await file.exists();
if (exist) {
final Uint8List bytes = await file.readAsBytes();
return bytes;
}
return null;
}
/// 获取图片的缓存路径并创建
Future _getCachePathString(String name) async {
// 获取图片的名称
String filePathFileName = md5.convert(convert.utf8.encode(name)).toString();
String extensionName = name.split('/').last.split('.').last;
// print('图片url:$name');
// print('filePathFileName:$filePathFileName');
// print('extensionName:$extensionName');
// 生成、获取结果存储路径
final tempDic = await getTemporaryDirectory();
Directory directory = Directory(tempDic.path + '/CacheImage/');
bool isFoldExist = await directory.exists();
if (!isFoldExist) {
await directory.create();
}
return directory.path + filePathFileName + '.$extensionName';
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is NetworkImage && other.url == url && other.scale == scale;
}
@override
int get hashCode => ui.hashValues(url, scale);
@override
String toString() => '${objectRuntimeType(this, 'NetworkImage')}("$url", scale: $scale)';
}
主要修改有:
1、从本地获取缓存并返回
/// 如果本地缓存过图片,直接返回图片
if (isLocalCache != null && isLocalCache == true) {
final Uint8List bytes = await _getImageFromLocal(key.url);
if (bytes != null && bytes.lengthInBytes != null && bytes.lengthInBytes != 0) {
return await PaintingBinding.instance.instantiateImageCodec(bytes);
}
}
2、图片网络情请求完之后,存储到本地
/// 网络请求结束后,将图片缓存到本地
if (isLocalCache != null && isLocalCache == true && bytes.lengthInBytes != 0) {
_saveImageToLocal(bytes, key.url);
}
3、保存到本地、从本地获取图片、获取并创建本地缓存路径的具体实现,主要是最其中图片网络请求获取到的bytes
和图片的url
进行存储等操作。
/// 图片路径通过MD5处理,然后缓存到本地
void _saveImageToLocal(Uint8List mUInt8List, String name) async {
String path = await _getCachePathString(name);
var file = File(path);
bool exist = await file.exists();
if (!exist) {
File(path).writeAsBytesSync(mUInt8List);
}
}
/// 从本地拿图片
Future _getImageFromLocal(String name) async {
String path = await _getCachePathString(name);
var file = File(path);
bool exist = await file.exists();
if (exist) {
final Uint8List bytes = await file.readAsBytes();
return bytes;
}
return null;
}
/// 获取图片的缓存路径并创建
Future _getCachePathString(String name) async {
// 获取图片的名称
String filePathFileName = md5.convert(convert.utf8.encode(name)).toString();
String extensionName = name.split('/').last.split('.').last;
// print('图片url:$name');
// print('filePathFileName:$filePathFileName');
// print('extensionName:$extensionName');
// 生成、获取结果存储路径
final tempDic = await getTemporaryDirectory();
Directory directory = Directory(tempDic.path + '/CacheImage/');
bool isFoldExist = await directory.exists();
if (!isFoldExist) {
await directory.create();
}
return directory.path + filePathFileName + '.$extensionName';
}
四、使用
将上面的命名构造方法复制出来,创建一个自己的命名构造方法,比如(部分代码):
class CustomFadeInImage extends StatelessWidget {
CustomFadeInImage.assetNetwork({
@required this.image,
this.placeholder,
this.width,
this.height,
this.fit,
this.alignment = Alignment.center,
this.imageScale = 1.0,
this.imageCacheWidth,
this.imageCacheHeight,
}) : imageProvider = ResizeImage.resizeIfNeeded(
imageCacheWidth, imageCacheHeight, MyLocalCacheNetworkImage(image, scale: imageScale, isLocalCache: true));
将ResizeImage.resizeIfNeeded
中的NetworkImage
替换为MyLocalCacheNetworkImage
即可。
五、缓存清理
清空对应的缓存目录里的图片即可。