在flutter中,默认的图片加载是缓存在内存中的,那么意味着我们结束应用程序后再次进入程序需要再次通过网络请求去加载一些图片资源,在用户的网络情况不是特别好的情况下就会造成不太良好的体验,网上也有一些技术大牛发布了一些加载库,确实厉害。当然别人能实现的我们自己也能实现,一直用别人的虽然节省了时间但不如自己亲自实现香,特别是这种小东西(比较大型的诸如Bloc这些状态管理框架还是随着自己对源码的掌握程度慢慢来,开发时间成本较高,肯定先用大牛的了。),如果自己实现或许能够提升那么一丢丢自己对flutter源码的认知。
废话少说,直接一步一步探究实现过程:
Image(image:?,)
我们在使用的时候这样去获取一个图片,这里点击进去可以发现它需要的是一个
ImageProvider 对象,这是一个抽象类,我们点进源码查看寻找它的实现类,我们可以发现
NetworkImage 这个类,我们平时在调用网络图片的时候是这样用的:
Image.network("src")
点击进去可以发现源码是这样的:
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);
可以清晰的看见它其实就是返回了一个NetworkImage对象,故我们要想优化读取网络图片不让其二次加载就可以在这个类中做文章,
我们先看源码:
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);
}
///这里在加载网络图片,我们的图片如果存储在本地了,就可以在此处先去查询本地是否有这个图片,如果有则直接返回本地的图片数据,直接 return.
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)';
}
源码中我标注的地方就是我们魔改源码的关键之处了,这里我们要引用一下MD5算法,每次传进来的图片URL地址我们都将其用MD5算法生成一个特定的KEY,然后将图片以这个KEY为关键存储在本地,再次加载时就可以通过这个KEY去寻找对应的图片文件了。
最后,上代码:
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import '../util.dart';
@immutable
class CacheImage extends ImageProvider {
/// Creates an object that fetches the image at the given URL.
///
/// The arguments [url] and [scale] must not be null.
const CacheImage(this.url, {this.scale = 1.0, this.headers});
final String url;
final double scale;
final Map? headers;
@override
Future obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter load(CacheImage 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(
CacheImage key,
StreamController chunkEvents,
DecoderCallback decode,
) async {
try {
assert(key == this);
final Uint8List? bytes = await _get(key.url);
if (bytes != 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.
await response.drain>();
throw NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
}
final Uint8List bytes1 = await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int? total) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
));
},
);
if (bytes1.lengthInBytes != 0) _save(bytes1, key.url);
if (bytes1.lengthInBytes == 0) throw Exception('CacheImage is an empty file: $resolved');
return decode(bytes1);
} 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 CacheImage && other.url == url && other.scale == scale;
}
@override
int get hashCode => ui.hashValues(url, scale);
@override
String toString() => '${objectRuntimeType(this, 'CacheImage')}("$url", scale: $scale)';
void _save(Uint8List uint8list, String name) async {
name = generateMD5(name);
Directory dir = await getTemporaryDirectory();
String path = dir.path + "/img/" + name;
//You can customize the path address of the cached image here
var file = File(path);
bool exist = await file.exists();
if (!exist) File(path).writeAsBytesSync(uint8list);
}
Future _get(String name) async {
name = generateMD5(name);
Directory dir = await getTemporaryDirectory();
String path = dir.path + "/img/" + name;
var file = File(path);
bool exist = await file.exists();
if (exist) {
final Uint8List bytes = await file.readAsBytes();
return bytes;
}
}
}
使用就特别方便了,上github链接:eaaomk(山水有相逢),
generateMD5这是工具类中的一个方法,代码很简单,如果不想自己写请移步github自取代码.
欢迎各位技术爱好者一些讨论学习进步。
山水有相逢。