| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- 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<String, DownloadItem> _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<double> progress = ValueNotifier(0.0);
- final Completer<void> 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<void> _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<int> 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<Uint8List> 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<double> get progress;
- ItemLoader();
- // 辅助方法:确保数据就绪后再解码
- Future<Uint8List> _prepareData() async {
- await completer.future;
- if (data == null) throw 'Data missing after completion';
- return data!;
- }
- Future<ui.Image> 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<ui.Image> 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<double> 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<Uint8List> _prepareData() async {
- await completer.future;
- // 关键点:如果内存里没数据(预加载命中的缓存),在此处执行补读
- return await downloadItem.ensureDataLoaded();
- }
- @override
- ValueNotifier<double> get progress => downloadItem.progress;
- }
|