download.dart 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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:image_puzzle/config/device.dart';
  6. import 'package:image_puzzle/models/items.dart';
  7. import 'package:image_puzzle/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. if (_cache[url] != null) {
  23. _log.info('Cache hit for $url');
  24. _cache[url]!.touch(); //update last use time.
  25. return _cache[url]!;
  26. } else {
  27. final item = DownloadItem(url, cachePath);
  28. _cache[url] = item;
  29. _watch(item);
  30. return item;
  31. }
  32. }
  33. _watch(DownloadItem item) async {
  34. try {
  35. await item.loadCompleter.future;
  36. } catch (err) {
  37. _log.info('Watch download item got error: $err');
  38. _cache.remove(item.url);
  39. }
  40. }
  41. _clean() {
  42. final list = _cache.values.toList();
  43. if (list.length <= maxCachedItems) return;
  44. _log.info('cleaning...');
  45. list.sort((a, b) => a.lastUsed.compareTo(b.lastUsed));
  46. while (list.length > maxCachedItems) {
  47. final item = list.removeAt(0);
  48. _log.info('clean item: $item');
  49. item.dispose();
  50. _cache.remove(item.url);
  51. }
  52. }
  53. clearAllCached() async {
  54. final file = await localFile('cache');
  55. await file.delete(recursive: true);
  56. }
  57. }
  58. class DownloadItem {
  59. final String url;
  60. final String cachePath;
  61. ValueNotifier<double> progress = ValueNotifier(0.0);
  62. final Completer<ui.Image> loadCompleter = Completer();
  63. Client? client;
  64. StreamSubscription? subscription;
  65. int lastUsed;
  66. int size = 0;
  67. ui.Image? image;
  68. Uint8List? data;
  69. DownloadItem(this.url, this.cachePath) : lastUsed = DateTime.now().millisecondsSinceEpoch {
  70. _log.info('New download item for: $url');
  71. _start();
  72. }
  73. touch() {
  74. lastUsed = DateTime.now().millisecondsSinceEpoch;
  75. }
  76. _start() async {
  77. try {
  78. final image = await _download();
  79. loadCompleter.complete(image);
  80. } catch (err) {
  81. loadCompleter.completeError(err);
  82. }
  83. }
  84. Future<ui.Image> _download() async {
  85. progress.value = 0;
  86. final file = await localFile(cachePath);
  87. _checkDispose();
  88. final Uint8List data;
  89. bool shouldSave = false;
  90. //if (await file.exists()) {
  91. if (await file.exists()) {
  92. _log.info('Disk cache hit..');
  93. data = await file.readAsBytes();
  94. _checkDispose();
  95. progress.value = 1;
  96. } else {
  97. final List<int> bytes = [];
  98. client = Client();
  99. final uri = Uri.parse(url);
  100. final request = Request('GET', uri);
  101. final response = await client!.send(request);
  102. _checkDispose();
  103. if (response.statusCode != 200) {
  104. throw Exception('Download error, stauts:${response.statusCode}, url=$uri');
  105. }
  106. if (response.contentLength == null) {
  107. throw Exception('Download error, no length, url=$uri');
  108. }
  109. final length = response.contentLength!;
  110. _log.info('message: contentLength=$length');
  111. final streamCompleter = Completer();
  112. subscription = response.stream.listen(
  113. (value) {
  114. try {
  115. // 有可能内存溢出, 先try/catch包一下
  116. bytes.addAll(value);
  117. progress.value = bytes.length / length;
  118. _log.info('message: progress=${progress.value}');
  119. } catch (e) {
  120. _log.warning("Out of memory: $e");
  121. // FirebaseCrashlytics.instance.log("OOM from download url: $uri, error: $e");
  122. streamCompleter.completeError(e);
  123. }
  124. },
  125. onDone: () {
  126. //_log.info('onDone..');
  127. streamCompleter.complete();
  128. },
  129. onError: (e) {
  130. _log.info('onError: $e');
  131. streamCompleter.completeError(e);
  132. },
  133. cancelOnError: true,
  134. );
  135. await streamCompleter.future;
  136. //await response.stream.first;
  137. _log.info('xxxxxxxxxxxxxxxxxxxx stream complete');
  138. _checkDispose();
  139. _log.info('message: download succeed, progress=$progress, length=${bytes.length}');
  140. bytes.removeRange(0, 24);
  141. data = Uint8List.fromList(bytes);
  142. shouldSave = true;
  143. _checkDispose();
  144. }
  145. _log.info('message: realbytes: ${data.length}');
  146. int size = Device.physicalSize.width.toInt();
  147. final ui.Codec codec = await ui.instantiateImageCodec(data, targetHeight: size, targetWidth: size);
  148. final ui.FrameInfo frameInfo = await codec.getNextFrame();
  149. final image = frameInfo.image;
  150. this.image = image;
  151. this.data = data;
  152. size = data.length;
  153. if (shouldSave) {
  154. await saveBytes(cachePath, data);
  155. }
  156. _checkDispose();
  157. _log.info('image: ${image.width}x${image.height}');
  158. client?.close();
  159. return image;
  160. }
  161. Future<ui.Image> getImageBySize(int dim, {allowUpscaling = false}) async {
  162. await loadCompleter.future;
  163. final ui.Codec codec = await ui.instantiateImageCodec(data!, targetHeight: dim, targetWidth: dim, allowUpscaling: allowUpscaling);
  164. final ui.FrameInfo frameInfo = await codec.getNextFrame();
  165. return frameInfo.image;
  166. }
  167. bool _isDisposed = false;
  168. _checkDispose() {
  169. _log.info('$this,checkDispose: $_isDisposed');
  170. if (_isDisposed) throw Exception('Request disposed');
  171. }
  172. dispose() async {
  173. _log.info('Disposing $this, client:$client');
  174. _isDisposed = true;
  175. // do clean.
  176. try {
  177. subscription?.cancel();
  178. client?.close();
  179. image?.dispose();
  180. } catch (error) {
  181. _log.info('xxxxxxxxxxxx $error');
  182. }
  183. }
  184. @override
  185. String toString() {
  186. return '[$cachePath]';
  187. }
  188. }
  189. abstract class ItemLoader {
  190. final Completer completer = Completer();
  191. Uint8List? get data;
  192. ValueNotifier<double> get progress;
  193. ItemLoader();
  194. Future<ui.Image> getImageBySize(int width, int height, {allowUpscaling = true}) async {
  195. await completer.future;
  196. final ui.Codec codec = await ui.instantiateImageCodec(data!, targetHeight: height, targetWidth: width, allowUpscaling: allowUpscaling);
  197. final ui.FrameInfo frameInfo = await codec.getNextFrame();
  198. return frameInfo.image;
  199. }
  200. factory ItemLoader.load(ListItem item) {
  201. switch (item.runtimeType) {
  202. case RemoteItem:
  203. return RemoteItemLoader((item as RemoteItem).image, item.cachePath);
  204. case AssetItem:
  205. return AssetItemLoader((item as AssetItem).image);
  206. default:
  207. throw 'Can\'t create ${item.runtimeType}';
  208. }
  209. }
  210. }
  211. class LocalItemLoader extends ItemLoader {
  212. final String path;
  213. Uint8List? _data;
  214. @override
  215. ValueNotifier<double> progress = ValueNotifier(0);
  216. LocalItemLoader(this.path) {
  217. _load();
  218. }
  219. _load() async {
  220. try {
  221. final file = await localFile(path);
  222. _data = await file.readAsBytes();
  223. completer.complete(data);
  224. progress.value = 1.0;
  225. } catch (error) {
  226. completer.completeError(error);
  227. }
  228. }
  229. @override
  230. Uint8List? get data => _data;
  231. }
  232. class AssetItemLoader extends ItemLoader {
  233. final String path;
  234. Uint8List? _data;
  235. @override
  236. ValueNotifier<double> progress = ValueNotifier(0);
  237. AssetItemLoader(this.path) {
  238. _load();
  239. }
  240. _load() async {
  241. try {
  242. _data = await loadFileDataFromAsset(path);
  243. completer.complete(data);
  244. progress.value = 1.0;
  245. } catch (error) {
  246. completer.completeError(error);
  247. }
  248. }
  249. @override
  250. Uint8List? get data => _data;
  251. }
  252. class RemoteItemLoader extends ItemLoader {
  253. final String url;
  254. final String cachePath;
  255. late final DownloadItem downloadItem;
  256. RemoteItemLoader(this.url, this.cachePath) {
  257. downloadItem = Download().download(url, cachePath);
  258. _load();
  259. }
  260. _load() async {
  261. try {
  262. await downloadItem.loadCompleter.future;
  263. completer.complete();
  264. } catch (err) {
  265. completer.completeError(err);
  266. }
  267. }
  268. @override
  269. Uint8List? get data => downloadItem.data;
  270. @override
  271. ValueNotifier<double> get progress => downloadItem.progress;
  272. }