diff --git a/lib/foundation/cache_manager.dart b/lib/foundation/cache_manager.dart index 9d7f42e6..ac85da39 100644 --- a/lib/foundation/cache_manager.dart +++ b/lib/foundation/cache_manager.dart @@ -1,7 +1,7 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:crypto/crypto.dart'; -import 'package:flutter/foundation.dart'; import 'package:pica_comic/foundation/app.dart'; import 'package:pica_comic/tools/io_extensions.dart'; import 'package:sqlite3/sqlite3.dart'; @@ -44,16 +44,32 @@ class CacheManager { } catch (e) { // ignore } - compute((path) => Directory(path).size, cachePath) - .then((value) => _currentSize = value); + _initCurrentSize(); } factory CacheManager() => instance ??= CacheManager._create(); + /// 异步初始化目录大小,使用安全的 _calcDirSize 避免 isolate OOM + Future _initCurrentSize() async { + try { + _currentSize = await _calcDirSize(cachePath); + } catch (e) { + // 初始化失败回退为 0 + _currentSize = 0; + } + } + /// set cache size limit in MB + /// 修改 4:缩小限制时立即触发清理 void setLimitSize(int size){ - _limitSize = size * 1024 * 1024; - } + _limitSize = size * 1024 * 1024; + if (_currentSize != null && _currentSize! > _limitSize) { + checkCache().catchError((_) { + // ignore: checkCache 失败不影响 setLimitSize,下次写入或手动触发会重试 + }); + } + } + void setType(String key, String? type){ _db.execute(''' @@ -75,28 +91,38 @@ class CacheManager { } Future writeCache(String key, Uint8List data, [int duration = 7 * 24 * 60 * 60 * 1000]) async{ - this.dir++; - this.dir %= 100; - var dir = this.dir; - var name = md5.convert(Uint8List.fromList(key.codeUnits)).toString(); - var file = File('$cachePath/$dir/$name'); - while(await file.exists()){ - name = md5.convert(Uint8List.fromList(name.codeUnits)).toString(); - file = File('$cachePath/$dir/$name'); - } + this.dir++; + this.dir %= 100; + var dir = this.dir; + var name = md5.convert(Uint8List.fromList(key.codeUnits)).toString(); + var file = File('$cachePath/$dir/$name'); + while(await file.exists()){ + name = md5.convert(Uint8List.fromList(name.codeUnits)).toString(); + file = File('$cachePath/$dir/$name'); + } + + // 先写入 DB 记录,再写文件:确保 DB 是文件的权威来源 + var expires = DateTime.now().millisecondsSinceEpoch + duration; + _db.execute(''' + INSERT OR REPLACE INTO cache (key, dir, name, expires) VALUES (?, ?, ?, ?) + ''', [key, dir.toString(), name, expires]); + + try { await file.create(recursive: true); await file.writeAsBytes(data); - var expires = DateTime.now().millisecondsSinceEpoch + duration; - _db.execute(''' - INSERT OR REPLACE INTO cache (key, dir, name, expires) VALUES (?, ?, ?, ?) - ''', [key, dir.toString(), name, expires]); - if(_currentSize != null) { - _currentSize = _currentSize! + data.length; - } - if(_currentSize != null && _currentSize! > _limitSize){ + } catch (e) { + // 文件写入失败,回滚 DB 记录,防止 DB 中存在无对应文件的记录 + _db.execute('DELETE FROM cache WHERE key = ?', [key]); + rethrow; + } + + if (_currentSize != null) { + _currentSize = _currentSize! + data.length; + if (_currentSize! > _limitSize) { await checkCache(); } } +} Future openWrite(String key) async{ this.dir++; @@ -120,113 +146,163 @@ class CacheManager { if(res.isEmpty){ return null; } - var row = res.first; - var dir = row[1] as String; - var name = row[2] as String; - var file = File('$cachePath/$dir/$name'); - if(await file.exists()){ - return file.path; + try { + var row = res.first; + var dir = row[1] as String; + var name = row[2] as String; + var file = File('$cachePath/$dir/$name'); + if(await file.exists()){ + return file.path; + } + } catch (_) { + // 权限不足或文件系统异常时返回 null } return null; } bool _isChecking = false; + /// 修改 1:移除 compute,使用 _calcDirSize 避免 isolate OOM;加 try-finally 防止 _isChecking 锁死; + /// 添加 anyDeleted 防止空转死循环 Future checkCache() async{ if(_isChecking){ return; } _isChecking = true; - var res = _db.select(''' - SELECT * FROM cache - WHERE expires < ? - ''', [DateTime.now().millisecondsSinceEpoch]); - for(var row in res){ - var dir = row[1] as String; - var name = row[2] as String; - var file = File('$cachePath/$dir/$name'); - if(await file.exists()){ - await file.delete(); + try { + // 第一步:清理已过期的缓存条目 + var res = _db.select(''' + SELECT * FROM cache + WHERE expires < ? + ''', [DateTime.now().millisecondsSinceEpoch]); + for(var row in res){ + try { + var dir = row[1] as String; + var name = row[2] as String; + var file = File('$cachePath/$dir/$name'); + if(await file.exists()){ + await file.delete(); + } + } catch (_) { + // 权限不足或文件被外部删除时跳过 + } } + _db.execute(''' + DELETE FROM cache + WHERE expires < ? + ''', [DateTime.now().millisecondsSinceEpoch]); + + // 第二步:获取当前条目数 + // int count = 0; + // var res2 = _db.select('SELECT COUNT(*) FROM cache'); + // if(res2.isNotEmpty){ + // count = res2.first[0] as int; + // } + + // 第三步:在主 isolate 中直接计算目录大小(移除 compute,避免 isolate OOM) + _currentSize = await _calcDirSize(cachePath); + + // 第四步:循环清理直到满足限制 + // while (_currentSize! > _limitSize || count > 2000) { + while (_currentSize! > _limitSize) { + var res3 = _db.select(''' + SELECT * FROM cache + ORDER BY expires ASC + LIMIT 10 + '''); + bool anyDeleted = false; + for(var row in res3){ + try { + var key = row[0] as String; + var dir = row[1] as String; + var name = row[2] as String; + var file = File('$cachePath/$dir/$name'); + if(await file.exists()){ + var size = await file.length(); + await file.delete(); + _db.execute('DELETE FROM cache WHERE key = ?', [key]); + _currentSize = _currentSize! - size; + anyDeleted = true; + // if(_currentSize! <= _limitSize && count - 1 <= 2000){ + if(_currentSize! <= _limitSize){ + break; + } + } else { + _db.execute('DELETE FROM cache WHERE key = ?', [key]); + anyDeleted = true; + } + } catch (_) { + //// 权限不足或文件操作异常时跳过当前条目,继续处理下一个 + } + // count--; + } + if (!anyDeleted) { + break; // 防止死循环:数据库中还有记录但文件都已不存在 + } + } + } finally { + _isChecking = false; } - _db.execute(''' - DELETE FROM cache - WHERE expires < ? - ''', [DateTime.now().millisecondsSinceEpoch]); + } - int count = 0; - var res2 = _db.select(''' - SELECT COUNT(*) FROM cache - '''); - if(res2.isNotEmpty){ - count = res2.first[0] as int; + /// 修改 1:在主 isolate 中分批计算目录大小,避免 listSync(recursive: true) OOM + Future _calcDirSize(String path) async { + int total = 0; + final dir = Directory(path); + if (!dir.existsSync()) return 0; + final List entries = dir.listSync(followLinks: false);//防止目录包含软链接 / 符号链接(指向自身 / 上级目录),递归无限执行 + for (final entity in entries) { + try { + if (entity is File) { + total += await entity.length(); + } else if (entity is Directory) { + total += await _calcDirSize(entity.path); + } + } catch (_) { + // 权限不足或文件被删除时跳过 + } } + return total; + } - compute((path) => Directory(path).size, cachePath) - .then((value) => _currentSize = value); - - while((_currentSize != null && _currentSize! > _limitSize) || count > 2000){ + Future delete(String key) async{ var res = _db.select(''' SELECT * FROM cache - ORDER BY expires ASC - limit 10 - '''); - for(var row in res){ - var key = row[0] as String; - var dir = row[1] as String; - var name = row[2] as String; - var file = File('$cachePath/$dir/$name'); + WHERE key = ? + ''', [key]); + if(res.isEmpty){ + return; + } + var row = res.first; + var dir = row[1] as String; + var name = row[2] as String; + var file = File('$cachePath/$dir/$name'); + + try { if(await file.exists()){ - var size = await file.length(); + var fileSize = await file.length(); await file.delete(); - _db.execute(''' - DELETE FROM cache - WHERE key = ? - ''', [key]); - _currentSize = _currentSize! - size; - if(_currentSize! <= _limitSize){ - break; + // 只有文件真正删除成功,才清理 DB 和扣减大小 + _db.execute('DELETE FROM cache WHERE key = ?', [key]); + if (_currentSize != null) { + _currentSize = _currentSize! - fileSize; } } else { - _db.execute(''' - DELETE FROM cache - WHERE key = ? - ''', [key]); + // 文件已被外部删除,只清理 DB 记录 + _db.execute('DELETE FROM cache WHERE key = ?', [key]); } - count--; + } catch (_) { + // 文件操作失败,保留 DB 记录不删除 + // 下次 delete() 或 checkCache() 会重试 } } - _isChecking = false; - } - - Future delete(String key) async{ - var res = _db.select(''' - SELECT * FROM cache - WHERE key = ? - ''', [key]); - if(res.isEmpty){ - return; - } - var row = res.first; - var dir = row[1] as String; - var name = row[2] as String; - var file = File('$cachePath/$dir/$name'); - var fileSize = 0; - if(await file.exists()){ - fileSize = await file.length(); - await file.delete(); - } - _db.execute(''' - DELETE FROM cache - WHERE key = ? - ''', [key]); - if(_currentSize != null) { - _currentSize = _currentSize! - fileSize; - } - } Future clear() async { - await Directory(cachePath).delete(recursive: true); + try { + await Directory(cachePath).delete(recursive: true); + } catch (_) { + // 权限不足时忽略 + } Directory(cachePath).createSync(recursive: true); _db.execute(''' DELETE FROM cache @@ -245,24 +321,27 @@ class CacheManager { var name = row[2] as String; var file = File('$cachePath/$dir/$name'); var fileSize = 0; - if(await file.exists()){ - fileSize = await file.length(); - try { - await file.delete(); + try { + if(await file.exists()){ + fileSize = await file.length(); } - finally {} + } catch (_) { + // 权限不足或文件被外部删除时跳过 + fileSize = 0; } + await file.deleteIgnoreError(); _db.execute(''' DELETE FROM cache WHERE key = ? ''', [key]); - if(_currentSize != null) { + if (_currentSize != null) { _currentSize = _currentSize! - fileSize; } } } } +/// 修改 6:新增 _writtenBytes 追踪,修改 close()/cancel()/reset() 同步 _currentSize class CachingFile{ CachingFile._(this.key, this.dir, this.name, this.file); @@ -276,29 +355,56 @@ class CachingFile{ final List _buffer = []; + int _writtenBytes = 0; // 跟踪已写入磁盘的字节数 + Future writeBytes(List data) async{ _buffer.addAll(data); if(_buffer.length > 1024 * 1024){ await file.writeAsBytes(_buffer, mode: FileMode.append); + _writtenBytes += _buffer.length; _buffer.clear(); } } - Future close() async{ - if(_buffer.isNotEmpty){ - await file.writeAsBytes(_buffer, mode: FileMode.append); +Future close() async{ + if(_buffer.isNotEmpty){ + await file.writeAsBytes(_buffer, mode: FileMode.append); + _writtenBytes += _buffer.length; + _buffer.clear(); + } + CacheManager()._db.execute(''' + INSERT OR REPLACE INTO cache (key, dir, name, expires) VALUES (?, ?, ?, ?) + ''', [key, dir, name, DateTime.now().millisecondsSinceEpoch + 7 * 24 * 60 * 60 * 1000]); + // 追踪写入的缓存大小 + final cm = CacheManager(); + if (cm._currentSize != null) { + cm._currentSize = cm._currentSize! + _writtenBytes; + if (cm._currentSize! > cm._limitSize) { + await cm.checkCache(); } - CacheManager()._db.execute(''' - INSERT OR REPLACE INTO cache (key, dir, name, expires) VALUES (?, ?, ?, ?) - ''', [key, dir, name, DateTime.now().millisecondsSinceEpoch + 7 * 24 * 60 * 60 * 1000]); - await CacheManager().checkCache(); } +} Future cancel() async{ + // 如果已写入部分数据且 _currentSize 已追踪,需要扣减 + if (_writtenBytes > 0) { + final cm = CacheManager(); + if (cm._currentSize != null) { + cm._currentSize = cm._currentSize! - _writtenBytes; + } + } await file.deleteIgnoreError(); } void reset() { + // 重置前扣减已追踪的大小 + if (_writtenBytes > 0) { + final cm = CacheManager(); + if (cm._currentSize != null) { + cm._currentSize = cm._currentSize! - _writtenBytes; + } + } + _writtenBytes = 0; _buffer.clear(); if(file.existsSync()) { file.deleteSync();