import 'dart:async'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; import 'package:image_puzzle/config/device.dart'; import 'package:image_puzzle/models/items.dart'; import 'package:image_puzzle/utils/utils.dart'; import 'package:logging/logging.dart'; final Logger _log = Logger('download.dart'); /// 最多缓存/并发下载n个图到内存 const maxCachedItems = 1; /// Sigeleton class Download { static final Download _instance = Download._internal(); factory Download() { return _instance; } Download._internal(); final Map _cache = {}; DownloadItem download(url, cachePath) { _clean(); if (_cache[url] != null) { _log.info('Cache hit for $url'); _cache[url]!.touch(); //update last use time. return _cache[url]!; } else { final item = DownloadItem(url, cachePath); _cache[url] = item; _watch(item); return item; } } _watch(DownloadItem item) async { try { await item.loadCompleter.future; } catch (err) { _log.info('Watch download item got error: $err'); _cache.remove(item.url); } } _clean() { final list = _cache.values.toList(); if (list.length <= maxCachedItems) return; _log.info('cleaning...'); list.sort((a, b) => a.lastUsed.compareTo(b.lastUsed)); while (list.length > maxCachedItems) { final item = list.removeAt(0); _log.info('clean item: $item'); item.dispose(); _cache.remove(item.url); } } clearAllCached() async { final file = await localFile('cache'); await file.delete(recursive: true); } } class DownloadItem { final String url; final String cachePath; ValueNotifier progress = ValueNotifier(0.0); final Completer loadCompleter = Completer(); Client? client; StreamSubscription? subscription; int lastUsed; int size = 0; ui.Image? image; Uint8List? data; DownloadItem(this.url, this.cachePath) : lastUsed = DateTime.now().millisecondsSinceEpoch { _log.info('New download item for: $url'); _start(); } touch() { lastUsed = DateTime.now().millisecondsSinceEpoch; } _start() async { try { final image = await _download(); loadCompleter.complete(image); } catch (err) { loadCompleter.completeError(err); } } Future _download() async { progress.value = 0; final file = await localFile(cachePath); _checkDispose(); final Uint8List data; bool shouldSave = false; //if (await file.exists()) { if (await file.exists()) { _log.info('Disk cache hit..'); data = await file.readAsBytes(); _checkDispose(); progress.value = 1; } else { final List bytes = []; client = Client(); final uri = Uri.parse(url); final request = Request('GET', uri); final response = await client!.send(request); _checkDispose(); if (response.statusCode != 200) { throw Exception('Download error, stauts:${response.statusCode}, url=$uri'); } if (response.contentLength == null) { throw Exception('Download error, no length, url=$uri'); } final length = response.contentLength!; _log.info('message: contentLength=$length'); final streamCompleter = Completer(); subscription = response.stream.listen( (value) { try { // 有可能内存溢出, 先try/catch包一下 bytes.addAll(value); progress.value = bytes.length / length; _log.info('message: progress=${progress.value}'); } catch (e) { _log.warning("Out of memory: $e"); // FirebaseCrashlytics.instance.log("OOM from download url: $uri, error: $e"); streamCompleter.completeError(e); } }, onDone: () { //_log.info('onDone..'); streamCompleter.complete(); }, onError: (e) { _log.info('onError: $e'); streamCompleter.completeError(e); }, cancelOnError: true, ); await streamCompleter.future; //await response.stream.first; _log.info('xxxxxxxxxxxxxxxxxxxx stream complete'); _checkDispose(); _log.info('message: download succeed, progress=$progress, length=${bytes.length}'); bytes.removeRange(0, 24); data = Uint8List.fromList(bytes); shouldSave = true; _checkDispose(); } _log.info('message: realbytes: ${data.length}'); int size = Device.physicalSize.width.toInt(); final ui.Codec codec = await ui.instantiateImageCodec(data, targetHeight: size, targetWidth: size); final ui.FrameInfo frameInfo = await codec.getNextFrame(); final image = frameInfo.image; this.image = image; this.data = data; size = data.length; if (shouldSave) { await saveBytes(cachePath, data); } _checkDispose(); _log.info('image: ${image.width}x${image.height}'); client?.close(); return image; } Future getImageBySize(int dim, {allowUpscaling = false}) async { await loadCompleter.future; final ui.Codec codec = await ui.instantiateImageCodec(data!, targetHeight: dim, targetWidth: dim, allowUpscaling: allowUpscaling); final ui.FrameInfo frameInfo = await codec.getNextFrame(); return frameInfo.image; } bool _isDisposed = false; _checkDispose() { _log.info('$this,checkDispose: $_isDisposed'); if (_isDisposed) throw Exception('Request disposed'); } dispose() async { _log.info('Disposing $this, client:$client'); _isDisposed = true; // do clean. try { subscription?.cancel(); client?.close(); image?.dispose(); } catch (error) { _log.info('xxxxxxxxxxxx $error'); } } @override String toString() { return '[$cachePath]'; } } abstract class ItemLoader { final Completer completer = Completer(); Uint8List? get data; ValueNotifier get progress; ItemLoader(); Future getImageBySize(int width, int height, {allowUpscaling = true}) async { await completer.future; final ui.Codec codec = await ui.instantiateImageCodec(data!, targetHeight: height, targetWidth: width, allowUpscaling: allowUpscaling); final ui.FrameInfo frameInfo = await codec.getNextFrame(); return frameInfo.image; } factory ItemLoader.load(ListItem item) { switch (item.runtimeType) { case RemoteItem: return RemoteItemLoader((item as RemoteItem).image, item.cachePath); case AssetItem: return AssetItemLoader((item as AssetItem).image); default: throw 'Can\'t create ${item.runtimeType}'; } } } class LocalItemLoader extends ItemLoader { final String path; Uint8List? _data; @override ValueNotifier progress = ValueNotifier(0); LocalItemLoader(this.path) { _load(); } _load() async { try { final file = await localFile(path); _data = await file.readAsBytes(); completer.complete(data); progress.value = 1.0; } catch (error) { completer.completeError(error); } } @override Uint8List? get data => _data; } class AssetItemLoader extends ItemLoader { final String path; Uint8List? _data; @override ValueNotifier progress = ValueNotifier(0); AssetItemLoader(this.path) { _load(); } _load() async { try { _data = await loadFileDataFromAsset(path); completer.complete(data); progress.value = 1.0; } catch (error) { completer.completeError(error); } } @override Uint8List? get data => _data; } class RemoteItemLoader extends ItemLoader { final String url; final String cachePath; late final DownloadItem downloadItem; RemoteItemLoader(this.url, this.cachePath) { downloadItem = Download().download(url, cachePath); _load(); } _load() async { try { await downloadItem.loadCompleter.future; completer.complete(); } catch (err) { completer.completeError(err); } } @override Uint8List? get data => downloadItem.data; @override ValueNotifier get progress => downloadItem.progress; }