download.dart 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. import 'dart:async';
  2. import 'dart:ui' as ui;
  3. import 'package:flutter/foundation.dart';
  4. import 'package:http/http.dart';
  5. import 'package:logging/logging.dart';
  6. import 'package:puzzleweave/models/api_helper.dart';
  7. import 'package:puzzleweave/models/data.dart';
  8. import 'package:puzzleweave/models/items.dart';
  9. import 'package:puzzleweave/utils/utils.dart';
  10. final Logger _log = Logger('download.dart');
  11. /// 最多缓存/并发下载n个图到内存
  12. const maxCachedItems = 1;
  13. /// Singleton
  14. class Download {
  15. static final Download _instance = Download._internal();
  16. factory Download() => _instance;
  17. Download._internal();
  18. final Map<String, DownloadItem> _cache = {};
  19. DownloadItem download(String url, String cachePath) {
  20. if (_cache[url] != null) {
  21. _log.info('Cache hit for $url');
  22. _cache[url]!.touch();
  23. return _cache[url]!;
  24. } else {
  25. final item = DownloadItem(url, cachePath);
  26. _cache[url] = item;
  27. _watch(item);
  28. return item;
  29. }
  30. }
  31. _watch(DownloadItem item) async {
  32. try {
  33. await item.loadCompleter.future;
  34. // !!! 修正点 1: 在任务完成后,异步触发清理
  35. // 任务完成后,它占用的内存 Image 和 Data 就可以被清理了
  36. Future.microtask(_clean);
  37. } catch (err) {
  38. // 发生错误,立即移除缓存项
  39. _log.info('Watch download item got error: $err');
  40. _cache.remove(item.url);
  41. }
  42. }
  43. _clean() {
  44. final list = _cache.values.where((item) => item.loadCompleter.isCompleted).toList();
  45. if (list.length <= maxCachedItems) return;
  46. _log.info('cleaning...');
  47. // 2. 按最近使用时间排序(时间越早越应该被清理)
  48. list.sort((a, b) => a.lastUsed.compareTo(b.lastUsed));
  49. // 3. 清理到只剩下 maxCachedItems 个
  50. while (list.length > maxCachedItems) {
  51. final item = list.removeAt(0);
  52. _log.info('Cleaning item from memory: $item');
  53. item.dispose();
  54. _cache.remove(item.url);
  55. }
  56. }
  57. clearAllCached() async {
  58. final file = await localFile('cache');
  59. await file.delete(recursive: true);
  60. }
  61. }
  62. class DownloadItem {
  63. final String url;
  64. final String cachePath;
  65. ValueNotifier<double> progress = ValueNotifier(0.0);
  66. final Completer<void> loadCompleter = Completer(); // 仅作为完成信号
  67. Client? client;
  68. StreamSubscription? subscription;
  69. int lastUsed;
  70. Uint8List? _data;
  71. DownloadItem(this.url, this.cachePath) : lastUsed = DateTime.now().millisecondsSinceEpoch {
  72. _log.info('New download item for: $url');
  73. _start();
  74. }
  75. Uint8List? get data => _data;
  76. set data(Uint8List? val) => _data = val;
  77. touch() => lastUsed = DateTime.now().millisecondsSinceEpoch;
  78. _start() async {
  79. try {
  80. await _download();
  81. if (!loadCompleter.isCompleted) loadCompleter.complete();
  82. } catch (err) {
  83. if (!loadCompleter.isCompleted) loadCompleter.completeError(err);
  84. }
  85. }
  86. Future<void> _download() async {
  87. progress.value = 0;
  88. final file = await localFile(cachePath);
  89. _checkDispose();
  90. // 核心改造:如果文件存在,只报完成,不读数据 (Lazy Load)
  91. if (await file.exists()) {
  92. _log.info('Disk cache hit (Metadata only) for $cachePath');
  93. progress.value = 1.0;
  94. return;
  95. }
  96. // 网络下载逻辑
  97. final List<int> bytes = [];
  98. client = Client();
  99. final response = await client!.send(Request('GET', Uri.parse(url)));
  100. _checkDispose();
  101. if (response.statusCode != 200) throw Exception('Status:${response.statusCode}');
  102. final length = response.contentLength ?? 0;
  103. final streamCompleter = Completer();
  104. subscription = response.stream.listen(
  105. (value) {
  106. bytes.addAll(value);
  107. if (length > 0) progress.value = bytes.length / length;
  108. },
  109. onDone: () => streamCompleter.complete(),
  110. onError: (e) => streamCompleter.completeError(e),
  111. cancelOnError: true,
  112. );
  113. await streamCompleter.future;
  114. _checkDispose();
  115. // 剔除 24 字节干扰码
  116. if (bytes.length > 24) {
  117. bytes.removeRange(0, 24);
  118. }
  119. _data = Uint8List.fromList(bytes);
  120. await saveBytes(cachePath, _data!); // 写入磁盘
  121. _log.info('Download and save complete for $url');
  122. client?.close();
  123. }
  124. /// 供 Loader 真正需要数据时调用
  125. Future<Uint8List> ensureDataLoaded() async {
  126. if (_data != null) return _data!;
  127. _log.info('Performing late read from disk: $cachePath');
  128. final file = await localFile(cachePath);
  129. _data = await file.readAsBytes();
  130. return _data!;
  131. }
  132. bool _isDisposed = false;
  133. _checkDispose() {
  134. if (_isDisposed) throw Exception('Disposed');
  135. }
  136. dispose() {
  137. _isDisposed = true;
  138. _data = null; // 释放内存
  139. subscription?.cancel();
  140. client?.close();
  141. }
  142. @override
  143. String toString() => '[$cachePath]';
  144. }
  145. abstract class ItemLoader {
  146. final Completer completer = Completer();
  147. Uint8List? get data;
  148. ValueNotifier<double> get progress;
  149. ItemLoader();
  150. // 辅助方法:确保数据就绪后再解码
  151. Future<Uint8List> _prepareData() async {
  152. await completer.future;
  153. if (data == null) throw 'Data missing after completion';
  154. return data!;
  155. }
  156. Future<ui.Image> getImageBySize(int width, int height, {allowUpscaling = true}) async {
  157. final bytes = await _prepareData();
  158. final codec = await ui.instantiateImageCodec(bytes, targetHeight: height, targetWidth: width, allowUpscaling: allowUpscaling);
  159. return (await codec.getNextFrame()).image;
  160. }
  161. Future<ui.Image> getImage() async {
  162. final bytes = await _prepareData();
  163. final codec = await ui.instantiateImageCodec(bytes);
  164. return (await codec.getNextFrame()).image;
  165. }
  166. /// 专门用于预加载的静态方法,不创建 Loader 实例
  167. static void preload(ListItem item, String quality) {
  168. if (BuiltinRegistry.contains(item.id)) {
  169. _log.info('Preload: Skipping builtin ${item.id}');
  170. return;
  171. }
  172. if (item is RemoteItem) {
  173. // 触发 Download 但不等待读入内存
  174. Download().download(ApiHelper.imageUri(item.id, quality), item.cachePath);
  175. }
  176. }
  177. factory ItemLoader.load(ListItem item, String quality) {
  178. if (BuiltinRegistry.contains(item.id)) {
  179. _log.info('Built-in hit: ${item.id}, skip network.');
  180. return AssetItemLoader('assets/builtin/${item.id}.jpeg');
  181. }
  182. switch (item.runtimeType) {
  183. case RemoteItem:
  184. return RemoteItemLoader(ApiHelper.imageUri(item.id, quality), item.cachePath);
  185. case AssetItem:
  186. return AssetItemLoader((item as AssetItem).image);
  187. default:
  188. throw 'Unknown item type';
  189. }
  190. }
  191. }
  192. class AssetItemLoader extends ItemLoader {
  193. final String path;
  194. Uint8List? _data;
  195. @override
  196. ValueNotifier<double> progress = ValueNotifier(0);
  197. AssetItemLoader(this.path) {
  198. _load();
  199. }
  200. _load() async {
  201. try {
  202. _data = await loadFileDataFromAsset(path);
  203. completer.complete();
  204. progress.value = 1.0;
  205. } catch (e) {
  206. completer.completeError(e);
  207. }
  208. }
  209. @override
  210. Uint8List? get data => _data;
  211. }
  212. class RemoteItemLoader extends ItemLoader {
  213. final String url;
  214. final String cachePath;
  215. late final DownloadItem downloadItem;
  216. RemoteItemLoader(this.url, this.cachePath) {
  217. downloadItem = Download().download(url, cachePath);
  218. _load();
  219. }
  220. _load() async {
  221. try {
  222. await downloadItem.loadCompleter.future;
  223. // 在这里不读 data,getImage 时才读
  224. completer.complete();
  225. } catch (err) {
  226. completer.completeError(err);
  227. }
  228. }
  229. @override
  230. Uint8List? get data {
  231. // 同步获取(如果已读入内存),如果没读,需通过 getImage 异步触发
  232. return downloadItem.data;
  233. }
  234. @override
  235. Future<Uint8List> _prepareData() async {
  236. await completer.future;
  237. // 关键点:如果内存里没数据(预加载命中的缓存),在此处执行补读
  238. return await downloadItem.ensureDataLoaded();
  239. }
  240. @override
  241. ValueNotifier<double> get progress => downloadItem.progress;
  242. }