詳解Flutter網(wǎng)絡(luò)圖片本地緩存的實(shí)現(xiàn)
一、問(wèn)題
Flutter
原有的圖片緩存機(jī)制,是通過(guò)PaintingBinding.instance!.imageCache
來(lái)管理緩存的,這個(gè)緩存緩存到的是內(nèi)存中,每次重新打開(kāi)APP
或者緩存被清理都會(huì)再次進(jìn)行網(wǎng)絡(luò)請(qǐng)求,大圖片加載慢不友好,且增加服務(wù)器負(fù)擔(dān)。
二、思路
1、查看FadeInImage.assetNetwork
、Image.network
等幾個(gè)網(wǎng)絡(luò)請(qǐng)求的命名構(gòu)造方法,初始化了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
類(lèi)型的NetworkImage
創(chuàng)建了ImageProvider
類(lèi)型的ResizeImage
。
而NetworkImage
是一個(gè)繼承ImageProvider
的抽象類(lèi)。
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); }
其中工廠方法給了一個(gè)值,const factory NetworkImage(String url, { double scale, Map<String, String>? headers }) = network_image.NetworkImage;
進(jìn)入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)'; }
對(duì)其中的_loadAsync
方法進(jìn)行修改,實(shí)現(xiàn)圖片的本地存儲(chǔ)和獲取,即可。
三、實(shí)現(xiàn)
1、新建一個(gè)文件my_local_cache_network_image.dart
,將_network_image_io.dart
內(nèi)容復(fù)制過(guò)來(lái),進(jìn)行修改。 2、全部文件內(nèi)容如下(非空安全版本):
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); /// 如果本地緩存過(guò)圖片,直接返回圖片 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, )); }, ); /// 網(wǎng)絡(luò)請(qǐng)求結(jié)束后,將圖片緩存到本地 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(); } } /// 圖片路徑通過(guò)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; } /// 獲取圖片的緩存路徑并創(chuàng)建 Future<String> _getCachePathString(String name) async { // 獲取圖片的名稱(chēng) 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'); // 生成、獲取結(jié)果存儲(chǔ)路徑 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、從本地獲取緩存并返回
/// 如果本地緩存過(guò)圖片,直接返回圖片 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、圖片網(wǎng)絡(luò)情請(qǐng)求完之后,存儲(chǔ)到本地
/// 網(wǎng)絡(luò)請(qǐng)求結(jié)束后,將圖片緩存到本地 if (isLocalCache != null && isLocalCache == true && bytes.lengthInBytes != 0) { _saveImageToLocal(bytes, key.url); }
3、保存到本地、從本地獲取圖片、獲取并創(chuàng)建本地緩存路徑的具體實(shí)現(xiàn),主要是最其中圖片網(wǎng)絡(luò)請(qǐng)求獲取到的bytes
和圖片的url
進(jìn)行存儲(chǔ)等操作。
/// 圖片路徑通過(guò)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; } /// 獲取圖片的緩存路徑并創(chuàng)建 Future<String> _getCachePathString(String name) async { // 獲取圖片的名稱(chēng) 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'); // 生成、獲取結(jié)果存儲(chǔ)路徑 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'; }
四、使用
將上面的命名構(gòu)造方法復(fù)制出來(lái),創(chuàng)建一個(gè)自己的命名構(gòu)造方法,比如(部分代碼):
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
即可。
五、緩存清理
清空對(duì)應(yīng)的緩存目錄里的圖片即可。
以上就是詳解Flutter網(wǎng)絡(luò)圖片本地緩存的實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于Flutter網(wǎng)絡(luò)圖片本地緩存的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android 使用Vitamio打造自己的萬(wàn)能播放器(7)——在線播放(下載視頻)
本文主要介紹Android Vitamio開(kāi)發(fā)播放器,這里提供在線播放和下載視頻實(shí)例代碼,有需要的小伙伴可以參考下2016-07-07Android自定義實(shí)現(xiàn)可滑動(dòng)按鈕
這篇文章主要為大家詳細(xì)介紹了Android自定義實(shí)現(xiàn)可滑動(dòng)的按鈕,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01詳解Android 語(yǔ)音播報(bào)實(shí)現(xiàn)方案(無(wú)SDK)
本篇文章主要介紹了詳解Android 語(yǔ)音播報(bào)實(shí)現(xiàn)方案(無(wú)SDK),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11Android Studio配置反混淆的實(shí)現(xiàn)
這篇文章主要介紹了Android Studio如何混淆的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10基于Android在布局中動(dòng)態(tài)添加view的兩種方法(總結(jié))
下面小編就為大家?guī)?lái)一篇基于Android在布局中動(dòng)態(tài)添加view的兩種方法(總結(jié))。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10Android HorizontalScrollView內(nèi)子控件橫向拖拽實(shí)例代碼
本文主要介紹Android HorizontalScrollView的使用,這里給大家一個(gè)實(shí)例來(lái)展示HorizontalScrollView內(nèi)子控件橫向拖拽的效果實(shí)現(xiàn),有需要的小伙伴可以參考下2016-07-07Jetpack?Compose重寫(xiě)TopAppBar實(shí)現(xiàn)標(biāo)題多行折疊詳解
這篇文章主要為大家介紹了Jetpack?Compose重寫(xiě)TopAppBar實(shí)現(xiàn)標(biāo)題多行折疊示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11