软件编程
位置:首页>> 软件编程>> flutter编程>> 详解Flutter网络图片本地缓存的实现

详解Flutter网络图片本地缓存的实现

作者:xmb  发布时间:2023-08-18 19:44:43 

标签:Flutter,网络图片,本地缓存

一、问题

Flutter原有的图片缓存机制,是通过PaintingBinding.instance!.imageCache来管理缓存的,这个缓存缓存到的是内存中,每次重新打开APP或者缓存被清理都会再次进行网络请求,大图片加载慢不友好,且增加服务器负担。

二、思路

1、查看FadeInImage.assetNetworkImage.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<String, String> 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<NetworkImage> {
 /// 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<String, String>? 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<String, String>? get headers;
 @override
 ImageStreamCompleter load(NetworkImage key, DecoderCallback decode);
}

其中工厂方法给了一个值,const factory NetworkImage(String url, { double scale, Map<String, String>? headers }) = network_image.NetworkImage;

进入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<image_provider.NetworkImage> 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<String, String>? headers;
 @override
 Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
   return SynchronousFuture<NetworkImage>(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<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
   return MultiFrameImageStreamCompleter(
     codec: _loadAsync(key as NetworkImage, chunkEvents, decode),
     chunkEvents: chunkEvents.stream,
     scale: key.scale,
     debugLabel: key.url,
     informationCollector: () {
       return <DiagnosticsNode>[
         DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
         DiagnosticsProperty<image_provider.NetworkImage>('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<ui.Codec> _loadAsync(
   NetworkImage key,
   StreamController<ImageChunkEvent> 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<NetworkImage> 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<String, String> headers;
 final bool isLocalCache;
 @override
 Future<NetworkImage> obtainKey(ImageConfiguration configuration) {
   return SynchronousFuture<NetworkImage>(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<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
   return MultiFrameImageStreamCompleter(
     codec: _loadAsync(key, chunkEvents, decode),
     chunkEvents: chunkEvents.stream,
     scale: key.scale,
     debugLabel: key.url,
     informationCollector: () {
       return <DiagnosticsNode>[
         DiagnosticsProperty<ImageProvider>('Image provider', this),
         DiagnosticsProperty<NetworkImage>('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<ui.Codec> _loadAsync(
   NetworkImage key,
   StreamController<ImageChunkEvent> 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<Uint8List> _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<String> _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<Uint8List> _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<String> _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 即可。

五、缓存清理

清空对应的缓存目录里的图片即可。

来源:https://juejin.cn/post/7007711028767096869

0
投稿

猜你喜欢

手机版 软件编程 asp之家 www.aspxhome.com