前言
一天測試小姐姐拿著手機過來說,你這里圖片下載有問題呀,為什么沒有網(wǎng)絡(開飛行模式)也彈Toast
提示下載成功呀缓艳?
下意識反應,肯定是Toast
提示彈早了看峻,剛點擊按鈕阶淘,還沒開始下載就彈了Toast
,趕緊拿手機過來操作驗證一波互妓。確實沒有網(wǎng)絡溪窒,彈了下載完成提示杰扫,去相冊檢查一下宣羊,嗯?圖片下載成功了日月,還有這種操作灼狰?趕緊檢查一下代碼惜浅,發(fā)現(xiàn)項目中使用的cached_network_image
三方庫加載的圖片,從名字上可以判斷伏嗜,這是一個緩存網(wǎng)絡圖片的加載框架坛悉。所以應該是圖片顯示出來以后就被緩存到本地了,實際下載的流程并未走網(wǎng)絡請求承绸,為了驗證想法裸影,看了下框架加載圖片流程,總結出下文军熏。
使用
組件CachedNetworkImage可以支持直接使用或者通過ImageProvider
轩猩。
引入依賴
dependencies:
cached_network_image: ^3.1.0
執(zhí)行flutter pub get
,項目中使用
Import it
import 'package:cached_network_image/cached_network_image.dart';
添加占位圖
CachedNetworkImage(
imageUrl: "http://via.placeholder.com/350x150",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
),
進度條展示
CachedNetworkImage(
imageUrl: "http://via.placeholder.com/350x150",
progressIndicatorBuilder: (context, url, downloadProgress) =>
CircularProgressIndicator(value: downloadProgress.progress),
errorWidget: (context, url, error) => Icon(Icons.error),
),
原生組件Image配合
Image(image: CachedNetworkImageProvider(url))
使用占位圖并提供provider給其他組件使用
CachedNetworkImage(
imageUrl: "http://via.placeholder.com/200x150",
imageBuilder: (context, imageProvider) => Container(
decoration: BoxDecoration(
image: DecorationImage(
image: imageProvider,
fit: BoxFit.cover,
colorFilter:
ColorFilter.mode(Colors.red, BlendMode.colorBurn)),
),
),
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
),
這樣就可以加載網(wǎng)絡圖片了荡澎,而且均践,圖片加載完成時,就被緩存到本地了摩幔,首先看下圖片的加載流程
官網(wǎng)說了彤委,它現(xiàn)在不包含緩存,緩存功能實際上是另一個庫
flutter_cache_manager
中實現(xiàn)的
原理
加載&顯示
這里我們僅梳理圖片加載和緩存的主流程或衡,對于一些其他分支流程焦影,或無關參數(shù)不做過多分析
首先,頁面上使用的構造函數(shù)接收了一個必傳參數(shù)imageUrl封断,用于生成ImageProvider提供圖片加載
class CachedNetworkImage extends StatelessWidget{
/// image提供
final CachedNetworkImageProvider _image;
/// 構造函數(shù)
CachedNetworkImage({
Key key,
@required this.imageUrl,
/// 省略部分
this.cacheManager,
/// ...
}) : assert(imageUrl != null),
/// ...
_image = CachedNetworkImageProvider(
imageUrl,
headers: httpHeaders,
cacheManager: cacheManager,
cacheKey: cacheKey,
imageRenderMethodForWeb: imageRenderMethodForWeb,
maxWidth: maxWidthDiskCache,
maxHeight: maxHeightDiskCache,
),
super(key: key);
@override
Widget build(BuildContext context) {
var octoPlaceholderBuilder =
placeholder != null ? _octoPlaceholderBuilder : null;
var octoProgressIndicatorBuilder =
progressIndicatorBuilder != null ? _octoProgressIndicatorBuilder : null;
/// ...
return OctoImage(
image: _image,
/// ...
);
}
}
這里可以看到斯辰,構造函數(shù)初始化了一個本地變量_image
類型是CachedNetworkImageProvider
,它繼承ImageProvider提供圖片加載坡疼,看下它的構造函數(shù)
/// 提供網(wǎng)絡圖片加載Provider并緩存
abstract class CachedNetworkImageProvider
extends ImageProvider<CachedNetworkImageProvider> {
/// Creates an object that fetches the image at the given URL.
const factory CachedNetworkImageProvider(
String url, {
int maxHeight,
int maxWidth,
String cacheKey,
double scale,
@Deprecated('ErrorListener is deprecated, use listeners on the imagestream')
ErrorListener errorListener,
Map<String, String> headers,
BaseCacheManager cacheManager,
ImageRenderMethodForWeb imageRenderMethodForWeb,
}) = image_provider.CachedNetworkImageProvider;
/// 可選cacheManager. 默認使用 DefaultCacheManager()
/// 當運行在web時,cacheManager沒有使用.
BaseCacheManager get cacheManager;
/// 請求url.
String get url;
/// 緩存key
String get cacheKey;
/// ...
@override
ImageStreamCompleter load(
CachedNetworkImageProvider key, DecoderCallback decode);
}
它的構造函數(shù)調用了image_provider.CachedNetworkImageProvider
的實例在_image_provider_io.dart
中是加載的具體實現(xiàn)類
/// IO implementation of the CachedNetworkImageProvider; the ImageProvider to
/// load network images using a cache.
class CachedNetworkImageProvider
extends ImageProvider<image_provider.CachedNetworkImageProvider>
implements image_provider.CachedNetworkImageProvider {
/// Creates an ImageProvider which loads an image from the [url], using the [scale].
/// When the image fails to load [errorListener] is called.
const CachedNetworkImageProvider(
this.url, {
/// ...
}) : assert(url != null),
assert(scale != null);
@override
final BaseCacheManager cacheManager;
/// ...
@override
Future<CachedNetworkImageProvider> obtainKey(
ImageConfiguration configuration) {
return SynchronousFuture<CachedNetworkImageProvider>(this);
}
/// 核心方法加載圖片入口
@override
ImageStreamCompleter load(
image_provider.CachedNetworkImageProvider key, DecoderCallback decode) {
final chunkEvents = StreamController<ImageChunkEvent>();
/// 多圖加載
return MultiImageStreamCompleter(
codec: _loadAsync(key, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
informationCollector: () sync* {
yield DiagnosticsProeperty<ImageProvider>(
'Image provider: $this \n Image key: $key',
this,
style: DiagnosticsTreeStyle.errorProperty,
);
},
);
}
這里的load方法即是圖片加載的啟動入口彬呻,它會在頁面可見時被調用
它返回了一個MultiImageStreamCompleter
傳入_loadAsync
,看下這個方法
/// 異步加載
Stream<ui.Codec> _loadAsync(
CachedNetworkImageProvider key,
StreamController<ImageChunkEvent> chunkEvents,
DecoderCallback decode,
) async* {
assert(key == this);
try {
/// 默認緩存管理器
var mngr = cacheManager ?? DefaultCacheManager();
assert(
mngr is ImageCacheManager || (maxWidth == null && maxHeight == null),
'To resize the image with a CacheManager the '
'CacheManager needs to be an ImageCacheManager. maxWidth and '
'maxHeight will be ignored when a normal CacheManager is used.');
/// 下載邏輯放在ImageCacheManager,得到下載stream
var stream = mngr is ImageCacheManager
? mngr.getImageFile(key.url,
maxHeight: maxHeight,
maxWidth: maxWidth,
withProgress: true,
headers: headers,
key: key.cacheKey)
: mngr.getFileStream(key.url,
withProgress: true, headers: headers, key: key.cacheKey);
await for (var result in stream) {
if (result is FileInfo) {
var file = result.file;
var bytes = await file.readAsBytes();
var decoded = await decode(bytes);
/// 下載完成返回結果
yield decoded;
}
}
} catch (e) {
/// ...
} finally {
await chunkEvents.close();
}
}
}
這里我們看到了默認緩存管理器cacheManager
創(chuàng)建的地方闸氮,為DefaultCacheManager
剪况,那么它如何緩存的呢,后邊再分析湖苞。
下載的邏輯也是放在了ImageCacheManager
下了拯欧,返回結果是一個stream
完成多圖下載的支持详囤,下載完成通過yield 返回給ui解碼最終顯示财骨。
MultiImageStreamCompleter
支持多圖加載繼承自ImageStreamCompleter
/// An ImageStreamCompleter with support for loading multiple images.
class MultiImageStreamCompleter extends ImageStreamCompleter {
/// The constructor to create an MultiImageStreamCompleter. The [codec]
/// should be a stream with the images that should be shown. The
/// [chunkEvents] should indicate the [ImageChunkEvent]s of the first image
/// to show.
MultiImageStreamCompleter({
@required Stream<ui.Codec> codec,
@required double scale,
Stream<ImageChunkEvent> chunkEvents,
InformationCollector informationCollector,
}) : assert(codec != null),
_informationCollector = informationCollector,
_scale = scale {
/// 顯示邏輯
codec.listen((event) {
if (_timer != null) {
_nextImageCodec = event;
} else {
_handleCodecReady(event);
}
}, onError: (dynamic error, StackTrace stack) {
reportError(
context: ErrorDescription('resolving an image codec'),
exception: error,
stack: stack,
informationCollector: informationCollector,
silent: true,
);
});
/// ...
}
}
/// 處理解碼完成
void _handleCodecReady(ui.Codec codec) {
_codec = codec;
assert(_codec != null);
if (hasListeners) {
_decodeNextFrameAndSchedule();
}
}
/// 解碼下一幀并繪制
Future<void> _decodeNextFrameAndSchedule() async {
try {
_nextFrame = await _codec.getNextFrame();
} catch (exception, stack) {
reportError(
context: ErrorDescription('resolving an image frame'),
exception: exception,
stack: stack,
informationCollector: _informationCollector,
silent: true,
);
return;
}
if (_codec.frameCount == 1) {
// ImageStreamCompleter listeners removed while waiting for next frame to
// be decoded.
// There's no reason to emit the frame without active listeners.
if (!hasListeners) {
return;
}
// This is not an animated image, just return it and don't schedule more
// frames.
_emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
return;
}
_scheduleAppFrame();
}
}
這里做了顯示邏輯,和最終轉化成flutter上幀的處理藏姐,_scheduleAppFrame
完成發(fā)送幀的處理
下載&緩存
上邊的mngr
調用了ImageCacheManager
中的getImageFile
方法現(xiàn)在就到了flutter_cache_manager
這個三方庫當中隆箩,它是被隱式依賴的,文件是image_cache_manager.dart
mixin ImageCacheManager on BaseCacheManager {
Stream<FileResponse> getImageFile(
String url, {
String key,
Map<String, String> headers,
bool withProgress,
int maxHeight,
int maxWidth,
}) async* {
if (maxHeight == null && maxWidth == null) {
yield* getFileStream(url,
key: key, headers: headers, withProgress: withProgress);
return;
}
/// ...
}
/// ...
}
getFileStream
方法實現(xiàn)在子類cache_manager.dart
文件中的CacheManager
class CacheManager implements BaseCacheManager {
/// 緩存管理
CacheStore _store;
/// Get the underlying store helper
CacheStore get store => _store;
/// 下載管理
WebHelper _webHelper;
/// Get the underlying web helper
WebHelper get webHelper => _webHelper;
/// 從下載或者緩存讀取file返回stream
@override
Stream<FileResponse> getFileStream(String url,
{String key, Map<String, String> headers, bool withProgress}) {
key ??= url;
final streamController = StreamController<FileResponse>();
_pushFileToStream(
streamController, url, key, headers, withProgress ?? false);
return streamController.stream;
}
Future<void> _pushFileToStream(StreamController streamController, String url,
String key, Map<String, String> headers, bool withProgress) async {
key ??= url;
FileInfo cacheFile;
try {
/// 緩存判斷
cacheFile = await getFileFromCache(key);
if (cacheFile != null) {
/// 有緩存直接返回
streamController.add(cacheFile);
withProgress = false;
}
} catch (e) {
print(
'CacheManager: Failed to load cached file for $url with error:\n$e');
}
/// 沒有緩存或者過期下載
if (cacheFile == null || cacheFile.validTill.isBefore(DateTime.now())) {
try {
await for (var response
in _webHelper.downloadFile(url, key: key, authHeaders: headers)) {
if (response is DownloadProgress && withProgress) {
streamController.add(response);
}
if (response is FileInfo) {
streamController.add(response);
}
}
} catch (e) {
assert(() {
print(
'CacheManager: Failed to download file from $url with error:\n$e');
return true;
}());
if (cacheFile == null && streamController.hasListener) {
streamController.addError(e);
}
}
}
unawaited(streamController.close());
}
}
緩存判斷邏輯在CacheStore
提供兩級緩存
class CacheStore {
Duration cleanupRunMinInterval = const Duration(seconds: 10);
/// 未下載完成緩存
final _futureCache = <String, Future<CacheObject>>{};
/// 已下載完緩存
final _memCache = <String, CacheObject>{};
/// ...
Future<FileInfo> getFile(String key, {bool ignoreMemCache = false}) async {
final cacheObject =
await retrieveCacheData(key, ignoreMemCache: ignoreMemCache);
if (cacheObject == null || cacheObject.relativePath == null) {
return null;
}
final file = await fileSystem.createFile(cacheObject.relativePath);
return FileInfo(
file,
FileSource.Cache,
cacheObject.validTill,
cacheObject.url,
);
}
Future<void> putFile(CacheObject cacheObject) async {
_memCache[cacheObject.key] = cacheObject;
await _updateCacheDataInDatabase(cacheObject);
}
Future<CacheObject> retrieveCacheData(String key,
{bool ignoreMemCache = false}) async {
/// 判斷是否已緩存過
if (!ignoreMemCache && _memCache.containsKey(key)) {
if (await _fileExists(_memCache[key])) {
return _memCache[key];
}
}
/// 未緩存的 已加入futureCache中的key直接返回
if (!_futureCache.containsKey(key)) {
final completer = Completer<CacheObject>();
/// 未加入的添加到futureCache
unawaited(_getCacheDataFromDatabase(key).then((cacheObject) async {
if (cacheObject != null && !await _fileExists(cacheObject)) {
final provider = await _cacheInfoRepository;
await provider.delete(cacheObject.id);
cacheObject = null;
}
_memCache[key] = cacheObject;
completer.complete(cacheObject);
unawaited(_futureCache.remove(key));
}));
_futureCache[key] = completer.future;
}
return _futureCache[key];
}
/// ...
/// 更新到數(shù)據(jù)庫
Future<dynamic> _updateCacheDataInDatabase(CacheObject cacheObject) async {
final provider = await _cacheInfoRepository;
return provider.updateOrInsert(cacheObject);
}
}
_cacheInfoRepository
緩存?zhèn)}庫是CacheObjectProvider
使用的數(shù)據(jù)庫緩存對象
class CacheObjectProvider extends CacheInfoRepository
with CacheInfoRepositoryHelperMethods {
Database db;
String _path;
String databaseName;
CacheObjectProvider({String path, this.databaseName}) : _path = path;
/// 打開
@override
Future<bool> open() async {
if (!shouldOpenOnNewConnection()) {
return openCompleter.future;
}
var path = await _getPath();
await File(path).parent.create(recursive: true);
db = await openDatabase(path, version: 3,
onCreate: (Database db, int version) async {
await db.execute('''
create table $_tableCacheObject (
${CacheObject.columnId} integer primary key,
${CacheObject.columnUrl} text,
${CacheObject.columnKey} text,
${CacheObject.columnPath} text,
${CacheObject.columnETag} text,
${CacheObject.columnValidTill} integer,
${CacheObject.columnTouched} integer,
${CacheObject.columnLength} integer
);
create unique index $_tableCacheObject${CacheObject.columnKey}
ON $_tableCacheObject (${CacheObject.columnKey});
''');
}, onUpgrade: (Database db, int oldVersion, int newVersion) async {
/// ...
return opened();
}
@override
Future<dynamic> updateOrInsert(CacheObject cacheObject) {
if (cacheObject.id == null) {
return insert(cacheObject);
} else {
return update(cacheObject);
}
}
@override
Future<CacheObject> insert(CacheObject cacheObject,
{bool setTouchedToNow = true}) async {
var id = await db.insert(
_tableCacheObject,
cacheObject.toMap(setTouchedToNow: setTouchedToNow),
);
return cacheObject.copyWith(id: id);
}
@override
Future<CacheObject> get(String key) async {
List<Map> maps = await db.query(_tableCacheObject,
columns: null, where: '${CacheObject.columnKey} = ?', whereArgs: [key]);
if (maps.isNotEmpty) {
return CacheObject.fromMap(maps.first.cast<String, dynamic>());
}
return null;
}
@override
Future<int> delete(int id) {
return db.delete(_tableCacheObject,
where: '${CacheObject.columnId} = ?', whereArgs: [id]);
}
}
可見數(shù)據(jù)庫緩存的是CacheObject
對象羔杨,保存了url捌臊、key、relativePath等信息
class CacheObject {
static const columnId = '_id';
static const columnUrl = 'url';
static const columnKey = 'key';
static const columnPath = 'relativePath';
static const columnETag = 'eTag';
static const columnValidTill = 'validTill';
static const columnTouched = 'touched';
static const columnLength = 'length';
}
沒有緩存下調用了_webHelper.downloadFile
方法
class WebHelper {
WebHelper(this._store, FileService fileFetcher)
: _memCache = {},
fileFetcher = fileFetcher ?? HttpFileService();
final CacheStore _store;
@visibleForTesting
final FileService fileFetcher;
final Map<String, BehaviorSubject<FileResponse>> _memCache;
final Queue<QueueItem> _queue = Queue();
///Download the file from the url
Stream<FileResponse> downloadFile(String url,
{String key,
Map<String, String> authHeaders,
bool ignoreMemCache = false}) {
key ??= url;
if (!_memCache.containsKey(key) || ignoreMemCache) {
var subject = BehaviorSubject<FileResponse>();
_memCache[key] = subject;
/// 下載或者加入隊列
unawaited(_downloadOrAddToQueue(url, key, authHeaders));
}
return _memCache[key].stream;
}
Future<void> _downloadOrAddToQueue(
String url,
String key,
Map<String, String> authHeaders,
) async {
//如果太多請求被執(zhí)行兜材,加入隊列等待
if (concurrentCalls >= fileFetcher.concurrentFetches) {
_queue.add(QueueItem(url, key, authHeaders));
return;
}
concurrentCalls++;
var subject = _memCache[key];
try {
await for (var result
in _updateFile(url, key, authHeaders: authHeaders)) {
subject.add(result);
}
} catch (e, stackTrace) {
subject.addError(e, stackTrace);
} finally {
concurrentCalls--;
await subject.close();
_memCache.remove(key);
_checkQueue();
}
}
///下載資源
Stream<FileResponse> _updateFile(String url, String key,
{Map<String, String> authHeaders}) async* {
var cacheObject = await _store.retrieveCacheData(key);
cacheObject = cacheObject == null
? CacheObject(url, key: key)
: cacheObject.copyWith(url: url);
/// 請求得到response
final response = await _download(cacheObject, authHeaders);
yield* _manageResponse(cacheObject, response);
}
Stream<FileResponse> _manageResponse(
CacheObject cacheObject, FileServiceResponse response) async* {
/// ...
if (statusCodesNewFile.contains(response.statusCode)) {
int savedBytes;
await for (var progress in _saveFile(newCacheObject, response)) {
savedBytes = progress;
yield DownloadProgress(
cacheObject.url, response.contentLength, progress);
}
newCacheObject = newCacheObject.copyWith(length: savedBytes);
}
/// 加入緩存
unawaited(_store.putFile(newCacheObject).then((_) {
if (newCacheObject.relativePath != oldCacheObject.relativePath) {
_removeOldFile(oldCacheObject.relativePath);
}
}));
final file = await _store.fileSystem.createFile(
newCacheObject.relativePath,
);
yield FileInfo(
file,
FileSource.Online,
newCacheObject.validTill,
newCacheObject.url,
);
}
Stream<int> _saveFile(CacheObject cacheObject, FileServiceResponse response) {
var receivedBytesResultController = StreamController<int>();
unawaited(_saveFileAndPostUpdates(
receivedBytesResultController,
cacheObject,
response,
));
return receivedBytesResultController.stream;
}
Future _saveFileAndPostUpdates(
StreamController<int> receivedBytesResultController,
CacheObject cacheObject,
FileServiceResponse response) async {
/// 根據(jù)路徑創(chuàng)建file
final file = await _store.fileSystem.createFile(cacheObject.relativePath);
try {
var receivedBytes = 0;
/// 寫文件
final sink = file.openWrite();
await response.content.map((s) {
receivedBytes += s.length;
receivedBytesResultController.add(receivedBytes);
return s;
}).pipe(sink);
} catch (e, stacktrace) {
receivedBytesResultController.addError(e, stacktrace);
}
await receivedBytesResultController.close();
}
}
總結
cached_network_image
圖片加載流程依賴ImageProvider
理澎,緩存和下載邏輯放在另一個庫flutter_cache_manager
下載文件在WebHelper
中提供隊列管理,依賴傳入FileService
做具體獲取文件方便擴展默認實現(xiàn)HttpFileService
曙寡,下載完成后路徑保存在CacheObject
保存在sqflite
數(shù)據(jù)庫