优化备份卡顿问题

This commit is contained in:
DelLevin-Home
2026-07-08 01:08:28 +08:00
parent 75f735282f
commit f8ea982636
3 changed files with 104 additions and 61 deletions

View File

@@ -426,6 +426,7 @@ class _CloudSheetContentState extends State<_CloudSheetContent> {
if (direction == SyncDirection.upload) { if (direction == SyncDirection.upload) {
// 上传:先打包,再上传 // 上传:先打包,再上传
setState(() => _syncStep = '正在打包数据...'); setState(() => _syncStep = '正在打包数据...');
await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画
final exportResult = await WebDAVService.instance.exportLocalData(); final exportResult = await WebDAVService.instance.exportLocalData();
if (!exportResult.success || exportResult.zipPath == null) { if (!exportResult.success || exportResult.zipPath == null) {
if (mounted) { if (mounted) {
@@ -436,6 +437,7 @@ class _CloudSheetContentState extends State<_CloudSheetContent> {
} }
if (!mounted) return; if (!mounted) return;
setState(() => _syncStep = '正在上传到云端...'); setState(() => _syncStep = '正在上传到云端...');
await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画
final result = await WebDAVService.instance.uploadExportedData(exportResult); final result = await WebDAVService.instance.uploadExportedData(exportResult);
if (result.success && result.needReload && mounted) { if (result.success && result.needReload && mounted) {
final provider = context.read<AppProvider>(); final provider = context.read<AppProvider>();
@@ -457,6 +459,7 @@ class _CloudSheetContentState extends State<_CloudSheetContent> {
} else { } else {
// 下载 // 下载
setState(() => _syncStep = '正在从云端下载...'); setState(() => _syncStep = '正在从云端下载...');
await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画
final config = await WebDAVService.instance.getConfig(); final config = await WebDAVService.instance.getConfig();
if (config == null) { if (config == null) {
if (mounted) { if (mounted) {

View File

@@ -131,6 +131,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
if (_syncDirection == SyncDirection.upload) { if (_syncDirection == SyncDirection.upload) {
// 上传:先打包再上传,分步显示 // 上传:先打包再上传,分步显示
setState(() => _syncStep = '正在打包数据...'); setState(() => _syncStep = '正在打包数据...');
await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画
final exportResult = await WebDAVService.instance.exportLocalData(); final exportResult = await WebDAVService.instance.exportLocalData();
if (!exportResult.success || exportResult.zipPath == null) { if (!exportResult.success || exportResult.zipPath == null) {
if (mounted) { if (mounted) {
@@ -141,10 +142,12 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
} }
if (!mounted) return; if (!mounted) return;
setState(() => _syncStep = '正在上传到云端...'); setState(() => _syncStep = '正在上传到云端...');
await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画
result = await WebDAVService.instance.uploadExportedData(exportResult); result = await WebDAVService.instance.uploadExportedData(exportResult);
} else { } else {
// 下载 // 下载
setState(() => _syncStep = '正在从云端下载...'); setState(() => _syncStep = '正在从云端下载...');
await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画
result = await WebDAVService.instance.syncData(direction: SyncDirection.download); result = await WebDAVService.instance.syncData(direction: SyncDirection.download);
} }

View File

@@ -19,8 +19,9 @@ class BackupService {
// ─── 共享导出逻辑 ───────────────────────────────────── // ─── 共享导出逻辑 ─────────────────────────────────────
/// 收集所有表数据和图片,构建 ZIP 字节 /// 收集所有表数据和图片,构建 ZIP 文件
Future<_ExportData> _buildExportData() async { Future<_ExportData> _buildExportData() async {
// 阶段1主线程收集数据DB 查询、SharedPreferences 需主线程)
final db = await DatabaseHelper.instance.database; final db = await DatabaseHelper.instance.database;
final movies = await db.query('movies'); final movies = await db.query('movies');
@@ -109,69 +110,25 @@ class BackupService {
}, },
}; };
// 创建 ZIP逐文件写入磁盘避免全部加载到内存 // 阶段2后台 isolate 执行 JSON 编码 + ZIP 压缩(避免阻塞主线程动画
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();
final tempZipPath = path.join(tempDir.path, 'mooknote_backup_temp.zip'); final appDir = await getApplicationDocumentsDirectory();
final encoder = ZipFileEncoder();
encoder.create(tempZipPath);
try { final result = await compute(_buildZipInIsolate, _ZipComputeParams(
// data.json backupData: backupData,
final jsonString = const JsonEncoder.withIndent(' ').convert(backupData); imagePaths: imagePaths.toList(),
final jsonBytes = Uint8List.fromList(utf8.encode(jsonString)); tempDirPath: tempDir.path,
final dataFile = File(path.join(tempDir.path, 'mooknote_data.json')); appDirPath: appDir.path,
await dataFile.writeAsBytes(jsonBytes); ));
encoder.addFile(dataFile, 'data.json');
await dataFile.delete();
int imageCount = 0; return _ExportData(
final appDir = await getApplicationDocumentsDirectory(); zipPath: result.zipPath,
final imagesRoot = path.join(appDir.path, 'images'); movieCount: movies.length,
bookCount: books.length,
for (final imagePath in imagePaths) { noteCount: notes.length,
final file = File(imagePath); imageCount: result.imageCount,
if (await file.exists()) { epubCount: result.epubCount,
String relativePath; );
if (imagePath.startsWith(imagesRoot)) {
relativePath = imagePath.substring(imagesRoot.length + 1);
} else {
relativePath = path.basename(imagePath);
}
encoder.addFile(file, 'images/$relativePath');
imageCount++;
}
}
// 收集 epub_books 目录下的 epub 文件
int epubCount = 0;
final epubRoot = path.join(appDir.path, 'epub_books');
final epubDir = Directory(epubRoot);
if (await epubDir.exists()) {
await for (final entity in epubDir.list(recursive: true)) {
if (entity is File) {
final relativePath = entity.path.substring(epubRoot.length + 1);
encoder.addFile(entity, 'epub_books/$relativePath');
epubCount++;
}
}
}
encoder.close();
// 返回 zip 文件路径,不再读入内存
return _ExportData(
zipPath: tempZipPath,
movieCount: movies.length,
bookCount: books.length,
noteCount: notes.length,
imageCount: imageCount,
epubCount: epubCount,
);
} catch (e) {
encoder.close();
try { await File(tempZipPath).delete(); } catch (_) {}
rethrow;
}
} }
// ─── 手动导出 ───────────────────────────────────────── // ─── 手动导出 ─────────────────────────────────────────
@@ -912,3 +869,83 @@ class ImportResult {
return stats!.entries.map((e) => '${e.key}: ${e.value}').join(''); return stats!.entries.map((e) => '${e.key}: ${e.value}').join('');
} }
} }
// ─── compute isolate 参数和函数 ──────────────────────────
class _ZipComputeParams {
final Map<String, dynamic> backupData;
final List<String> imagePaths;
final String tempDirPath;
final String appDirPath;
_ZipComputeParams({
required this.backupData,
required this.imagePaths,
required this.tempDirPath,
required this.appDirPath,
});
}
class _ZipComputeResult {
final String? zipPath;
final int imageCount;
final int epubCount;
_ZipComputeResult({this.zipPath, required this.imageCount, required this.epubCount});
}
/// 在后台 isolate 中执行 JSON 编码 + ZIP 压缩,避免阻塞主线程
_ZipComputeResult _buildZipInIsolate(_ZipComputeParams params) {
final tempZipPath = path.join(params.tempDirPath, 'mooknote_backup_temp.zip');
final encoder = ZipFileEncoder();
encoder.create(tempZipPath);
try {
// data.json
final jsonString = const JsonEncoder.withIndent(' ').convert(params.backupData);
final jsonBytes = Uint8List.fromList(utf8.encode(jsonString));
final dataFile = File(path.join(params.tempDirPath, 'mooknote_data.json'));
dataFile.writeAsBytesSync(jsonBytes);
encoder.addFile(dataFile, 'data.json');
dataFile.deleteSync();
int imageCount = 0;
final imagesRoot = path.join(params.appDirPath, 'images');
for (final imagePath in params.imagePaths) {
final file = File(imagePath);
if (file.existsSync()) {
String relativePath;
if (imagePath.startsWith(imagesRoot)) {
relativePath = imagePath.substring(imagesRoot.length + 1);
} else {
relativePath = path.basename(imagePath);
}
encoder.addFile(file, 'images/$relativePath');
imageCount++;
}
}
// 收集 epub_books 目录下的 epub 文件
int epubCount = 0;
final epubRoot = path.join(params.appDirPath, 'epub_books');
final epubDir = Directory(epubRoot);
if (epubDir.existsSync()) {
for (final entity in epubDir.listSync(recursive: true)) {
if (entity is File) {
final relativePath = entity.path.substring(epubRoot.length + 1);
encoder.addFile(entity, 'epub_books/$relativePath');
epubCount++;
}
}
}
encoder.close();
return _ZipComputeResult(zipPath: tempZipPath, imageCount: imageCount, epubCount: epubCount);
} catch (e) {
encoder.close();
try { File(tempZipPath).deleteSync(); } catch (_) {}
rethrow;
}
}