import 'dart:async'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; import 'package:logging/logging.dart'; import 'package:puzzleweave/models/api_helper.dart'; import 'package:puzzleweave/models/data.dart'; import 'package:puzzleweave/models/items.dart'; import 'package:puzzleweave/utils/utils.dart'; final Logger _log = Logger('download.dart'); /// 最多缓存/并发下载n个图到内存 const maxCachedItems = 1; /// Singleton class Download { static final Download _instance = Download._internal(); factory Download() => _instance; Download._internal(); final Map _cache = {}; DownloadItem download(String url, String cachePath) { if (_cache[url] != null) { _log.info('Cache hit for $url'); _cache[url]!.touch(); 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; // !!! 修正点 1: 在任务完成后,异步触发清理 // 任务完成后,它占用的内存 Image 和 Data 就可以被清理了 Future.microtask(_clean); } catch (err) { // 发生错误,立即移除缓存项 _log.info('Watch download item got error: $err'); _cache.remove(item.url); } } _clean() { final list = _cache.values.where((item) => item.loadCompleter.isCompleted).toList(); if (list.length <= maxCachedItems) return; _log.info('cleaning...'); // 2. 按最近使用时间排序(时间越早越应该被清理) list.sort((a, b) => a.lastUsed.compareTo(b.lastUsed)); // 3. 清理到只剩下 maxCachedItems 个 while (list.length > maxCachedItems) { final item = list.removeAt(0); _log.info('Cleaning item from memory: $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; Uint8List? _data; DownloadItem(this.url, this.cachePath) : lastUsed = DateTime.now().millisecondsSinceEpoch { _log.info('New download item for: $url'); _start(); } Uint8List? get data => _data; set data(Uint8List? val) => _data = val; touch() => lastUsed = DateTime.now().millisecondsSinceEpoch; _start() async { try { await _download(); if (!loadCompleter.isCompleted) loadCompleter.complete(); } catch (err) { if (!loadCompleter.isCompleted) loadCompleter.completeError(err); } } Future _download() async { progress.value = 0; final file = await localFile(cachePath); _checkDispose(); // 核心改造:如果文件存在,只报完成,不读数据 (Lazy Load) if (await file.exists()) { _log.info('Disk cache hit (Metadata only) for $cachePath'); progress.value = 1.0; return; } // 网络下载逻辑 final List bytes = []; client = Client(); final response = await client!.send(Request('GET', Uri.parse(url))); _checkDispose(); if (response.statusCode != 200) throw Exception('Status:${response.statusCode}'); final length = response.contentLength ?? 0; final streamCompleter = Completer(); subscription = response.stream.listen( (value) { bytes.addAll(value); if (length > 0) progress.value = bytes.length / length; }, onDone: () => streamCompleter.complete(), onError: (e) => streamCompleter.completeError(e), cancelOnError: true, ); await streamCompleter.future; _checkDispose(); // 剔除 24 字节干扰码 if (bytes.length > 24) { bytes.removeRange(0, 24); } _data = Uint8List.fromList(bytes); await saveBytes(cachePath, _data!); // 写入磁盘 _log.info('Download and save complete for $url'); client?.close(); } /// 供 Loader 真正需要数据时调用 Future ensureDataLoaded() async { if (_data != null) return _data!; _log.info('Performing late read from disk: $cachePath'); final file = await localFile(cachePath); _data = await file.readAsBytes(); return _data!; } bool _isDisposed = false; _checkDispose() { if (_isDisposed) throw Exception('Disposed'); } dispose() { _isDisposed = true; _data = null; // 释放内存 subscription?.cancel(); client?.close(); } @override String toString() => '[$cachePath]'; } abstract class ItemLoader { final Completer completer = Completer(); Uint8List? get data; ValueNotifier get progress; ItemLoader(); // 辅助方法:确保数据就绪后再解码 Future _prepareData() async { await completer.future; if (data == null) throw 'Data missing after completion'; return data!; } Future getImageBySize(int width, int height, {allowUpscaling = true}) async { final bytes = await _prepareData(); final codec = await ui.instantiateImageCodec(bytes, targetHeight: height, targetWidth: width, allowUpscaling: allowUpscaling); return (await codec.getNextFrame()).image; } Future getImage() async { final bytes = await _prepareData(); final codec = await ui.instantiateImageCodec(bytes); return (await codec.getNextFrame()).image; } /// 专门用于预加载的静态方法,不创建 Loader 实例 static void preload(ListItem item, String quality) { if (BuiltinRegistry.contains(item.id)) { _log.info('Preload: Skipping builtin ${item.id}'); return; } if (item is RemoteItem) { // 触发 Download 但不等待读入内存 Download().download(ApiHelper.imageUri(item.id, quality), item.cachePath); } } factory ItemLoader.load(ListItem item, String quality) { if (BuiltinRegistry.contains(item.id)) { _log.info('Built-in hit: ${item.id}, skip network.'); return AssetItemLoader('assets/builtin/${item.id}.jpeg'); } switch (item.runtimeType) { case RemoteItem: return RemoteItemLoader(ApiHelper.imageUri(item.id, quality), item.cachePath); case AssetItem: return AssetItemLoader((item as AssetItem).image); default: throw 'Unknown item type'; } } } 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(); progress.value = 1.0; } catch (e) { completer.completeError(e); } } @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; // 在这里不读 data,getImage 时才读 completer.complete(); } catch (err) { completer.completeError(err); } } @override Uint8List? get data { // 同步获取(如果已读入内存),如果没读,需通过 getImage 异步触发 return downloadItem.data; } @override Future _prepareData() async { await completer.future; // 关键点:如果内存里没数据(预加载命中的缓存),在此处执行补读 return await downloadItem.ensureDataLoaded(); } @override ValueNotifier get progress => downloadItem.progress; }