download.dart 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  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:puzzleweave/config/device.dart';
  6. import 'package:puzzleweave/models/items.dart';
  7. import 'package:puzzleweave/utils/utils.dart';
  8. import 'package:logging/logging.dart';
  9. final Logger _log = Logger('download.dart');
  10. /// 最多缓存/并发下载n个图到内存
  11. const maxCachedItems = 1;
  12. /// Sigeleton
  13. class Download {
  14. static final Download _instance = Download._internal();
  15. factory Download() {
  16. return _instance;
  17. }
  18. Download._internal();
  19. final Map<String, DownloadItem> _cache = {};
  20. DownloadItem download(url, cachePath) {
  21. // 移除同步 _clean() 调用,避免干扰正在启动的下载序列
  22. // _clean();
  23. if (_cache[url] != null) {
  24. _log.info('Cache hit for $url');
  25. _cache[url]!.touch(); //update last use time.
  26. return _cache[url]!;
  27. } else {
  28. final item = DownloadItem(url, cachePath);
  29. _cache[url] = item;
  30. _watch(item);
  31. return item;
  32. }
  33. }
  34. _watch(DownloadItem item) async {
  35. try {
  36. await item.loadCompleter.future;
  37. // !!! 修正点 1: 在任务完成后,异步触发清理
  38. // 任务完成后,它占用的内存 Image 和 Data 就可以被清理了
  39. Future.microtask(_clean);
  40. } catch (err) {
  41. // 发生错误,立即移除缓存项
  42. _log.info('Watch download item got error: $err');
  43. _cache.remove(item.url);
  44. }
  45. }
  46. _clean() {
  47. // final list = _cache.values.toList();
  48. // 1. 筛选出可以被清理的项:必须是已完成加载(已完成下载并写入磁盘)
  49. final list = _cache.values
  50. .where((item) => item.loadCompleter.isCompleted) // !!! 修正点 2: 仅清理已完成加载的项
  51. .toList();
  52. if (list.length <= maxCachedItems) return;
  53. _log.info('cleaning...');
  54. // 2. 按最近使用时间排序(时间越早越应该被清理)
  55. list.sort((a, b) => a.lastUsed.compareTo(b.lastUsed));
  56. // 3. 清理到只剩下 maxCachedItems 个
  57. while (list.length > maxCachedItems) {
  58. final item = list.removeAt(0);
  59. _log.info('clean item: $item');
  60. item.dispose();
  61. _cache.remove(item.url);
  62. }
  63. }
  64. clearAllCached() async {
  65. final file = await localFile('cache');
  66. await file.delete(recursive: true);
  67. }
  68. }
  69. class DownloadItem {
  70. final String url;
  71. final String cachePath;
  72. ValueNotifier<double> progress = ValueNotifier(0.0);
  73. final Completer<ui.Image> loadCompleter = Completer();
  74. Client? client;
  75. StreamSubscription? subscription;
  76. int lastUsed;
  77. int size = 0;
  78. ui.Image? image;
  79. Uint8List? data;
  80. DownloadItem(this.url, this.cachePath) : lastUsed = DateTime.now().millisecondsSinceEpoch {
  81. _log.info('New download item for: $url');
  82. _start();
  83. }
  84. touch() {
  85. lastUsed = DateTime.now().millisecondsSinceEpoch;
  86. }
  87. _start() async {
  88. try {
  89. final image = await _download();
  90. loadCompleter.complete(image);
  91. } catch (err) {
  92. loadCompleter.completeError(err);
  93. }
  94. }
  95. Future<ui.Image> _download() async {
  96. progress.value = 0;
  97. final file = await localFile(cachePath);
  98. _checkDispose();
  99. final Uint8List data;
  100. bool shouldSave = false;
  101. //if (await file.exists()) {
  102. if (await file.exists()) {
  103. _log.info('Disk cache hit..');
  104. data = await file.readAsBytes();
  105. _checkDispose();
  106. progress.value = 1;
  107. } else {
  108. final List<int> bytes = [];
  109. client = Client();
  110. final uri = Uri.parse(url);
  111. final request = Request('GET', uri);
  112. final response = await client!.send(request);
  113. _checkDispose();
  114. if (response.statusCode != 200) {
  115. throw Exception('Download error, stauts:${response.statusCode}, url=$uri');
  116. }
  117. if (response.contentLength == null) {
  118. throw Exception('Download error, no length, url=$uri');
  119. }
  120. final length = response.contentLength!;
  121. _log.info('message: contentLength=$length');
  122. final streamCompleter = Completer();
  123. subscription = response.stream.listen(
  124. (value) {
  125. try {
  126. // 有可能内存溢出, 先try/catch包一下
  127. bytes.addAll(value);
  128. progress.value = bytes.length / length;
  129. _log.info('message: progress=${progress.value}');
  130. } catch (e) {
  131. _log.warning("Out of memory: $e");
  132. // FirebaseCrashlytics.instance.log("OOM from download url: $uri, error: $e");
  133. streamCompleter.completeError(e);
  134. }
  135. },
  136. onDone: () {
  137. //_log.info('onDone..');
  138. streamCompleter.complete();
  139. },
  140. onError: (e) {
  141. _log.info('onError: $e');
  142. streamCompleter.completeError(e);
  143. },
  144. cancelOnError: true,
  145. );
  146. await streamCompleter.future;
  147. //await response.stream.first;
  148. _log.info('xxxxxxxxxxxxxxxxxxxx stream complete');
  149. _checkDispose();
  150. _log.info('message: download succeed, progress=$progress, length=${bytes.length}');
  151. bytes.removeRange(0, 24);
  152. data = Uint8List.fromList(bytes);
  153. shouldSave = true;
  154. _checkDispose();
  155. }
  156. _log.info('message: realbytes: ${data.length}');
  157. int size = Device.physicalSize.width.toInt();
  158. final ui.Codec codec = await ui.instantiateImageCodec(data, targetHeight: size, targetWidth: size);
  159. final ui.FrameInfo frameInfo = await codec.getNextFrame();
  160. final image = frameInfo.image;
  161. this.image = image;
  162. this.data = data;
  163. size = data.length;
  164. if (shouldSave) {
  165. await saveBytes(cachePath, data);
  166. }
  167. _checkDispose();
  168. _log.info('image: ${image.width}x${image.height}');
  169. client?.close();
  170. return image;
  171. }
  172. Future<ui.Image> getImageBySize(int dim, {allowUpscaling = false}) async {
  173. await loadCompleter.future;
  174. final ui.Codec codec = await ui.instantiateImageCodec(data!, targetHeight: dim, targetWidth: dim, allowUpscaling: allowUpscaling);
  175. final ui.FrameInfo frameInfo = await codec.getNextFrame();
  176. return frameInfo.image;
  177. }
  178. bool _isDisposed = false;
  179. _checkDispose() {
  180. _log.info('$this,checkDispose: $_isDisposed');
  181. if (_isDisposed) throw Exception('Request disposed');
  182. }
  183. dispose() async {
  184. _log.info('Disposing $this, client:$client');
  185. _isDisposed = true;
  186. // do clean.
  187. try {
  188. subscription?.cancel();
  189. client?.close();
  190. image?.dispose();
  191. } catch (error) {
  192. _log.info('xxxxxxxxxxxx $error');
  193. }
  194. }
  195. @override
  196. String toString() {
  197. return '[$cachePath]';
  198. }
  199. }
  200. abstract class ItemLoader {
  201. final Completer completer = Completer();
  202. Uint8List? get data;
  203. ValueNotifier<double> get progress;
  204. ItemLoader();
  205. Future<ui.Image> getImageBySize(int width, int height, {allowUpscaling = true}) async {
  206. await completer.future;
  207. final ui.Codec codec = await ui.instantiateImageCodec(data!, targetHeight: height, targetWidth: width, allowUpscaling: allowUpscaling);
  208. final ui.FrameInfo frameInfo = await codec.getNextFrame();
  209. return frameInfo.image;
  210. }
  211. Future<ui.Image> getImage() async {
  212. await completer.future;
  213. final ui.Codec codec = await ui.instantiateImageCodec(data!);
  214. final ui.FrameInfo frameInfo = await codec.getNextFrame();
  215. return frameInfo.image;
  216. }
  217. factory ItemLoader.load(ListItem item) {
  218. switch (item.runtimeType) {
  219. case RemoteItem:
  220. return RemoteItemLoader((item as RemoteItem).image, item.cachePath);
  221. case AssetItem:
  222. return AssetItemLoader((item as AssetItem).image);
  223. default:
  224. throw 'Can\'t create ${item.runtimeType}';
  225. }
  226. }
  227. }
  228. class LocalItemLoader extends ItemLoader {
  229. final String path;
  230. Uint8List? _data;
  231. @override
  232. ValueNotifier<double> progress = ValueNotifier(0);
  233. LocalItemLoader(this.path) {
  234. _load();
  235. }
  236. _load() async {
  237. try {
  238. final file = await localFile(path);
  239. _data = await file.readAsBytes();
  240. completer.complete(data);
  241. progress.value = 1.0;
  242. } catch (error) {
  243. completer.completeError(error);
  244. }
  245. }
  246. @override
  247. Uint8List? get data => _data;
  248. }
  249. class AssetItemLoader extends ItemLoader {
  250. final String path;
  251. Uint8List? _data;
  252. @override
  253. ValueNotifier<double> progress = ValueNotifier(0);
  254. AssetItemLoader(this.path) {
  255. _load();
  256. }
  257. _load() async {
  258. try {
  259. _data = await loadFileDataFromAsset(path);
  260. completer.complete(data);
  261. progress.value = 1.0;
  262. } catch (error) {
  263. completer.completeError(error);
  264. }
  265. }
  266. @override
  267. Uint8List? get data => _data;
  268. }
  269. class RemoteItemLoader extends ItemLoader {
  270. final String url;
  271. final String cachePath;
  272. late final DownloadItem downloadItem;
  273. RemoteItemLoader(this.url, this.cachePath) {
  274. downloadItem = Download().download(url, cachePath);
  275. _load();
  276. }
  277. _load() async {
  278. try {
  279. await downloadItem.loadCompleter.future;
  280. completer.complete();
  281. } catch (err) {
  282. completer.completeError(err);
  283. }
  284. }
  285. @override
  286. Uint8List? get data => downloadItem.data;
  287. @override
  288. ValueNotifier<double> get progress => downloadItem.progress;
  289. }