| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- import 'dart:async';
- import 'dart:ui' as ui;
- import 'package:flutter/foundation.dart';
- import 'package:http/http.dart';
- import 'package:puzzleweave/config/device.dart';
- import 'package:puzzleweave/models/items.dart';
- import 'package:puzzleweave/utils/utils.dart';
- import 'package:logging/logging.dart';
- final Logger _log = Logger('download.dart');
- /// 最多缓存/并发下载n个图到内存
- const maxCachedItems = 1;
- /// Sigeleton
- class Download {
- static final Download _instance = Download._internal();
- factory Download() {
- return _instance;
- }
- Download._internal();
- final Map<String, DownloadItem> _cache = {};
- DownloadItem download(url, cachePath) {
- // 移除同步 _clean() 调用,避免干扰正在启动的下载序列
- // _clean();
- if (_cache[url] != null) {
- _log.info('Cache hit for $url');
- _cache[url]!.touch(); //update last use time.
- 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.toList();
- // 1. 筛选出可以被清理的项:必须是已完成加载(已完成下载并写入磁盘)
- final list = _cache.values
- .where((item) => item.loadCompleter.isCompleted) // !!! 修正点 2: 仅清理已完成加载的项
- .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('clean item: $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<ui.Image> loadCompleter = Completer();
- Client? client;
- StreamSubscription? subscription;
- int lastUsed;
- int size = 0;
- ui.Image? image;
- Uint8List? data;
- DownloadItem(this.url, this.cachePath) : lastUsed = DateTime.now().millisecondsSinceEpoch {
- _log.info('New download item for: $url');
- _start();
- }
- touch() {
- lastUsed = DateTime.now().millisecondsSinceEpoch;
- }
- _start() async {
- try {
- final image = await _download();
- loadCompleter.complete(image);
- } catch (err) {
- loadCompleter.completeError(err);
- }
- }
- Future<ui.Image> _download() async {
- progress.value = 0;
- final file = await localFile(cachePath);
- _checkDispose();
- final Uint8List data;
- bool shouldSave = false;
- //if (await file.exists()) {
- if (await file.exists()) {
- _log.info('Disk cache hit..');
- data = await file.readAsBytes();
- _checkDispose();
- progress.value = 1;
- } else {
- final List<int> bytes = [];
- client = Client();
- final uri = Uri.parse(url);
- final request = Request('GET', uri);
- final response = await client!.send(request);
- _checkDispose();
- if (response.statusCode != 200) {
- throw Exception('Download error, stauts:${response.statusCode}, url=$uri');
- }
- if (response.contentLength == null) {
- throw Exception('Download error, no length, url=$uri');
- }
- final length = response.contentLength!;
- _log.info('message: contentLength=$length');
- final streamCompleter = Completer();
- subscription = response.stream.listen(
- (value) {
- try {
- // 有可能内存溢出, 先try/catch包一下
- bytes.addAll(value);
- progress.value = bytes.length / length;
- _log.info('message: progress=${progress.value}');
- } catch (e) {
- _log.warning("Out of memory: $e");
- // FirebaseCrashlytics.instance.log("OOM from download url: $uri, error: $e");
- streamCompleter.completeError(e);
- }
- },
- onDone: () {
- //_log.info('onDone..');
- streamCompleter.complete();
- },
- onError: (e) {
- _log.info('onError: $e');
- streamCompleter.completeError(e);
- },
- cancelOnError: true,
- );
- await streamCompleter.future;
- //await response.stream.first;
- _log.info('xxxxxxxxxxxxxxxxxxxx stream complete');
- _checkDispose();
- _log.info('message: download succeed, progress=$progress, length=${bytes.length}');
- bytes.removeRange(0, 24);
- data = Uint8List.fromList(bytes);
- shouldSave = true;
- _checkDispose();
- }
- _log.info('message: realbytes: ${data.length}');
- int size = Device.physicalSize.width.toInt();
- final ui.Codec codec = await ui.instantiateImageCodec(data, targetHeight: size, targetWidth: size);
- final ui.FrameInfo frameInfo = await codec.getNextFrame();
- final image = frameInfo.image;
- this.image = image;
- this.data = data;
- size = data.length;
- if (shouldSave) {
- await saveBytes(cachePath, data);
- }
- _checkDispose();
- _log.info('image: ${image.width}x${image.height}');
- client?.close();
- return image;
- }
- Future<ui.Image> getImageBySize(int dim, {allowUpscaling = false}) async {
- await loadCompleter.future;
- final ui.Codec codec = await ui.instantiateImageCodec(data!, targetHeight: dim, targetWidth: dim, allowUpscaling: allowUpscaling);
- final ui.FrameInfo frameInfo = await codec.getNextFrame();
- return frameInfo.image;
- }
- bool _isDisposed = false;
- _checkDispose() {
- _log.info('$this,checkDispose: $_isDisposed');
- if (_isDisposed) throw Exception('Request disposed');
- }
- dispose() async {
- _log.info('Disposing $this, client:$client');
- _isDisposed = true;
- // do clean.
- try {
- subscription?.cancel();
- client?.close();
- image?.dispose();
- } catch (error) {
- _log.info('xxxxxxxxxxxx $error');
- }
- }
- @override
- String toString() {
- return '[$cachePath]';
- }
- }
- abstract class ItemLoader {
- final Completer completer = Completer();
- Uint8List? get data;
- ValueNotifier<double> get progress;
- ItemLoader();
- Future<ui.Image> getImageBySize(int width, int height, {allowUpscaling = true}) async {
- await completer.future;
- final ui.Codec codec = await ui.instantiateImageCodec(data!, targetHeight: height, targetWidth: width, allowUpscaling: allowUpscaling);
- final ui.FrameInfo frameInfo = await codec.getNextFrame();
- return frameInfo.image;
- }
- Future<ui.Image> getImage() async {
- await completer.future;
- final ui.Codec codec = await ui.instantiateImageCodec(data!);
- final ui.FrameInfo frameInfo = await codec.getNextFrame();
- return frameInfo.image;
- }
- factory ItemLoader.load(ListItem item) {
- switch (item.runtimeType) {
- case RemoteItem:
- return RemoteItemLoader((item as RemoteItem).image, item.cachePath);
- case AssetItem:
- return AssetItemLoader((item as AssetItem).image);
- default:
- throw 'Can\'t create ${item.runtimeType}';
- }
- }
- }
- class LocalItemLoader extends ItemLoader {
- final String path;
- Uint8List? _data;
- @override
- ValueNotifier<double> progress = ValueNotifier(0);
- LocalItemLoader(this.path) {
- _load();
- }
- _load() async {
- try {
- final file = await localFile(path);
- _data = await file.readAsBytes();
- completer.complete(data);
- progress.value = 1.0;
- } catch (error) {
- completer.completeError(error);
- }
- }
- @override
- Uint8List? get data => _data;
- }
- 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(data);
- progress.value = 1.0;
- } catch (error) {
- completer.completeError(error);
- }
- }
- @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;
- completer.complete();
- } catch (err) {
- completer.completeError(err);
- }
- }
- @override
- Uint8List? get data => downloadItem.data;
- @override
- ValueNotifier<double> get progress => downloadItem.progress;
- }
|