Browse Source

commit before rebuild piece group

guoziyun 4 tháng trước cách đây
mục cha
commit
4d1de7be6b

+ 8 - 0
CHANGELOG.md

@@ -17,3 +17,11 @@
 - banner广告优化,Key 隔离,减少潜在风险
 - 修复切换到任务列表,背景音乐pause后又再次播放的bug, 提升了应用前后台切换性能(main.dart中使用了GlobalKey的原因)
 - 其他优化
+
+## 1.0.9+9
+
+- 进入拼图初始化优化, 如果图片损坏,自动删除磁盘缓存并尝试重新下载,避免锁死在这一关
+- download优化,增加防抖;缓存文件写入优化, 先写入tmp文件,再重命名,保证是原子操作,避免写入“半截”文件的可能性
+- "下一关"按钮防抖和安全pop up优化(根据firebase报错的修改)
+- 内存分级, 低内存设备不展示banner广告
+- 启动优化, 启动main中所有堵塞性的await, 改为立即启动ui,后台并行初始化firebase,本地存储等await动作 -资源竞争优化: 关卡结束点击next, 立即pop up返回主页, 不再等待插屏广告结果再次回到play页面, 这样的好处是可以立即释放play界面资源,为插屏广告腾出内存空间。 用户体验也会更好,广告结束直接就在首页,不会闪

+ 547 - 0
MEMORY_OPTIMIZATION_GUIDE.md

@@ -0,0 +1,547 @@
+# 内存管理优化指南
+
+## 问题分析
+
+当前应用的内存问题主要集中在以下几个方面:
+
+1. **ui.Image 对象未及时释放** - 每个关卡占用 15-30MB
+2. **DownloadItem 持有原始图片数据** - 额外占用 5-15MB
+3. **AnimationController 泄漏** - 每个页面 7 个控制器
+4. **Picture 对象累积** - backgroundPicture 和 cardPicture
+
+---
+
+## 优化方案 1: Board 资源立即释放
+
+### 当前代码 (board_play.dart)
+
+```dart
+class _BoardPlayState extends State<BoardPlay> {
+  Board? board;
+  
+  @override
+  void dispose() {
+    board?.dispose();  // ⚠️ 可能太晚
+    super.dispose();
+  }
+  
+  void _onSuccess() {
+    data.workDone(widget.item);
+    // ❌ 资源仍然占用内存
+  }
+}
+```
+
+### 优化后代码
+
+```dart
+class _BoardPlayState extends State<BoardPlay> {
+  Board? board;
+  Timer? _resourceCleanupTimer;
+  
+  @override
+  void dispose() {
+    _resourceCleanupTimer?.cancel();
+    _cleanupBoardResources();
+    super.dispose();
+  }
+  
+  void _cleanupBoardResources() {
+    if (board != null) {
+      _log.info('Cleaning up board resources');
+      board!.dispose();
+      board = null;
+    }
+  }
+  
+  void _onSuccess() {
+    _log.info('Level completed, scheduling resource cleanup');
+    data.workDone(widget.item);
+    
+    // ✅ 延迟 2 秒后释放资源(等待动画完成)
+    _resourceCleanupTimer = Timer(Duration(seconds: 2), () {
+      if (mounted) {
+        _cleanupBoardResources();
+        // ✅ 强制触发垃圾回收(仅在 debug 模式)
+        if (kDebugMode) {
+          _log.info('Requesting garbage collection');
+        }
+      }
+    });
+  }
+  
+  // ✅ 页面退出时立即清理
+  void _onWillPop(bool didPop, dynamic result) async {
+    _log.info('Page popping, cleaning up resources');
+    _cleanupBoardResources();
+    
+    if (!didPop && mounted) {
+      Navigator.of(context).pop();
+    }
+  }
+}
+```
+
+---
+
+## 优化方案 2: Board.dispose() 增强
+
+### 当前代码 (board.dart)
+
+```dart
+class Board {
+  final ui.Image image;
+  ui.Picture? backgroundPicture;
+  ui.Picture? cardPicture;
+  
+  dispose() {
+    backgroundPicture?.dispose();
+    backgroundPicture = null;
+    cardPicture?.dispose();
+    cardPicture = null;
+    image.dispose();  // ⚠️ 最后才释放
+    boardNotifier.dispose();
+  }
+}
+```
+
+### 优化后代码
+
+```dart
+class Board {
+  final ui.Image image;
+  ui.Picture? backgroundPicture;
+  ui.Picture? cardPicture;
+  bool _isDisposed = false;
+  
+  dispose() {
+    if (_isDisposed) {
+      _log.warning('Board already disposed');
+      return;
+    }
+    
+    _log.info('Disposing board resources');
+    _isDisposed = true;
+    
+    // ✅ 按照占用内存大小顺序释放(先释放大的)
+    
+    // 1. 释放 Image (最大,10-20MB)
+    try {
+      image.dispose();
+      _log.info('Image disposed');
+    } catch (e) {
+      _log.warning('Failed to dispose image: $e');
+    }
+    
+    // 2. 释放 Pictures (中等,5-10MB)
+    try {
+      backgroundPicture?.dispose();
+      backgroundPicture = null;
+      _log.info('Background picture disposed');
+    } catch (e) {
+      _log.warning('Failed to dispose background picture: $e');
+    }
+    
+    try {
+      cardPicture?.dispose();
+      cardPicture = null;
+      _log.info('Card picture disposed');
+    } catch (e) {
+      _log.warning('Failed to dispose card picture: $e');
+    }
+    
+    // 3. 清空 pieces 列表
+    for (var piece in pieces) {
+      piece.path = null;
+      piece.outLinePath = null;
+      piece.innerLinePath = null;
+      piece.group = null;
+    }
+    pieces.clear();
+    
+    // 4. 清空 groups
+    backupGroups.clear();
+    
+    // 5. 释放 notifier
+    try {
+      boardNotifier.dispose();
+    } catch (e) {
+      _log.warning('Failed to dispose boardNotifier: $e');
+    }
+    
+    _log.info('Board disposal complete');
+  }
+}
+```
+
+---
+
+## 优化方案 3: DownloadItem 内存优化
+
+### 当前代码 (download.dart)
+
+```dart
+class DownloadItem {
+  Uint8List? _data;  // ❌ 持有原始数据
+  
+  Future<Uint8List> ensureDataLoaded() async {
+    if (_data != null) return _data!;
+    final file = await localFile(cachePath);
+    _data = await file.readAsBytes();
+    return _data!;
+  }
+}
+```
+
+### 优化后代码
+
+```dart
+class DownloadItem {
+  Uint8List? _data;
+  DateTime? _lastAccessTime;
+  static const _dataRetentionDuration = Duration(minutes: 5);
+  Timer? _cleanupTimer;
+  
+  Future<Uint8List> ensureDataLoaded() async {
+    _lastAccessTime = DateTime.now();
+    
+    if (_data != null) {
+      _log.info('Data cache hit for $cachePath');
+      _scheduleCleanup();
+      return _data!;
+    }
+    
+    _log.info('Loading data from disk: $cachePath');
+    final file = await localFile(cachePath);
+    _data = await file.readAsBytes();
+    
+    // ✅ 调度自动清理
+    _scheduleCleanup();
+    
+    return _data!;
+  }
+  
+  // ✅ 自动清理未使用的数据
+  void _scheduleCleanup() {
+    _cleanupTimer?.cancel();
+    _cleanupTimer = Timer(_dataRetentionDuration, () {
+      if (_data != null) {
+        final timeSinceLastAccess = DateTime.now().difference(_lastAccessTime!);
+        if (timeSinceLastAccess >= _dataRetentionDuration) {
+          _log.info('Auto-cleaning unused data: $cachePath');
+          _data = null;
+        }
+      }
+    });
+  }
+  
+  // ✅ 手动清理
+  void clearData() {
+    _cleanupTimer?.cancel();
+    _data = null;
+    _log.info('Manually cleared data: $cachePath');
+  }
+  
+  dispose() {
+    _isDisposed = true;
+    _cleanupTimer?.cancel();
+    _data = null;
+    subscription?.cancel();
+    client?.close();
+  }
+}
+```
+
+---
+
+## 优化方案 4: 完成关卡后清理缓存
+
+### 当前代码 (data.dart)
+
+```dart
+class Data {
+  void workDone(ListItem item, {Duration? timeSpent}) {
+    final newWork = Work.fromListItem(item, timeSpent: timeSpent);
+    final updatedWorks = [...completedWorks.value, newWork];
+    completedWorks.value = updatedWorks;
+    _persistence.completedWorks = updatedWorks;
+    
+    _clearCompletedItemCache(item);  // ✅ 已有,但可以增强
+  }
+}
+```
+
+### 优化后代码
+
+```dart
+class Data {
+  void workDone(ListItem item, {Duration? timeSpent}) {
+    final newWork = Work.fromListItem(item, timeSpent: timeSpent);
+    final updatedWorks = [...completedWorks.value, newWork];
+    completedWorks.value = updatedWorks;
+    _persistence.completedWorks = updatedWorks;
+    
+    // ✅ 立即清理缓存
+    _clearCompletedItemCache(item);
+    
+    // ✅ 清理内存中的下载项
+    _clearDownloadItemFromMemory(item);
+  }
+  
+  void _clearDownloadItemFromMemory(ListItem item) {
+    if (item is RemoteItem) {
+      try {
+        final downloadItem = Download()._cache[ApiHelper.imageUri(item.id, 'high')];
+        if (downloadItem != null) {
+          _log.info('Clearing download item from memory: ${item.id}');
+          downloadItem.clearData();
+          Download()._cache.remove(ApiHelper.imageUri(item.id, 'high'));
+        }
+      } catch (e) {
+        _log.warning('Failed to clear download item: $e');
+      }
+    }
+  }
+  
+  void _clearCompletedItemCache(ListItem item) async {
+    // 删除进度 JSON
+    try {
+      final jsonFile = await localFile(item.jsonPath);
+      if (await jsonFile.exists()) {
+        await jsonFile.delete();
+        _log.info('Cleared JSON cache: ${item.jsonPath}');
+      }
+    } catch (e) {
+      _log.severe('Failed to clear JSON cache: $e');
+    }
+
+    // 删除图片缓存
+    if (item is RemoteItem) {
+      try {
+        final imageFile = await localFile(item.cachePath);
+        if (await imageFile.exists()) {
+          await imageFile.delete();
+          _log.info('Cleared image cache: ${item.cachePath}');
+        }
+        
+        final tmpFile = await localFile('${item.cachePath}.tmp');
+        if (await tmpFile.exists()) {
+          await tmpFile.delete();
+        }
+      } catch (e) {
+        _log.severe('Failed to clear image cache: $e');
+      }
+    }
+  }
+}
+```
+
+---
+
+## 优化方案 5: 内存监控和自动清理
+
+### 新增工具类 (lib/utils/memory_monitor.dart)
+
+```dart
+import 'dart:async';
+import 'dart:io';
+import 'package:flutter/foundation.dart';
+import 'package:logging/logging.dart';
+
+final Logger _log = Logger('memory_monitor.dart');
+
+class MemoryMonitor {
+  static final MemoryMonitor _instance = MemoryMonitor._internal();
+  factory MemoryMonitor() => _instance;
+  MemoryMonitor._internal();
+  
+  Timer? _monitorTimer;
+  int _highMemoryWarningCount = 0;
+  
+  // 内存阈值 (MB)
+  static const int warningThreshold = 150;
+  static const int criticalThreshold = 200;
+  
+  // 回调函数
+  Function()? onHighMemory;
+  Function()? onCriticalMemory;
+  
+  void startMonitoring({
+    Duration interval = const Duration(seconds: 30),
+    Function()? onHighMemory,
+    Function()? onCriticalMemory,
+  }) {
+    this.onHighMemory = onHighMemory;
+    this.onCriticalMemory = onCriticalMemory;
+    
+    _monitorTimer?.cancel();
+    _monitorTimer = Timer.periodic(interval, (_) => _checkMemory());
+    
+    _log.info('Memory monitoring started');
+  }
+  
+  void stopMonitoring() {
+    _monitorTimer?.cancel();
+    _log.info('Memory monitoring stopped');
+  }
+  
+  void _checkMemory() {
+    if (!Platform.isAndroid && !Platform.isIOS) return;
+    
+    try {
+      final rss = ProcessInfo.currentRss;
+      final memoryMB = rss / (1024 * 1024);
+      
+      _log.info('Current memory usage: ${memoryMB.toStringAsFixed(1)} MB');
+      
+      if (memoryMB > criticalThreshold) {
+        _log.severe('CRITICAL memory usage: ${memoryMB.toStringAsFixed(1)} MB');
+        _highMemoryWarningCount++;
+        onCriticalMemory?.call();
+        
+        // 触发紧急清理
+        _emergencyCleanup();
+      } else if (memoryMB > warningThreshold) {
+        _log.warning('HIGH memory usage: ${memoryMB.toStringAsFixed(1)} MB');
+        _highMemoryWarningCount++;
+        onHighMemory?.call();
+      } else {
+        _highMemoryWarningCount = 0;
+      }
+      
+      // 上报到 Firebase
+      if (kReleaseMode && memoryMB > warningThreshold) {
+        FirebaseHelper.logEvent('high_memory_usage', {
+          'memory_mb': memoryMB.round(),
+          'threshold': memoryMB > criticalThreshold ? 'critical' : 'warning',
+        });
+      }
+    } catch (e) {
+      _log.warning('Failed to check memory: $e');
+    }
+  }
+  
+  void _emergencyCleanup() {
+    _log.warning('Executing emergency memory cleanup');
+    
+    try {
+      // 1. 清理下载缓存
+      Download()._cache.forEach((key, item) {
+        item.clearData();
+      });
+      
+      // 2. 清理图片缓存(如果有全局缓存)
+      // imageCache.clear();
+      // imageCache.clearLiveImages();
+      
+      _log.info('Emergency cleanup completed');
+    } catch (e) {
+      _log.severe('Emergency cleanup failed: $e');
+    }
+  }
+  
+  // 获取当前内存使用情况
+  static double getCurrentMemoryMB() {
+    try {
+      final rss = ProcessInfo.currentRss;
+      return rss / (1024 * 1024);
+    } catch (e) {
+      return 0;
+    }
+  }
+}
+
+// 使用示例:在 main.dart 中启动监控
+/*
+void main() {
+  runApp(MyApp());
+  
+  // 启动内存监控
+  MemoryMonitor().startMonitoring(
+    onHighMemory: () {
+      _log.warning('High memory detected, consider cleanup');
+    },
+    onCriticalMemory: () {
+      _log.severe('Critical memory, forcing cleanup');
+    },
+  );
+}
+*/
+```
+
+---
+
+## 测试验证
+
+### 内存测试脚本
+
+```dart
+// 在 BoardPlay 中添加内存日志
+class _BoardPlayState extends State<BoardPlay> {
+  @override
+  void initState() {
+    super.initState();
+    _logMemoryUsage('initState');
+  }
+  
+  @override
+  void dispose() {
+    _logMemoryUsage('dispose (before cleanup)');
+    _cleanupBoardResources();
+    _logMemoryUsage('dispose (after cleanup)');
+    super.dispose();
+  }
+  
+  void _onSuccess() {
+    _logMemoryUsage('onSuccess (before cleanup)');
+    data.workDone(widget.item);
+    
+    Timer(Duration(seconds: 2), () {
+      _cleanupBoardResources();
+      _logMemoryUsage('onSuccess (after cleanup)');
+    });
+  }
+  
+  void _logMemoryUsage(String label) {
+    final memoryMB = MemoryMonitor.getCurrentMemoryMB();
+    _log.info('[$label] Memory: ${memoryMB.toStringAsFixed(1)} MB');
+  }
+}
+```
+
+### 预期结果
+
+优化前:
+```
+[initState] Memory: 120.5 MB
+[onSuccess (before cleanup)] Memory: 145.2 MB
+[onSuccess (after cleanup)] Memory: 143.8 MB  ❌ 只释放了 1.4MB
+[dispose (before cleanup)] Memory: 145.0 MB
+[dispose (after cleanup)] Memory: 144.2 MB
+```
+
+优化后:
+```
+[initState] Memory: 120.5 MB
+[onSuccess (before cleanup)] Memory: 145.2 MB
+[onSuccess (after cleanup)] Memory: 125.3 MB  ✅ 释放了 19.9MB
+[dispose (before cleanup)] Memory: 125.5 MB
+[dispose (after cleanup)] Memory: 105.8 MB  ✅ 释放了 19.7MB
+```
+
+---
+
+## 总结
+
+通过以上优化,预期可以:
+
+1. **减少内存占用 50-70%**
+2. **LMK 率从 0.84% 降至 <0.2%**
+3. **Crash 率从 2.96% 降至 <1%**
+4. **连续玩 10 关后内存占用 < 150MB**
+
+关键点:
+- ✅ 立即释放已完成关卡的资源
+- ✅ 自动清理未使用的下载数据
+- ✅ 监控内存使用,触发紧急清理
+- ✅ 按内存占用大小顺序释放资源

+ 438 - 0
MEMORY_USAGE_ANALYSIS.md

@@ -0,0 +1,438 @@
+# BoardPlay 内存占用分析与优化方案
+
+## 🔴 当前问题
+
+**实测内存**: 400-600MB (Release 模式)
+**目标内存**: <150MB (Release 模式)
+**差距**: 需要减少 60-75% 的内存占用
+
+---
+
+## 📊 内存占用来源分析
+
+### 1. **ui.Image 对象** - 最大占用 (约 200-300MB)
+
+```dart
+// board_play.dart
+ui.Image image;  // 主图片: 1800x2700 RGBA = 19.4MB
+ui.Image cardImage;  // 卡牌图片: 多个 = 5-10MB
+
+// board.dart
+ui.Picture backgroundPicture;  // 背景 Picture: 20-30MB
+ui.Picture cardPicture;  // 卡牌 Picture: 5-10MB
+```
+
+**问题**:
+- 图片分辨率过高 (1800x2700)
+- RGBA 格式占用 4 bytes/pixel
+- Picture 对象额外占用内存
+
+**计算**:
+```
+主图片: 1800 × 2700 × 4 bytes = 19.4 MB
+25个碎片 Path: 25 × 0.5MB = 12.5 MB
+Picture 缓存: 20-30 MB
+总计: ~50-60 MB (理论值)
+实际: 200-300 MB (因为有多份拷贝和临时对象)
+```
+
+---
+
+### 2. **DownloadItem 数据** - 中等占用 (约 50-100MB)
+
+```dart
+// download.dart
+class DownloadItem {
+  Uint8List? _data;  // 原始图片数据: 5-15MB
+}
+
+// 问题: 可能同时缓存多个图片
+const maxCachedItems = 1;  // 但实际可能有多个
+```
+
+---
+
+### 3. **Flutter 引擎开销** - 固定占用 (约 50-100MB)
+
+- Dart VM
+- Skia 渲染引擎
+- 系统库
+
+---
+
+### 4. **其他占用** (约 50-100MB)
+
+- AnimationController (7个)
+- CustomPainter 缓存
+- 广告 SDK
+- 音频资源
+
+---
+
+## 🎯 优化方案
+
+### 优先级 P0: 立即优化 (预计减少 200-300MB)
+
+#### 1. 降低图片分辨率 (减少 100-150MB)
+
+**当前问题**:
+```dart
+// device.dart
+String get suggestedQuality {
+  if (isTablet) return "2400";  // 太高!
+  if (isLowEndDevice) return "1200";
+  return "1800";  // 默认太高!
+}
+```
+
+**优化方案**:
+```dart
+String get suggestedQuality {
+  // ✅ 基于实际屏幕分辨率计算
+  final screenWidth = screenSize.width;
+  final dpr = effectivePixelRatio;
+  final targetPixels = screenWidth * dpr * aspectRatio;
+  
+  if (isTablet) {
+    return targetPixels > 2000 ? "2000" : "1600";
+  }
+  
+  if (isLowEndDevice) {
+    return "1000";  // 降低到 1000
+  }
+  
+  // 普通设备:根据屏幕计算
+  if (targetPixels > 1600) return "1600";
+  if (targetPixels > 1200) return "1200";
+  return "1000";
+}
+```
+
+**预期效果**:
+- 1800 → 1200: 图片大小减少 56%
+- 内存占用: 19.4MB → 8.6MB (单张)
+- 总内存减少: ~100MB
+
+---
+
+#### 2. 使用 RGB 格式替代 RGBA (减少 25%)
+
+**优化方案**:
+```dart
+// 在 image_decoder.dart 中
+Future<ui.Image> _decodeImageInIsolate(_DecodeParams params) async {
+  final codec = await ui.instantiateImageCodec(
+    params.bytes,
+    targetWidth: params.targetWidth,
+    targetHeight: params.targetHeight,
+    allowUpscaling: params.allowUpscaling,
+  );
+
+  final frameInfo = await codec.getNextFrame();
+  final image = frameInfo.image;
+  
+  // ✅ 转换为 RGB 格式(如果图片不需要透明度)
+  if (!params.needsAlpha) {
+    return _convertToRGB(image);
+  }
+  
+  return image;
+}
+
+Future<ui.Image> _convertToRGB(ui.Image rgbaImage) async {
+  final recorder = ui.PictureRecorder();
+  final canvas = Canvas(recorder);
+  
+  // 绘制到不透明背景
+  canvas.drawColor(Colors.white, BlendMode.src);
+  canvas.drawImage(rgbaImage, Offset.zero, Paint());
+  
+  final picture = recorder.endRecording();
+  final img = await picture.toImage(
+    rgbaImage.width,
+    rgbaImage.height,
+  );
+  
+  rgbaImage.dispose();
+  picture.dispose();
+  
+  return img;
+}
+```
+
+**预期效果**:
+- RGBA (4 bytes) → RGB (3 bytes): 减少 25%
+- 8.6MB → 6.5MB
+
+---
+
+#### 3. 立即释放 Picture 对象 (减少 30-50MB)
+
+**当前问题**:
+```dart
+// board.dart
+ui.Picture? backgroundPicture;  // 一直持有
+ui.Picture? cardPicture;  // 一直持有
+```
+
+**优化方案**:
+```dart
+class Board {
+  ui.Picture? backgroundPicture;
+  ui.Picture? cardPicture;
+  
+  // ✅ 游戏开始后立即释放 Picture
+  void start() {
+    _status = BoardStatus.playing;
+    
+    // 发牌动画结束,立即释放 Picture
+    _releasePictures();
+    
+    invalidate();
+  }
+  
+  void _releasePictures() {
+    if (backgroundPicture != null) {
+      backgroundPicture!.dispose();
+      backgroundPicture = null;
+      _log.info('Background picture released');
+    }
+    
+    if (cardPicture != null) {
+      cardPicture!.dispose();
+      cardPicture = null;
+      _log.info('Card picture released');
+    }
+  }
+}
+```
+
+**预期效果**:
+- 减少 30-50MB 内存占用
+
+---
+
+### 优先级 P1: 重要优化 (预计减少 50-100MB)
+
+#### 4. 优化 Path 缓存
+
+**当前问题**:
+```dart
+// piece.dart
+class Piece {
+  Path? path;
+  Path? innerLinePath;
+  Path? outLinePath;
+  // 25个碎片 × 3个Path × 0.5MB = 37.5MB
+}
+```
+
+**优化方案**:
+```dart
+class Piece {
+  Path? _cachedPath;
+  Path? _cachedInnerLinePath;
+  Path? _cachedOutLinePath;
+  
+  // ✅ 只在需要时生成,用完立即释放
+  List<Path> generatePathsOnDemand() {
+    final paths = generatePaths();
+    
+    // 游戏进行中,不缓存 Path
+    if (board.status == BoardStatus.playing) {
+      return paths;
+    }
+    
+    // 只在动画时缓存
+    _cachedPath = paths[0];
+    _cachedInnerLinePath = paths[1];
+    _cachedOutLinePath = paths[2];
+    
+    return paths;
+  }
+  
+  void clearPathCache() {
+    _cachedPath = null;
+    _cachedInnerLinePath = null;
+    _cachedOutLinePath = null;
+  }
+}
+```
+
+---
+
+#### 5. 限制 AnimationController 数量
+
+**当前问题**:
+```dart
+// board_play.dart
+late AnimationController _moveAnimationController;
+late AnimationController _mergeAnimationController;
+late AnimationController _prepareAnimationController;
+late AnimationController dealingAnimationController;
+late AnimationController flipAnimationController;
+late AnimationController _successAnimationController;
+late AnimationController _hardModeBannerController;
+// 7个控制器
+```
+
+**优化方案**:
+```dart
+// 复用控制器
+late AnimationController _primaryController;  // 主要动画
+late AnimationController _secondaryController;  // 次要动画
+
+// 根据需要切换用途
+void _startMoveAnimation() {
+  _primaryController.duration = Duration(milliseconds: 200);
+  _primaryController.forward(from: 0.0);
+}
+```
+
+---
+
+### 优先级 P2: 进一步优化
+
+#### 6. 使用纹理压缩
+
+```dart
+// 使用 ETC2 或 ASTC 压缩格式
+// 需要在服务端预处理图片
+```
+
+#### 7. 分块加载大图
+
+```dart
+// 只加载可见区域
+// 适用于超大图片
+```
+
+---
+
+## 🔧 立即可应用的优化
+
+### 修改 1: device.dart
+
+```dart
+String get suggestedQuality {
+  if (isTablet) return "1600";  // 2400 → 1600
+  if (isLowEndDevice) return "1000";  // 1200 → 1000
+  return "1200";  // 1800 → 1200
+}
+```
+
+### 修改 2: board.dart
+
+```dart
+void start() {
+  _status = BoardStatus.playing;
+  
+  // ✅ 立即释放 Picture
+  backgroundPicture?.dispose();
+  backgroundPicture = null;
+  cardPicture?.dispose();
+  cardPicture = null;
+  
+  invalidate();
+}
+```
+
+### 修改 3: board_play.dart
+
+```dart
+void _onSuccess() {
+  // ... 现有代码
+  
+  // ✅ 立即清理资源
+  Timer(Duration(seconds: 1), () {
+    if (board != null) {
+      board!.dispose();
+      board = null;
+    }
+  });
+}
+```
+
+---
+
+## 📊 预期优化效果
+
+| 优化项 | 当前 | 优化后 | 减少 |
+|--------|------|--------|------|
+| 图片分辨率 | 1800px | 1200px | -100MB |
+| Picture 缓存 | 持有 | 释放 | -40MB |
+| RGB 格式 | RGBA | RGB | -30MB |
+| Path 缓存 | 全部 | 按需 | -20MB |
+| **总计** | **400-600MB** | **150-200MB** | **-60%** |
+
+---
+
+## ✅ 验证方法
+
+```dart
+// 在 board_play.dart 中添加
+@override
+void initState() {
+  super.initState();
+  MemoryMonitor.logMemoryUsage('BoardPlay init');
+}
+
+void _init() async {
+  MemoryMonitor.logMemoryUsage('Before image decode');
+  
+  // 解码图片
+  final images = await ImageDecoder.decodeImages(...);
+  
+  MemoryMonitor.logMemoryUsage('After image decode');
+  
+  // 创建 Board
+  board = await Board.create(...);
+  
+  MemoryMonitor.logMemoryUsage('After board create');
+}
+
+@override
+void dispose() {
+  MemoryMonitor.logMemoryUsage('Before dispose');
+  board?.dispose();
+  MemoryMonitor.logMemoryUsage('After dispose');
+  super.dispose();
+}
+```
+
+**预期日志**:
+```
+优化前:
+[BoardPlay init] Memory: 120 MB
+[After image decode] Memory: 350 MB  ❌
+[After board create] Memory: 450 MB  ❌
+[After dispose] Memory: 130 MB
+
+优化后:
+[BoardPlay init] Memory: 120 MB
+[After image decode] Memory: 180 MB  ✅
+[After board create] Memory: 200 MB  ✅
+[After dispose] Memory: 125 MB  ✅
+```
+
+---
+
+## 🎯 行动计划
+
+### 第一步 (立即执行,30分钟)
+1. 修改 `device.dart` 降低图片质量
+2. 修改 `board.dart` 释放 Picture
+3. 测试验证
+
+### 第二步 (1小时)
+1. 优化 `_onSuccess()` 立即清理
+2. 添加内存监控日志
+3. 测试多个关卡
+
+### 第三步 (2小时)
+1. 实现 RGB 格式转换
+2. 优化 Path 缓存策略
+3. 全面测试
+
+**预计总时间**: 3.5小时
+**预计效果**: 内存从 400-600MB 降至 150-200MB

+ 745 - 0
PERFORMANCE_OPTIMIZATION_REPORT.md

@@ -0,0 +1,745 @@
+# Jigsort Solitaire 性能优化报告
+
+## 当前性能指标
+- **User-perceived crash rate**: 2.96% ⚠️
+- **User-perceived ANR rate**: 1.52% ⚠️  
+- **User-perceived LMK rate**: 0.84% ⚠️
+- **Slow cold start rate**: 34.33% 🔴
+
+---
+
+## 🔴 严重问题 (Critical Issues)
+
+### 1. 主线程阻塞 - 导致ANR的主要原因
+
+#### 问题1.1: `main()` 函数中的同步初始化
+**位置**: `lib/main.dart:40-140`
+
+**问题**:
+```dart
+void main() async {
+  WidgetsFlutterBinding.ensureInitialized();
+  
+  // ❌ 阻塞主线程
+  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
+  await Persistence().initialize();  // SharedPreferences 初始化
+  RemoteConfig().initialize();       // 远程配置初始化
+  
+  Directory baseDir = await getApplicationDocumentsDirectory();
+  
+  if (Persistence().firstRun) {
+    final json = await loadJSONFromAsset('assets/builtin/${cfg.Config.firstId}.json');
+    await saveJson('work/${cfg.Config.firstId}.json', json);
+  }
+  
+  runApp(MyApp(baseDir: baseDir));
+}
+```
+
+**影响**: 
+- 启动时间增加 500-1500ms
+- 低端设备可能触发 ANR (>5秒)
+- 直接导致 **Slow cold start rate 34.33%**
+
+**优化方案**:
+```dart
+void main() {
+  WidgetsFlutterBinding.ensureInitialized();
+  
+  // ✅ 立即启动 UI,后台初始化
+  runApp(const MyApp());
+}
+
+class MyApp extends StatefulWidget {
+  @override
+  State<MyApp> createState() => _MyAppState();
+}
+
+class _MyAppState extends State<MyApp> {
+  bool _isInitialized = false;
+  
+  @override
+  void initState() {
+    super.initState();
+    _initializeAsync();
+  }
+  
+  Future<void> _initializeAsync() async {
+    // 并行初始化
+    await Future.wait([
+      _initFirebase(),
+      Persistence().initialize(),
+      _prepareFirstRunData(),
+    ]);
+    
+    setState(() => _isInitialized = true);
+  }
+  
+  Future<void> _initFirebase() async {
+    if (!kIsWeb && Platform.isAndroid) {
+      await Firebase.initializeApp(
+        options: DefaultFirebaseOptions.currentPlatform
+      );
+      // ... 错误处理配置
+    }
+  }
+  
+  @override
+  Widget build(BuildContext context) {
+    if (!_isInitialized) {
+      return MaterialApp(
+        home: Scaffold(
+          body: Center(child: CircularProgressIndicator()),
+        ),
+      );
+    }
+    
+    return MaterialApp(/* 正常UI */);
+  }
+}
+```
+
+**预期收益**: 
+- 启动时间减少 40-60%
+- ANR率降低至 <0.5%
+- Slow cold start rate 降至 <15%
+
+---
+
+#### 问题1.2: `BoardPlay._init()` 中的图片解码阻塞
+**位置**: `lib/play/board_play.dart:700-800`
+
+**问题**:
+```dart
+_init() async {
+  // ❌ 大图片解码在主线程,阻塞 UI
+  ui.Image image = await itemLoader.getImageBySize(
+    bestImageSize.width.round(), 
+    bestImageSize.height.round()
+  );
+  
+  // ❌ 卡牌图片解码也在主线程
+  final ByteData cardData = await rootBundle.load('assets/images/backcard_red.png');
+  final ui.Codec cardCodec = await ui.instantiateImageCodec(
+    cardData.buffer.asUint8List(),
+    targetWidth: bestCardImageSize.width.round(),
+    targetHeight: bestCardImageSize.height.round(),
+  );
+  final ui.FrameInfo cardFrameInfo = await cardCodec.getNextFrame();
+}
+```
+
+**影响**:
+- 进入游戏页面时卡顿 500-2000ms
+- 高分辨率图片 (2000x3000) 解码可能触发 ANR
+- 导致 **ANR rate 1.52%**
+
+**优化方案**:
+```dart
+// 使用 compute 在 isolate 中解码
+Future<ui.Image> _decodeImageInIsolate(Uint8List bytes, int width, int height) async {
+  return await compute(_decodeImage, {
+    'bytes': bytes,
+    'width': width,
+    'height': height,
+  });
+}
+
+static Future<ui.Image> _decodeImage(Map<String, dynamic> params) async {
+  final bytes = params['bytes'] as Uint8List;
+  final width = params['width'] as int;
+  final height = params['height'] as int;
+  
+  final codec = await ui.instantiateImageCodec(
+    bytes,
+    targetWidth: width,
+    targetHeight: height,
+  );
+  return (await codec.getNextFrame()).image;
+}
+
+_init() async {
+  // ✅ 并行解码,不阻塞主线程
+  final results = await Future.wait([
+    _decodeImageInIsolate(imageBytes, width, height),
+    _decodeImageInIsolate(cardBytes, cardWidth, cardHeight),
+  ]);
+  
+  final image = results[0];
+  final cardImage = results[1];
+}
+```
+
+**预期收益**:
+- 进入游戏页面流畅度提升 70%
+- ANR率降低至 <0.3%
+
+---
+
+### 2. 内存泄漏 - 导致崩溃和LMK的主要原因
+
+#### 问题2.1: `ui.Image` 未及时释放
+**位置**: `lib/play/board.dart`, `lib/models/download.dart`
+
+**问题**:
+```dart
+class Board {
+  final ui.Image image;  // ❌ 大图片常驻内存
+  ui.Picture? backgroundPicture;  // ❌ Picture 也占用大量内存
+  ui.Picture? cardPicture;
+  
+  dispose() {
+    backgroundPicture?.dispose();
+    cardPicture?.dispose();
+    image.dispose();  // ⚠️ 但调用时机可能太晚
+  }
+}
+
+class DownloadItem {
+  Uint8List? _data;  // ❌ 原始图片数据常驻内存
+}
+```
+
+**影响**:
+- 每个关卡占用 15-30MB 内存
+- 多次进入游戏后内存累积 100MB+
+- 导致 **LMK rate 0.84%** 和 **Crash rate 2.96%**
+
+**优化方案**:
+```dart
+// 1. 立即释放已完成关卡的资源
+class _BoardPlayState {
+  @override
+  void dispose() {
+    // ✅ 立即释放
+    board?.dispose();
+    board = null;
+    
+    // ✅ 清理下载缓存
+    itemLoader.dispose();
+    
+    super.dispose();
+  }
+}
+
+// 2. 优化 DownloadItem 内存管理
+class DownloadItem {
+  Uint8List? _data;
+  
+  Future<Uint8List> ensureDataLoaded() async {
+    if (_data != null) return _data!;
+    
+    // ✅ 从磁盘读取后立即使用,不长期持有
+    final file = await localFile(cachePath);
+    final bytes = await file.readAsBytes();
+    
+    // ⚠️ 不赋值给 _data,避免内存累积
+    return bytes;
+  }
+  
+  dispose() {
+    _data = null;  // ✅ 立即释放
+    subscription?.cancel();
+    client?.close();
+  }
+}
+
+// 3. 关卡完成后立即清理
+void _onSuccess() {
+  data.workDone(widget.item);
+  
+  // ✅ 立即释放当前关卡资源
+  Future.delayed(Duration(seconds: 2), () {
+    board?.dispose();
+    board = null;
+  });
+}
+```
+
+**预期收益**:
+- 内存占用减少 50-70%
+- LMK率降低至 <0.2%
+- Crash率降低至 <1%
+
+---
+
+#### 问题2.2: `AnimationController` 未正确释放
+**位置**: `lib/play/board_play.dart:150-250`
+
+**问题**:
+```dart
+class _BoardPlayState {
+  late AnimationController _moveAnimationController;
+  late AnimationController _mergeAnimationController;
+  late AnimationController _prepareAnimationController;
+  late AnimationController dealingAnimationController;
+  late AnimationController flipAnimationController;
+  late AnimationController _successAnimationController;
+  late AnimationController _hardModeBannerController;
+  
+  // ❌ 7个动画控制器,如果页面快速退出可能未完全释放
+}
+```
+
+**优化方案**:
+```dart
+@override
+void dispose() {
+  // ✅ 先停止所有动画
+  _moveAnimationController.stop();
+  _mergeAnimationController.stop();
+  _prepareAnimationController.stop();
+  dealingAnimationController.stop();
+  flipAnimationController.stop();
+  _successAnimationController.stop();
+  _hardModeBannerController.stop();
+  
+  // ✅ 移除监听器
+  _moveAnimationController.removeListener(_moveAnimationListener);
+  _moveAnimationController.removeStatusListener(_moveAnimationStatusListener);
+  
+  // ✅ 释放资源
+  _moveAnimationController.dispose();
+  _mergeAnimationController.dispose();
+  _prepareAnimationController.dispose();
+  dealingAnimationController.dispose();
+  flipAnimationController.dispose();
+  _successAnimationController.dispose();
+  _hardModeBannerController.dispose();
+  
+  // ✅ 清空引用
+  moveItems = null;
+  _mergeGroups = null;
+  
+  super.dispose();
+}
+```
+
+---
+
+### 3. 绘制性能问题 - 导致卡顿和ANR
+
+#### 问题3.1: `BoardPainter` 过度重绘
+**位置**: `lib/play/board_painter.dart:30-100`
+
+**问题**:
+```dart
+class BoardPainter extends CustomPainter {
+  BoardPainter({required this.board, required this.prepareAnimation}) 
+    : super(repaint: Listenable.merge([board.boardNotifier, prepareAnimation]));
+  
+  @override
+  void paint(Canvas canvas, Size size) {
+    // ❌ 每次 boardNotifier 变化都完全重绘
+    for (final piece in board.pieces) {
+      _drawPiece(canvas, size, piece);  // 可能绘制 25 个碎片
+    }
+  }
+  
+  @override
+  bool shouldRepaint(covariant BoardPainter oldDelegate) {
+    // ❌ 判断条件过于宽松
+    return oldDelegate.board.status != board.status;
+  }
+}
+```
+
+**影响**:
+- 每次拖动触发 60fps 重绘,每帧绘制 25 个碎片
+- 5x5 宫格时可能掉帧至 30fps
+- 导致 ANR 和用户体验差
+
+**优化方案**:
+```dart
+class BoardPainter extends CustomPainter {
+  // ✅ 使用 RepaintBoundary 隔离静态内容
+  @override
+  void paint(Canvas canvas, Size size) {
+    // 1. 静态背景只绘制一次 (已优化,使用 Picture)
+    if (board.backgroundPicture != null) {
+      canvas.drawPicture(board.backgroundPicture!);
+    }
+    
+    // 2. 只绘制变化的碎片
+    if (_draggingPiece != null) {
+      // ✅ 只重绘拖动的碎片和群组
+      _drawDraggingPieces(canvas, size);
+    } else {
+      // ✅ 正常绘制
+      for (final piece in board.pieces) {
+        _drawPiece(canvas, size, piece);
+      }
+    }
+  }
+  
+  @override
+  bool shouldRepaint(covariant BoardPainter oldDelegate) {
+    // ✅ 更精确的判断
+    return oldDelegate.board != board || 
+           oldDelegate.board.boardNotifier.value != board.boardNotifier.value;
+  }
+}
+
+// ✅ 在 Widget 层使用 RepaintBoundary
+Widget _buildPuzzleCanvas(double width, double height) {
+  return RepaintBoundary(  // ✅ 已有,保持
+    child: CustomPaint(
+      painter: BoardPainter(board: board!, prepareAnimation: _prepareAnimationController),
+      size: Size(width, height),
+      child: GestureDetector(/* ... */),
+    ),
+  );
+}
+```
+
+**预期收益**:
+- 拖动流畅度提升至稳定 60fps
+- CPU 占用降低 30-40%
+
+---
+
+#### 问题3.2: 路径生成未缓存
+**位置**: `lib/play/piece.dart:800-1200`
+
+**问题**:
+```dart
+class Piece {
+  Path? path;
+  Path? innerLinePath;
+  Path? outLinePath;
+  
+  List<Path> generatePaths({bool forceRecalculate = false}) {
+    // ❌ 每次绘制都可能重新生成路径
+    if (group == null && !forceRecalculate && path != null) {
+      return [path!, outLinePath!, innerLinePath!];
+    }
+    
+    // ⚠️ 复杂的路径生成逻辑
+    path = _generateClipPath(width, height, borders, board.cornerRadius);
+    outLinePath = _generateBorderPath(/* ... */);
+    innerLinePath = _generateBorderPath(/* ... */);
+  }
+}
+```
+
+**优化方案**:
+```dart
+class Piece {
+  Path? _cachedPath;
+  Path? _cachedOutLinePath;
+  Path? _cachedInnerLinePath;
+  int? _cachedBordersHash;  // ✅ 缓存边界状态
+  
+  List<Path> generatePaths({bool forceRecalculate = false}) {
+    final bordersHash = _computeBordersHash();
+    
+    // ✅ 只在边界状态变化时重新生成
+    if (!forceRecalculate && 
+        _cachedPath != null && 
+        _cachedBordersHash == bordersHash) {
+      return [_cachedPath!, _cachedOutLinePath!, _cachedInnerLinePath!];
+    }
+    
+    _cachedPath = _generateClipPath(/* ... */);
+    _cachedOutLinePath = _generateBorderPath(/* ... */);
+    _cachedInnerLinePath = _generateBorderPath(/* ... */);
+    _cachedBordersHash = bordersHash;
+    
+    return [_cachedPath!, _cachedOutLinePath!, _cachedInnerLinePath!];
+  }
+  
+  int _computeBordersHash() {
+    return (_hasTopBorder ? 1 : 0) |
+           (_hasRightBorder ? 2 : 0) |
+           (_hasBottomBorder ? 4 : 0) |
+           (_hasLeftBorder ? 8 : 0);
+  }
+}
+```
+
+---
+
+## 🟡 中等问题 (Medium Issues)
+
+### 4. 网络请求优化
+
+#### 问题4.1: 图片下载未限流
+**位置**: `lib/models/download.dart:15-50`
+
+**问题**:
+```dart
+const maxCachedItems = 1;  // ❌ 只缓存1个,导致频繁下载
+
+class Download {
+  DownloadItem download(String url, String cachePath) {
+    if (_cache[url] != null) {
+      return _cache[url]!;
+    } else {
+      final item = DownloadItem(url, cachePath);
+      _cache[url] = item;
+      return item;
+    }
+  }
+}
+```
+
+**优化方案**:
+```dart
+const maxCachedItems = 3;  // ✅ 增加缓存数量
+const maxConcurrentDownloads = 2;  // ✅ 限制并发下载
+
+class Download {
+  int _activeDownloads = 0;
+  final Queue<Completer<void>> _downloadQueue = Queue();
+  
+  Future<DownloadItem> download(String url, String cachePath) async {
+    // ✅ 限流控制
+    while (_activeDownloads >= maxConcurrentDownloads) {
+      final completer = Completer<void>();
+      _downloadQueue.add(completer);
+      await completer.future;
+    }
+    
+    _activeDownloads++;
+    try {
+      final item = DownloadItem(url, cachePath);
+      await item.loadCompleter.future;
+      return item;
+    } finally {
+      _activeDownloads--;
+      if (_downloadQueue.isNotEmpty) {
+        _downloadQueue.removeFirst().complete();
+      }
+    }
+  }
+}
+```
+
+---
+
+#### 问题4.2: `CachedRequest` 重复请求
+**位置**: `lib/models/cached_request.dart:50-100`
+
+**问题**:
+```dart
+class CachedRequest {
+  _remoteLoad() async {
+    // ❌ 每次都发起新请求,没有防抖
+    final response = await http.get(Uri.parse(url));
+  }
+  
+  @override
+  void didChangeAppLifecycleState(AppLifecycleState state) {
+    if (state == AppLifecycleState.resumed) {
+      // ❌ 每次恢复都刷新,可能过于频繁
+      refresh();
+    }
+  }
+}
+```
+
+**优化方案**:
+```dart
+class CachedRequest {
+  DateTime? _lastFetchTime;
+  static const _minRefreshInterval = Duration(minutes: 5);
+  
+  Future<void> refresh() async {
+    // ✅ 防抖:5分钟内不重复请求
+    if (_lastFetchTime != null &&
+        DateTime.now().difference(_lastFetchTime!) < _minRefreshInterval) {
+      _log.info('Skipping refresh, too soon since last fetch');
+      return;
+    }
+    
+    await _remoteLoad();
+  }
+  
+  @override
+  void didChangeAppLifecycleState(AppLifecycleState state) {
+    if (state == AppLifecycleState.resumed) {
+      // ✅ 智能刷新:只在数据过期时刷新
+      final timeSinceLastFetch = _lastFetchTime != null
+          ? DateTime.now().difference(_lastFetchTime!)
+          : Duration(days: 1);
+      
+      if (timeSinceLastFetch > Duration(hours: 1)) {
+        refresh();
+      }
+    }
+  }
+}
+```
+
+---
+
+### 5. 数据持久化优化
+
+#### 问题5.1: `SharedPreferences` 频繁写入
+**位置**: `lib/persistence/persistence.dart:200-250`
+
+**问题**:
+```dart
+class PreferencesValue<T> {
+  set value(T v) {
+    _value = v;
+    // ❌ 每次赋值都立即写入磁盘
+    if (_value is bool) {
+      unawaited(prefs.setBool(key, _value as bool));
+    }
+    // ...
+  }
+}
+```
+
+**优化方案**:
+```dart
+class PreferencesValue<T> {
+  Timer? _saveTimer;
+  
+  set value(T v) {
+    _value = v;
+    
+    // ✅ 延迟批量写入
+    _saveTimer?.cancel();
+    _saveTimer = Timer(Duration(seconds: 1), () {
+      _saveToPrefs(v);
+    });
+  }
+  
+  void _saveToPrefs(T v) {
+    if (v is bool) {
+      unawaited(prefs.setBool(key, v));
+    }
+    // ...
+  }
+  
+  void dispose() {
+    _saveTimer?.cancel();
+    _saveToPrefs(_value);  // 立即保存
+  }
+}
+```
+
+---
+
+## 🟢 低优先级优化 (Low Priority)
+
+### 6. 代码质量改进
+
+#### 6.1 移除未使用的代码
+```dart
+// lib/main.dart:180-200
+// ❌ 注释掉的旧代码应删除
+// ProxyProvider2<SettingsController, ValueNotifier<AppLifecycleState>, AudioController>(
+//   lazy: false,
+//   create: (context) => AudioController()..initialize(),
+//   ...
+// ),
+```
+
+#### 6.2 优化日志输出
+```dart
+// ❌ 生产环境仍然输出大量日志
+_log.info('_onPanStart');
+_log.info('_onPanUpdate');
+
+// ✅ 使用条件编译
+if (kDebugMode) {
+  _log.info('_onPanStart');
+}
+```
+
+---
+
+## 📊 优化优先级和预期收益
+
+| 优先级 | 问题 | 预期收益 | 实施难度 | 预计工时 |
+|--------|------|----------|----------|----------|
+| 🔴 P0 | 主线程阻塞 (main初始化) | ANR -60%, 启动时间 -50% | 中 | 4h |
+| 🔴 P0 | 图片解码阻塞 | ANR -40%, 卡顿 -70% | 中 | 6h |
+| 🔴 P0 | 内存泄漏 (Image未释放) | Crash -50%, LMK -70% | 低 | 3h |
+| 🟡 P1 | AnimationController泄漏 | Crash -20% | 低 | 2h |
+| 🟡 P1 | 绘制性能优化 | 流畅度 +30% | 中 | 4h |
+| 🟡 P2 | 网络请求限流 | 网络稳定性 +20% | 低 | 2h |
+| 🟢 P3 | SharedPreferences优化 | 磁盘IO -50% | 低 | 2h |
+
+**总计预计工时**: 23小时
+
+**预期最终指标**:
+- Crash rate: 2.96% → **<1.0%** ✅
+- ANR rate: 1.52% → **<0.3%** ✅
+- LMK rate: 0.84% → **<0.2%** ✅
+- Slow cold start: 34.33% → **<12%** ✅
+
+---
+
+## 🛠️ 实施建议
+
+### 第一阶段 (Week 1) - 解决启动和ANR问题
+1. 优化 `main()` 函数异步初始化
+2. 图片解码移至 isolate
+3. 立即释放已完成关卡的 Image 资源
+
+### 第二阶段 (Week 2) - 解决内存和崩溃问题
+1. 修复 AnimationController 泄漏
+2. 优化 DownloadItem 内存管理
+3. 添加内存监控和自动清理
+
+### 第三阶段 (Week 3) - 性能打磨
+1. 优化绘制性能
+2. 网络请求限流
+3. 数据持久化优化
+
+---
+
+## 📝 监控建议
+
+### 添加性能监控埋点
+```dart
+// 启动时间监控
+class PerformanceMonitor {
+  static final Stopwatch _startupTimer = Stopwatch();
+  
+  static void startMonitoring() {
+    _startupTimer.start();
+  }
+  
+  static void reportStartupComplete() {
+    _startupTimer.stop();
+    FirebaseHelper.logEvent('app_startup_time', {
+      'duration_ms': _startupTimer.elapsedMilliseconds,
+    });
+  }
+}
+
+// 内存监控
+class MemoryMonitor {
+  static void checkMemoryUsage() {
+    final info = ProcessInfo.currentRss;
+    if (info > 200 * 1024 * 1024) {  // 200MB
+      FirebaseHelper.logEvent('high_memory_usage', {
+        'memory_mb': info / (1024 * 1024),
+      });
+    }
+  }
+}
+```
+
+---
+
+## ✅ 验证清单
+
+优化完成后,请验证以下指标:
+
+- [ ] 冷启动时间 < 2秒 (中端设备)
+- [ ] 进入游戏页面无明显卡顿 (< 500ms)
+- [ ] 拖动碎片流畅 (稳定 60fps)
+- [ ] 连续玩10关后内存占用 < 150MB
+- [ ] 快速进出游戏页面无崩溃
+- [ ] 低端设备 (2GB RAM) 可正常运行
+
+---
+
+**报告生成时间**: 2024
+**分析工具**: 静态代码分析 + Google Play Console 数据

+ 368 - 0
QUICK_START_GUIDE.md

@@ -0,0 +1,368 @@
+# 性能优化快速应用指南
+
+## 📋 优化文件清单
+
+已创建以下优化文件:
+
+1. ✅ `lib/main_optimized.dart` - 启动优化
+2. ✅ `lib/utils/image_decoder.dart` - 图片解码优化
+3. ✅ `lib/utils/memory_monitor.dart` - 内存监控
+4. ✅ `lib/play/board_play_optimized.dart` - 游戏页面优化
+5. ✅ `lib/models/download_optimized.dart` - 下载管理优化
+6. ✅ `PERFORMANCE_OPTIMIZATION_REPORT.md` - 完整优化报告
+7. ✅ `MEMORY_OPTIMIZATION_GUIDE.md` - 内存优化指南
+
+---
+
+## 🚀 第一阶段:快速修复(2-4小时)
+
+### 步骤 1: 应用启动优化 (1小时)
+
+**目标**: 解决 Slow cold start rate 34.33%
+
+**操作**:
+```bash
+# 1. 备份原文件
+cp lib/main.dart lib/main_backup.dart
+
+# 2. 应用优化
+# 将 lib/main_optimized.dart 的内容复制到 lib/main.dart
+```
+
+**关键改动**:
+- ✅ 移除 `main()` 函数中的 `await`
+- ✅ 异步初始化 Firebase 和 Persistence
+- ✅ 添加加载界面和错误处理
+
+**验证**:
+```dart
+// 在 main.dart 中添加性能监控
+void main() {
+  final stopwatch = Stopwatch()..start();
+  
+  runApp(MyApp());
+  
+  print('App started in ${stopwatch.elapsedMilliseconds}ms');
+}
+```
+
+---
+
+### 步骤 2: 图片解码优化 (1.5小时)
+
+**目标**: 解决 ANR rate 1.52%
+
+**操作**:
+```dart
+// 1. 确保 image_decoder.dart 已存在(已创建)
+
+// 2. 在 board_play.dart 中导入
+import 'package:puzzleweave/utils/image_decoder.dart';
+
+// 3. 替换 _init() 方法中的图片解码代码
+// 找到这段代码:
+ui.Image image = await itemLoader.getImageBySize(...);
+final ByteData cardData = await rootBundle.load(...);
+final ui.Codec cardCodec = await ui.instantiateImageCodec(...);
+
+// 替换为:
+final imageBytes = await itemLoader.ensureDataLoaded();
+final cardData = await rootBundle.load(...);
+
+final images = await ImageDecoder.decodeImages(
+  bytesList: [imageBytes, cardData.buffer.asUint8List()],
+  targetWidths: [bestImageSize.width.round(), bestCardImageSize.width.round()],
+  targetHeights: [bestImageSize.height.round(), bestCardImageSize.height.round()],
+);
+
+final image = images[0];
+final cardImage = images[1];
+```
+
+**验证**:
+```dart
+// 添加日志查看解码时间
+final stopwatch = Stopwatch()..start();
+final images = await ImageDecoder.decodeImages(...);
+_log.info('Image decode took ${stopwatch.elapsedMilliseconds}ms');
+```
+
+---
+
+### 步骤 3: 内存清理优化 (1.5小时)
+
+**目标**: 解决 Crash rate 2.96% 和 LMK rate 0.84%
+
+**操作**:
+
+**3.1 在 board_play.dart 中添加资源清理**:
+```dart
+class _BoardPlayState extends State<BoardPlay> {
+  Timer? _resourceCleanupTimer;
+  
+  // 添加这个方法
+  void _cleanupBoardResources() {
+    if (board != null) {
+      _log.info('Cleaning up board resources');
+      board!.dispose();
+      board = null;
+    }
+  }
+  
+  // 修改 _onSuccess() 方法
+  void _onSuccess() {
+    // ... 现有代码
+    
+    // ✅ 添加这行
+    _scheduleResourceCleanup();
+  }
+  
+  // 添加这个方法
+  void _scheduleResourceCleanup() {
+    _resourceCleanupTimer?.cancel();
+    _resourceCleanupTimer = Timer(Duration(seconds: 2), () {
+      if (mounted) {
+        _cleanupBoardResources();
+      }
+    });
+  }
+  
+  // 修改 dispose() 方法
+  @override
+  void dispose() {
+    // ✅ 在最前面添加
+    _resourceCleanupTimer?.cancel();
+    
+    // ✅ 在最后添加
+    _cleanupBoardResources();
+    
+    super.dispose();
+  }
+}
+```
+
+**3.2 在 data.dart 中增强缓存清理**:
+```dart
+void workDone(ListItem item, {Duration? timeSpent}) {
+  // ... 现有代码
+  
+  _clearCompletedItemCache(item);
+  
+  // ✅ 添加内存清理
+  _clearDownloadItemFromMemory(item);
+}
+
+// ✅ 添加这个方法
+void _clearDownloadItemFromMemory(ListItem item) {
+  if (item is RemoteItem) {
+    try {
+      final url = ApiHelper.imageUri(item.id, 'high');
+      final downloadItem = Download()._cache[url];
+      if (downloadItem != null) {
+        _log.info('Clearing download item from memory: ${item.id}');
+        downloadItem.data = null; // 清空数据
+        Download()._cache.remove(url);
+      }
+    } catch (e) {
+      _log.warning('Failed to clear download item: $e');
+    }
+  }
+}
+```
+
+---
+
+## 🎯 第二阶段:深度优化(4-6小时)
+
+### 步骤 4: 启用内存监控 (1小时)
+
+**操作**:
+```dart
+// 1. 在 main.dart 的 _MyAppState 中添加
+@override
+void initState() {
+  super.initState();
+  _initializeAsync();
+  
+  // ✅ 启动内存监控
+  MemoryMonitor().startMonitoring(
+    interval: Duration(seconds: 30),
+    onHighMemory: () {
+      _log.warning('High memory detected');
+    },
+    onCriticalMemory: () {
+      _log.severe('Critical memory, forcing cleanup');
+    },
+  );
+}
+```
+
+---
+
+### 步骤 5: 应用下载优化 (2小时)
+
+**操作**:
+参考 `lib/models/download_optimized.dart`,将优化应用到 `lib/models/download.dart`
+
+**关键改动**:
+1. 添加 `_lastAccessTime` 和 `_cleanupTimer`
+2. 添加 `clearData()` 方法
+3. 添加 `_scheduleCleanup()` 方法
+4. 修改 `ensureDataLoaded()` 方法
+
+---
+
+### 步骤 6: 优化 Board 资源管理 (1小时)
+
+**操作**:
+```dart
+// 在 board.dart 的 dispose() 方法中
+dispose() {
+  if (_isDisposed) return;
+  _isDisposed = true;
+  
+  _log.info('Disposing board resources');
+  
+  // ✅ 按内存占用大小顺序释放
+  try {
+    image.dispose();
+    _log.info('Image disposed');
+  } catch (e) {
+    _log.warning('Failed to dispose image: $e');
+  }
+  
+  try {
+    backgroundPicture?.dispose();
+    backgroundPicture = null;
+  } catch (e) {
+    _log.warning('Failed to dispose background picture: $e');
+  }
+  
+  try {
+    cardPicture?.dispose();
+    cardPicture = null;
+  } catch (e) {
+    _log.warning('Failed to dispose card picture: $e');
+  }
+  
+  // 清空 pieces
+  for (var piece in pieces) {
+    piece.path = null;
+    piece.outLinePath = null;
+    piece.innerLinePath = null;
+    piece.group = null;
+  }
+  pieces.clear();
+  backupGroups.clear();
+  
+  boardNotifier.dispose();
+}
+```
+
+---
+
+## ✅ 验证清单
+
+完成优化后,请验证以下指标:
+
+### 性能指标
+- [ ] 冷启动时间 < 2秒 (中端设备)
+- [ ] 进入游戏页面 < 500ms
+- [ ] 拖动碎片流畅 (60fps)
+- [ ] 连续玩10关后内存 < 150MB
+
+### 测试步骤
+```dart
+// 1. 添加性能日志
+class _BoardPlayState {
+  @override
+  void initState() {
+    super.initState();
+    MemoryMonitor.logMemoryUsage('BoardPlay init');
+  }
+  
+  @override
+  void dispose() {
+    MemoryMonitor.logMemoryUsage('BoardPlay dispose (before)');
+    _cleanupBoardResources();
+    MemoryMonitor.logMemoryUsage('BoardPlay dispose (after)');
+    super.dispose();
+  }
+}
+
+// 2. 运行测试
+// - 启动应用,记录启动时间
+// - 进入游戏,记录加载时间
+// - 完成10关,记录内存变化
+// - 检查日志中的内存数据
+```
+
+### 日志示例
+```
+优化前:
+[BoardPlay init] Memory: 120.5 MB
+[Level complete] Memory: 145.2 MB
+[BoardPlay dispose (before)] Memory: 145.0 MB
+[BoardPlay dispose (after)] Memory: 144.2 MB  ❌ 只释放了 0.8MB
+
+优化后:
+[BoardPlay init] Memory: 120.5 MB
+[Level complete] Memory: 145.2 MB
+[BoardPlay dispose (before)] Memory: 125.5 MB
+[BoardPlay dispose (after)] Memory: 105.8 MB  ✅ 释放了 19.7MB
+```
+
+---
+
+## 📊 预期效果
+
+| 指标 | 优化前 | 优化后 | 改善 |
+|------|--------|--------|------|
+| Crash rate | 2.96% | <1.0% | **-66%** |
+| ANR rate | 1.52% | <0.3% | **-80%** |
+| LMK rate | 0.84% | <0.2% | **-76%** |
+| Slow cold start | 34.33% | <12% | **-65%** |
+| 内存占用 | 150MB+ | <100MB | **-33%** |
+
+---
+
+## 🐛 常见问题
+
+### Q1: 编译错误 "Undefined name 'Platform'"
+**A**: 添加导入 `import 'dart:io';`
+
+### Q2: 编译错误 "Undefined name 'ImageDecoder'"
+**A**: 确保 `lib/utils/image_decoder.dart` 文件存在并正确导入
+
+### Q3: 内存监控不工作
+**A**: 确保在 Android/iOS 平台运行,Web 平台不支持
+
+### Q4: 图片解码超时
+**A**: 检查网络连接,或增加超时时间:
+```dart
+await ImageDecoder.decodeImages(
+  // ...
+  timeout: Duration(seconds: 60), // 增加超时
+);
+```
+
+---
+
+## 📞 需要帮助?
+
+如果遇到问题,请提供:
+1. 错误日志
+2. 设备信息(Android/iOS版本,内存大小)
+3. 复现步骤
+
+---
+
+## 🎉 完成!
+
+完成所有步骤后,请:
+1. 运行完整测试
+2. 检查性能指标
+3. 提交到测试环境
+4. 收集用户反馈
+
+预计优化后,Google Play 的性能指标将在 1-2 周内显著改善。

+ 1 - 1
README.md

@@ -41,7 +41,7 @@ flutter build ipa --export-method ad-hoc --bundle-sksl-path flutter_01.sksl.json
 ## 正式打包编译:
 
 ```
-flutter build appbundle --release
+flutter build appbundle --release --no-enable-impeller
 ```
 
 ## 国际化

+ 4 - 0
android/app/src/main/AndroidManifest.xml

@@ -2,6 +2,7 @@
     <uses-permission android:name="android.permission.VIBRATE" />
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="com.google.android.gms.permission.AD_ID" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
 
     <application
@@ -10,6 +11,9 @@
         android:hardwareAccelerated="true"
         android:icon="@mipmap/launcher_icon">
         <!-- 还是使用skia引擎,稳定性好点-->
+        <meta-data
+            android:name="io.flutter.embedding.android.EnableImpeller"
+            android:value="false" />
         <meta-data
             android:name="io.flutter.embedding.android.Renderer"
             android:value="skia" />

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
assets/lottie/box.json


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
assets/lottie/loading.json


+ 55 - 20
lib/ads/ads_state.dart

@@ -76,8 +76,11 @@ abstract class AdsState<T extends StatefulWidget> extends State<T> {
     lifecycleNotifier.removeListener(_handleAppLifecycle);
     lifecycleNotifier.addListener(_handleAppLifecycle);
 
-    _applovinAdsController.loadInterstitialAd();
-    _applovinAdsController.loadRewardedAd();
+    // 优化:不在 initState 立即加载,通过延迟分散启动压力
+    Future.microtask(() {
+      _applovinAdsController.loadInterstitialAd();
+      _applovinAdsController.loadRewardedAd();
+    });
 
     // 初始检查一次 Banner
     _updateBannerVisibility();
@@ -89,45 +92,69 @@ abstract class AdsState<T extends StatefulWidget> extends State<T> {
     _updateBannerVisibility();
   }
 
-  // 核心改进:主动更新 Banner 状态
   Future<void> _updateBannerVisibility() async {
+    // 1. 立即检查:如果已经不在此页面,直接退出
     if (!mounted) return;
 
-    // 1. 确保 SDK 已初始化
-    bool ready = await _applovinAdsController.completer.future;
+    try {
+      // 2. 检查 SDK 初始化(增加超时保护,防止 completer 永远不返回导致的内存泄漏)
+      bool ready = await _applovinAdsController.completer.future.timeout(const Duration(seconds: 5), onTimeout: () => false);
+
+      // 3. 再次检查:在 await 异步回来后,必须再次检查 mounted
+      // 这是解决 Null check 报错的关键,因为此时 state 可能已被 dispose
+      if (!mounted) return;
 
-    // 2. 获取当前关卡数
-    int doneLevels = data.currentLevel;
+      // 4. 获取当前关卡数和状态
+      int doneLevels = data.currentLevel;
 
-    // 3. 综合判断:SDK就绪 + 逻辑允许 + 应用在前台
-    bool shouldShow = ready && shouldShowBannerAd(doneLevels) && lifecycleNotifier.value == AppLifecycleState.resumed;
+      // 确保生命周期对象还在
+      final currentState = lifecycleNotifier.value;
 
-    if (_isBannerVisible != shouldShow) {
-      setState(() {
-        _isBannerVisible = shouldShow;
-      });
-      _log.info("Banner visibility updated: $_isBannerVisible");
+      // 5. 综合判断
+      bool shouldShow = ready && shouldShowBannerAd(doneLevels) && currentState == AppLifecycleState.resumed;
+
+      if (_isBannerVisible != shouldShow) {
+        // 6. 最终加固:在 setState 前最后一刻检查
+        setState(() {
+          _isBannerVisible = shouldShow;
+        });
+        _log.info("Banner visibility updated: $_isBannerVisible");
+      }
+    } catch (e) {
+      _log.warning("Update banner visibility failed: $e");
     }
   }
 
+  /// 🔥 关键优化:重写此方法。子类在调用 Navigator.push 离开页面前,应手动调用
+  void cleanBanner() {
+    _log.info("🔥 Cleaning up ads before navigation...");
+    // 物理销毁
+    _applovinAdsController.destroyBanner();
+  }
+
   @override
   void dispose() {
     data.completedWorks.removeListener(_onLevelChanged);
+    lifecycleNotifier.removeListener(_handleAppLifecycle);
+    _applovinAdsController.interstitialAdState.removeListener(_onInterstitialAdState);
+    _applovinAdsController.rewardedAdState.removeListener(_onRewardAdState);
+
+    // 2. 强制物理销毁原生 Banner 纹理(解决三星 J2 GPU Leak 问题)
+    _applovinAdsController.destroyBanner();
 
     intersReadyNotifier.dispose();
     rewardReadyNotifier.dispose();
 
-    lifecycleNotifier.removeListener(_handleAppLifecycle);
     if (_intersCompleter != null && !_intersCompleter!.isCompleted) {
       _intersCompleter!.complete(false);
     }
     _intersCompleter = null;
+
     if (_rewardCompleter != null && !_rewardCompleter!.isCompleted) {
       _rewardCompleter!.complete(false);
     }
     _rewardCompleter = null;
-    _applovinAdsController.interstitialAdState.removeListener(_onInterstitialAdState);
-    _applovinAdsController.rewardedAdState.removeListener(_onRewardAdState);
+
     super.dispose();
   }
 
@@ -138,6 +165,9 @@ abstract class AdsState<T extends StatefulWidget> extends State<T> {
 
   /// 检查是否应该展示banner广告
   bool shouldShowBannerAd(int doneLevels) {
+    // 增加:如果是低内存设备,坚决不展示 Banner
+    if (_applovinAdsController.isLowRamDevice) return false;
+
     if (doneLevels == 0) {
       _log.info("首关不展示banner广告");
       return false; // 首关是引导关卡,一定不要显示广告
@@ -290,9 +320,14 @@ abstract class AdsState<T extends StatefulWidget> extends State<T> {
     _log.info('AppLifecycleState changed: ${lifecycleNotifier.value}');
     lifeState = lifecycleNotifier.value;
 
-    // 当回到前台时,重新检查 Banner;切到后台时,理论上 MaxAdView 会自动处理,
-    // 但我们可以通过 setState 强制隐藏它来确保安全。
-    _updateBannerVisibility();
+    // 优化:回到后台时主动清理 Banner,回到前台再重建
+    // 这能极大缓解 FD 句柄数堆积的问题
+    if (lifeState == AppLifecycleState.paused) {
+      _applovinAdsController.destroyBanner();
+      if (mounted) setState(() => _isBannerVisible = false);
+    } else if (lifeState == AppLifecycleState.resumed) {
+      _updateBannerVisibility();
+    }
 
     if (lifeState == AppLifecycleState.inactive) {
       //前台可见,但是无法交互

+ 99 - 34
lib/ads/applovin_ads_controller.dart

@@ -3,9 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:io';
 import 'dart:math';
 
 import 'package:applovin_max/applovin_max.dart';
+import 'package:device_info_plus/device_info_plus.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -25,6 +27,17 @@ const int bannerReportDuration = 3 * 60 * 1000; // banner paid 3分钟报一次
 /// Allows showing ads. A facade for `package:google_mobile_ads`.
 class ApplovinAdsController {
   final BuildContext context;
+  // 新增:设备保护标记
+  bool _isLowRamDevice = false;
+  bool _isProblematicDevice = false;
+  bool get isLowRamDevice => _isLowRamDevice;
+
+  // 🔥 关键:设备黑名单检测
+  static final Set<String> _crashProneDevices = {'hisense', 'itel', 'huawei y9' /*'samsung galaxy a50s'*/};
+
+  bool _shouldSkipAds() {
+    return _isLowRamDevice || _isProblematicDevice;
+  }
 
   late double bannerPaidValueMicros; // banner广告累计收益
   late int lastBannerPaidReportTimestamp; // 上次banner广告收益上报时间戳,用于控制banner广告收益上报频率
@@ -36,14 +49,21 @@ class ApplovinAdsController {
 
   final Completer<bool> completer = Completer();
 
-  /// Creates an [ApplovinAdsController] that wraps around a [MobileAds] [instance].
-  ///
-  /// Example usage:
-  ///
-  ///     var controller = ApplovinAdsController(MobileAds.instance);
   ApplovinAdsController(this.context);
 
-  void dispose() {}
+  void dispose() {
+    // 🔥 关键修复:防止内存泄漏
+    if (!completer.isCompleted) {
+      completer.complete(false);
+    }
+
+    // 清理状态通知器
+    interstitialAdState.dispose();
+    rewardedAdState.dispose();
+
+    // 清理回调
+    rewardCallback = null;
+  }
 
   /// Initializes the injected [MobileAds.instance].
   Future<void> initialize() async {
@@ -51,18 +71,36 @@ class ApplovinAdsController {
 
     _log.info('AppLovinMAX.initialize...');
 
+    // 1. 设备兼容性检测(关键:防止外部纹理崩溃)
+    if (Platform.isAndroid) {
+      final deviceInfo = DeviceInfoPlugin();
+      final androidInfo = await deviceInfo.androidInfo;
+
+      // 低端机检测
+      _isLowRamDevice = androidInfo.isLowRamDevice || (androidInfo.systemFeatures.contains('android.hardware.ram.low'));
+
+      // 问题设备检测
+      String manufacturer = androidInfo.manufacturer.toLowerCase();
+      String model = androidInfo.model.toLowerCase();
+      _isProblematicDevice = _crashProneDevices.any((device) => manufacturer.contains(device) || model.contains(device));
+
+      if (_isProblematicDevice) {
+        _log.warning('🔥 Problematic device detected: $manufacturer $model');
+      }
+    }
+
     // 用于模拟测试欧洲UMP是否正常,release版本注意注释掉
     // AppLovinMAX.setVerboseLogging(true);
     // AppLovinMAX.setConsentFlowDebugUserGeography(ConsentFlowUserGeography.gdpr);
 
-    // 1. 开启合规流开关
+    // 2. 开启合规流开关
     AppLovinMAX.setTermsAndPrivacyPolicyFlowEnabled(true);
 
-    // 2. 设置你的隐私政策和用户协议链接(必须是有效的 URL)
+    // 3. 设置你的隐私政策和用户协议链接(必须是有效的 URL)
     AppLovinMAX.setPrivacyPolicyUrl("https://longreachai.net/game/privacy_policy.html");
     AppLovinMAX.setTermsOfServiceUrl("https://longreachai.net/game/terms_of_service.html");
 
-    // 3. 然后再初始化 SDK
+    // 4. 然后再初始化 SDK
     MaxConfiguration? sdkConfiguration = await AppLovinMAX.initialize(AdHelper.applovinSdkKey);
 
     _log.info('AppLovinMAX.initialize success!');
@@ -73,11 +111,15 @@ class ApplovinAdsController {
     revenueThreshold = RemoteConfig().adRevenueThreshold;
     if (revenueThreshold <= 0.0) revenueThreshold = 0.01;
 
-    initializeBannerAds();
+    // 5. 分步异步加载 (防止 FD 句柄数瞬间爆表)
+    await Future.delayed(const Duration(milliseconds: 500));
+    if (!_shouldSkipAds()) initializeBannerAds();
 
-    initializeInterstitialAds();
+    await Future.delayed(const Duration(milliseconds: 500));
+    if (!_shouldSkipAds()) initializeInterstitialAds();
 
-    initializeRewardedAd();
+    await Future.delayed(const Duration(milliseconds: 500));
+    if (!_shouldSkipAds()) initializeRewardedAd();
 
     completer.complete(true);
 
@@ -85,24 +127,34 @@ class ApplovinAdsController {
   }
 
   void initializeBannerAds() {
-    AppLovinMAX.setBannerExtraParameter(AdHelper.applovinBannerAdUnitId, "adaptive_banner", "true");
-    // MAX automatically sizes banners to 320×50 on phones and 728×90 on tablets
-    // 移除这一行,避免与 MaxAdView Widget 冲突!
-    // AppLovin MAX 在 Flutter 中的最佳实践是只使用 MaxAdView Widget,让 Flutter 来管理 Banner 视图的布局和生命周期
-    // 同时调用 AppLovinMAX.createBanner (原生创建) 和渲染 MaxAdView (Flutter Widget),特别是在 iOS 上,原生层可能会意外地创建了一个透明的全屏视图来管理广告
-    // AppLovinMAX.createBanner(AdHelper.applovinBannerAdUnitId, AdViewPosition.bottomCenter);
+    // 强制关闭低端机的自适应 Banner,因为其渲染开销极大
+    AppLovinMAX.setBannerExtraParameter(AdHelper.applovinBannerAdUnitId, "adaptive_banner", _isLowRamDevice ? "false" : "true");
+  }
+
+  /// 强制销毁 Banner(解决 JNI CheckException 的大杀器)
+  /// 在 AdsState dispose 或页面跳转前调用
+  void destroyBanner() {
+    _log.info("🔥 Explicitly destroying banner to release GPU buffers");
+    try {
+      // 强制通知原生层销毁 Banner 视图
+      AppLovinMAX.destroyBanner(AdHelper.applovinBannerAdUnitId);
+    } catch (e) {
+      _log.warning("Destroy banner error: $e");
+    }
   }
 
   Widget getBannerWidget(String positionKey) {
-    // 如果调用时 SDK 还没初始化完成,返回空,避免 Native 层空指针
-    if (!_hasInit) return const SizedBox.shrink();
+    // 增加:问题设备或未初始化,直接不渲染 Widget
+    if (!_hasInit || _shouldSkipAds()) {
+      return const SizedBox.shrink();
+    }
 
     return MaxAdView(
       key: ValueKey(positionKey), // 只要 positionKey 不变,页面内刷新就不会重建
       adUnitId: AdHelper.applovinBannerAdUnitId,
       adFormat: AdFormat.banner,
       placement: 'banner',
-      extraParameters: const {'adaptive_banner': 'true'},
+      extraParameters: {'adaptive_banner': _isLowRamDevice ? 'false' : 'true'},
       listener: AdViewAdListener(
         onAdLoadedCallback: (ad) {
           // // 广告加载成功后, ad.size 包含实际的宽度和高度 (dp)
@@ -248,8 +300,9 @@ class ApplovinAdsController {
     if (isInit == null || !isInit) {
       return;
     }
-    bool isReady = (await AppLovinMAX.isInterstitialReady(AdHelper.applovinInterstitialAdUnitId))!;
-    if (isReady) {
+    // 🔥 修复:安全解包
+    bool? isReady = await AppLovinMAX.isInterstitialReady(AdHelper.applovinInterstitialAdUnitId);
+    if (isReady == true) {
       _log.info("applovin interstitial ad already ready, no need to load!");
       return;
     }
@@ -265,8 +318,9 @@ class ApplovinAdsController {
       if (isInit == null || !isInit) {
         return false;
       }
-      bool isReady = (await AppLovinMAX.isInterstitialReady(AdHelper.applovinInterstitialAdUnitId))!;
-      return isReady;
+      // 🔥 修复:安全解包,防止崩溃
+      bool? isReady = await AppLovinMAX.isInterstitialReady(AdHelper.applovinInterstitialAdUnitId);
+      return isReady ?? false;
     } catch (e) {
       _log.warning('isInterstitialReady error: $e');
     }
@@ -278,13 +332,21 @@ class ApplovinAdsController {
   showInterstitialAd({String adSrc = "", String skuId = ""}) async {
     this.adSrc = adSrc;
     this.skuId = skuId;
+
+    // 🔥 新增:设备保护
+    if (_shouldSkipAds()) {
+      _log.info('🔥 Device protection: skipping interstitial ad');
+      return;
+    }
+
     try {
       bool? isInit = await AppLovinMAX.isInitialized();
       if (isInit == null || !isInit) {
         return;
       }
-      bool isReady = (await AppLovinMAX.isInterstitialReady(AdHelper.applovinInterstitialAdUnitId))!;
-      if (isReady) {
+      // 🔥 修复:安全解包
+      bool? isReady = await AppLovinMAX.isInterstitialReady(AdHelper.applovinInterstitialAdUnitId);
+      if (isReady == true) {
         AppLovinMAX.showInterstitial(AdHelper.applovinInterstitialAdUnitId, placement: 'inters');
       } else {
         AppLovinMAX.loadInterstitial(AdHelper.applovinInterstitialAdUnitId);
@@ -367,8 +429,9 @@ class ApplovinAdsController {
       return;
     }
 
-    bool isReady = (await AppLovinMAX.isRewardedAdReady(AdHelper.applovinRewardedAdUnitId))!;
-    if (isReady) {
+    // 🔥 修复:安全解包
+    bool? isReady = await AppLovinMAX.isRewardedAdReady(AdHelper.applovinRewardedAdUnitId);
+    if (isReady == true) {
       _log.info("applovin reward ad already ready, no need to load!");
       return;
     }
@@ -384,10 +447,11 @@ class ApplovinAdsController {
       if (isInit == null || !isInit) {
         return false;
       }
-      bool isReady = (await AppLovinMAX.isRewardedAdReady(AdHelper.applovinRewardedAdUnitId))!;
-      return isReady;
+      // 🔥 修复:安全解包
+      bool? isReady = await AppLovinMAX.isRewardedAdReady(AdHelper.applovinRewardedAdUnitId);
+      return isReady ?? false;
     } catch (e) {
-      _log.warning('isInterstitialReady error: $e');
+      _log.warning('isRewardedAdReady error: $e');
     }
     return false;
   }
@@ -402,8 +466,9 @@ class ApplovinAdsController {
       if (isInit == null || !isInit) {
         return false;
       }
-      bool isReady = (await AppLovinMAX.isRewardedAdReady(AdHelper.applovinRewardedAdUnitId))!;
-      if (isReady) {
+      // 🔥 修复:安全解包
+      bool? isReady = await AppLovinMAX.isRewardedAdReady(AdHelper.applovinRewardedAdUnitId);
+      if (isReady == true) {
         AppLovinMAX.showRewardedAd(AdHelper.applovinRewardedAdUnitId, placement: 'reward');
         rewardCallback = onUserEarnedReward;
         return true;

+ 0 - 1
lib/collection/collection_screen.dart

@@ -9,7 +9,6 @@ import 'package:puzzleweave/models/cached_request.dart';
 import 'package:puzzleweave/models/data.dart';
 import 'package:puzzleweave/models/items.dart';
 import 'package:logging/logging.dart';
-import 'package:lottie/lottie.dart';
 import 'package:provider/provider.dart';
 import 'package:puzzleweave/skin/skin.dart';
 

+ 38 - 16
lib/config/device.dart

@@ -8,29 +8,50 @@ import 'package:flutter/material.dart';
 class Device {
   static Locale? locale;
 
+  AndroidDeviceInfo? androidDeviceInfo;
+
   final Directory baseDir;
 
   Device(this.context, this.baseDir);
 
-  AndroidDeviceInfo? androidDeviceInfo;
+  // ✅ 判断是否32位设备(安全访问)
+  bool get is32BitDevice {
+    if (Platform.isIOS) return false;
+    final info = androidDeviceInfo;
+    if (info == null) return false; // 默认假设64位
+    return !info.supportedAbis.any((abi) => abi.contains('64'));
+  }
 
-  // 新增:判断是否32位设备
-  bool is32BitDevice() {
-    if (Platform.isIOS) return false; // 近年iOS全是64位
-    if (androidDeviceInfo == null) return false;
-    final abis = androidDeviceInfo!.supportedAbis;
-    return !abis.any((abi) => abi.contains('64'));
+  /// 获取平台性能(安全默认值)
+  int get androidSdkInt {
+    final info = androidDeviceInfo;
+    return info?.version.sdkInt ?? 100; // 默认假设高版本
+  }
+
+  bool get isOldAndroid => androidSdkInt < 26;
+
+  bool get isLowRamDevice {
+    final info = androidDeviceInfo;
+    return info?.isLowRamDevice ?? false; // 默认假设不是
   }
 
-  /// 获取平台性能
-  int get androidSdkInt => androidDeviceInfo != null ? androidDeviceInfo!.version.sdkInt : 100;
-  bool get isOldAndroid => androidSdkInt < 26; // 安卓8以下
-  bool get isLowRamDevice => androidDeviceInfo != null ? androidDeviceInfo!.isLowRamDevice : false;
   bool get lowCpu => Platform.numberOfProcessors <= 2;
 
+  /// ✅ 综合判断是否低端设备(即使 androidDeviceInfo 为 null 也能工作)
   bool get isLowEndDevice {
-    if (androidDeviceInfo == null) return false;
-    return lowCpu || isOldAndroid || isLowRamDevice || is32BitDevice();
+    if (Platform.isIOS) return false;
+
+    // 基于 CPU 的快速判断(不依赖 androidDeviceInfo)
+    if (lowCpu) return true;
+
+    // 如果设备信息已加载,进行详细判断
+    final info = androidDeviceInfo;
+    if (info != null) {
+      return isOldAndroid || isLowRamDevice || is32BitDevice;
+    }
+
+    // 默认假设不是低端设备
+    return false;
   }
 
   static double get devPixelRatio => PlatformDispatcher.instance.views.first.devicePixelRatio;
@@ -72,10 +93,11 @@ class Device {
     return realDPR;
   }
 
+  /// ✅ 建议的图片质量(优先基于屏幕尺寸,避免依赖 androidDeviceInfo)
   String get suggestedQuality {
-    if (isLowEndDevice) return "1200";
-    if (isTablet) return "2400"; // 平板需要更高像素
-    return "1800";
+    if (isTablet) return "1600";  // 2400 → 1600 (减少56%内存)
+    if (isLowEndDevice) return "1000";  // 1200 → 1000 (减少44%内存)
+    return "1200";  // 1800 → 1200 (减少56%内存)
   }
 
   /// safeArea高度 Z

+ 0 - 1
lib/gallery/gallery_screen.dart

@@ -6,7 +6,6 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart';
 import 'package:firebase_messaging/firebase_messaging.dart';
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
-import 'package:lottie/lottie.dart';
 import 'package:provider/provider.dart';
 import 'package:puzzleweave/ads/applovin_ads_controller.dart';
 import 'package:puzzleweave/audio/jc_audio_controller.dart';

+ 6 - 0
lib/homepage/home_board_play.dart

@@ -15,6 +15,7 @@ import 'package:puzzleweave/play/confetti_layer.dart';
 import 'package:puzzleweave/skin/skin.dart';
 import 'package:logging/logging.dart';
 import 'package:provider/provider.dart';
+import 'package:puzzleweave/utils/memory_monitor.dart';
 
 final Logger _log = Logger('home_board_play');
 
@@ -208,6 +209,7 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
 
   @override
   void dispose() {
+    MemoryMonitor.logMemoryUsage('HomeBoardPlay dispose (before)');
     board.isReadyNotifier.removeListener(_onBoardReady);
     _dealingPeriodicTimer?.cancel();
     _overlayEntry?.remove();
@@ -218,6 +220,7 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
     _unlockController.dispose();
     _dealingController.dispose();
     collectionSubscription?.cancel();
+    MemoryMonitor.logMemoryUsage('HomeBoardPlay dispose (after)');
     super.dispose();
   }
 
@@ -291,12 +294,15 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
   }
 
   void startFlipAnimation() {
+    MemoryMonitor.logMemoryUsage('Collection flip animation');
     _flipController.forward(from: 0.0);
     audio.playSfx(SfxType.flip);
     if (data.currentLevel != 0 && (data.currentCollectionIndex + 1) * 25 == data.currentLevel && currentCollectionItem != null) {
       data.collectionDone(currentCollectionItem!);
       audio.playSfx(SfxType.star);
       confettiLayer.play();
+      // 合集完成时清理内存
+      // MemoryMonitor().manualCleanup();
     }
   }
 

+ 244 - 287
lib/homepage/home_screen.dart

@@ -3,14 +3,12 @@ import 'dart:io';
 import 'dart:math';
 
 import 'package:app_tracking_transparency/app_tracking_transparency.dart';
-import 'package:applovin_max/applovin_max.dart';
 import 'package:firebase_crashlytics/firebase_crashlytics.dart';
 import 'package:firebase_messaging/firebase_messaging.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_svg/svg.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:logging/logging.dart';
-import 'package:lottie/lottie.dart';
 import 'package:provider/provider.dart';
 import 'package:puzzleweave/ads/applovin_ads_controller.dart';
 import 'package:puzzleweave/audio/jc_audio_controller.dart';
@@ -21,18 +19,18 @@ import 'package:puzzleweave/firebase/adjust_helper.dart';
 import 'package:puzzleweave/homepage/home_board_play.dart';
 import 'package:puzzleweave/l10n/app_localizations.dart';
 import 'package:puzzleweave/models/cached_request.dart';
-import 'package:puzzleweave/models/data.dart';
 import 'package:puzzleweave/models/download.dart';
 import 'package:puzzleweave/models/items.dart';
 import 'package:puzzleweave/persistence/persistence.dart';
-import 'package:puzzleweave/platform/my_method_channel.dart';
 import 'package:puzzleweave/play/board_play.dart';
 import 'package:puzzleweave/settings/settings_screen.dart';
 import 'package:puzzleweave/skin/skin.dart';
 import 'package:puzzleweave/utils/mybutton.dart';
 import 'package:puzzleweave/utils/utils.dart';
 
+import '../ads/ad_helper.dart';
 import '../ads/ads_state.dart';
+import '../utils/memory_monitor.dart';
 
 final Logger _log = Logger('home_screen');
 
@@ -43,77 +41,113 @@ class HomeScreen extends StatefulWidget {
   State<StatefulWidget> createState() => _HomeScreen();
 }
 
-const int minimumRemoteLoadCount = 30; // 假设加载到 30 张图才算网络畅通
+const int minimumRemoteLoadCount = 30;
 
 class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
   late Device device;
   late JcAudioController audio;
-  late Data data;
   List<ListItem>? latest;
   late CachedRequest latestCachedRequest;
   late StreamSubscription? latestSubscription;
 
-  // 自定义画布控制器(可选,用于控制画布绘制逻辑)
   final _canvasKey = GlobalKey<HomeBoardPlayState>();
-  // !!! 新增:用于定位 Collection 按钮的 GlobalKey
   final GlobalKey _collectionKey = GlobalKey();
 
   bool isLoading = true;
+  bool firstRun = false;
 
-  // !!! 新增:Collection 按钮的动画控制器和动画
-  late AnimationController _collectionController; // 左上角 collection button 的动画控制器
-  late Animation<double> _collectionAnimation; // 放大/缩小动画
+  // ✅ 优化点1: 导航状态管理
+  bool _isNavigating = false;
 
-  bool firstRun = false;
+  // ✅ 优化点2: 防抖机制
+  Timer? _refreshDebouncer;
+
+  // ✅ 优化点3: 缓存计算结果
+  double? _cachedCanvasWidth;
+  double? _cachedCanvasHeight;
+  bool _layoutCalculated = false;
+
+  late AnimationController _collectionController;
+  late Animation<double> _collectionAnimation;
+
+  bool interPending = false;
 
   @override
   void initState() {
     super.initState();
-
     _log.info("首页初始化");
 
-    // 在组件绘制后检查 firstRun 并导航
-    if (Persistence().firstRun) {
-      firstRun = true;
-      WidgetsBinding.instance.addPostFrameCallback((_) {
-        // 仅当未跳转过时执行
-        _handleFirstRunNavigation();
-      });
-      Persistence().firstRun = false;
-    }
+    _initializeComponents();
+    _setupAnimations();
+    _handleInitialNavigation();
+  }
 
+  void _initializeComponents() {
     device = context.read<Device>();
     audio = context.read<JcAudioController>();
-    data = context.read<Data>();
     latestCachedRequest = data.latest;
-    // 主动获取缓存数据(关键)
+
     final cachedData = latestCachedRequest.cachedData;
     if (cachedData != null) {
       _onLatestDataUpdate(cachedData);
     }
     latestSubscription = latestCachedRequest.stream.listen(_onLatestDataUpdate, onError: _onLatestDataError);
 
-    // !!! 改造点 1: 初始化 Collection 按钮动画
-    _collectionController =
-        AnimationController(
-          // 设定总时长
-          duration: const Duration(milliseconds: 300),
-          vsync: this,
-        )..addStatusListener((status) {
-          if (status == AnimationStatus.completed) {
-            audio.playSfx(SfxType.pop);
+    onInterstitialAdState = _createInterStateListener();
+  }
+
+  Function(AdState state) _createInterStateListener() {
+    return (AdState state) {
+      _log.info('Interstitial ad state changed: $state');
+      if (state == AdState.dismissed && interPending) {
+        _log.info('Interstitial ad dismissed, executing pending post-ad logic.');
+        _canvasKey.currentState?.startFlipAnimation();
+
+        final bool hasSufficientData = latest != null && latest!.length >= minimumRemoteLoadCount;
+        final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch;
+
+        if (hasSufficientData) {
+          if (isNetworkActive) {
+            _log.info('Game finished, data complete & Network Active. Triggering sequential preloading...');
+            _preloadNextImages();
+          } else {
+            _log.info('Game finished, data complete but Network inactive. Attempting refresh.');
+            refresh();
           }
-        });
+        } else {
+          _log.info('Game finished, remote data incomplete. Attempting refresh...');
+          refresh();
+        }
+        interPending = false;
+      }
+    };
+  }
+
+  void _setupAnimations() {
+    _collectionController = AnimationController(duration: const Duration(milliseconds: 300), vsync: this)
+      ..addStatusListener((status) {
+        if (status == AnimationStatus.completed) {
+          audio.playSfx(SfxType.pop);
+        }
+      });
 
-    // !!! 改造点 2: 使用 TweenSequence 实现平滑的放大和缩小
     _collectionAnimation = TweenSequence<double>([
-      // 阶段 1: 放大到 1.3 (占总时长的 50%)
       TweenSequenceItem(tween: Tween<double>(begin: 1.0, end: 1.4).chain(CurveTween(curve: Curves.easeOut)), weight: 40.0),
-      // 阶段 2: 缩小回 1.0 (占总时长的 50%)
       TweenSequenceItem(tween: Tween<double>(begin: 1.4, end: 1.0).chain(CurveTween(curve: Curves.easeIn)), weight: 60.0),
     ]).animate(_collectionController);
+  }
+
+  void _handleInitialNavigation() {
+    if (Persistence().firstRun) {
+      firstRun = true;
+      WidgetsBinding.instance.addPostFrameCallback((_) {
+        if (mounted && !_isNavigating) {
+          _handleFirstRunNavigation();
+        }
+      });
+      Persistence().firstRun = false;
+    }
 
-    // 只有在应用是 resumed 状态且当前页面在前台时才自动播
     WidgetsBinding.instance.addPostFrameCallback((_) {
       if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed && mounted) {
         audio.startMusic();
@@ -121,8 +155,10 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
     });
   }
 
-  // 首页初始化之后的跳转,首次运行直接进入play页面,上次从play页面退出有缓存存在也跳转到play页面
-  void _handleFirstRunNavigation() async {
+  // ✅ 优化点4: 异步化首次运行导航
+  void _handleFirstRunNavigation() {
+    if (_isNavigating) return;
+
     _log.info('First run detected, navigating to initial play page.');
     final AssetItem initialItem = AssetItem(
       Config.firstId,
@@ -137,222 +173,198 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
     return gotoPlay(initialItem, firstRun: true);
   }
 
-  // 检查是否需要跳转到boardplay
-  void checkGoPlay() async {
-    if (currentItem != null) {
-      final jsonFile = await localFile(currentItem!.jsonPath);
-      final exists = await jsonFile.exists();
+  // ✅ 优化点5: 异步化文件检查
+  void checkGoPlay() {
+    if (currentItem == null || _isNavigating) return;
 
-      // !!! 关键修复:检查当前组件是否还在组件树中
-      if (!mounted) return;
+    Future.microtask(() async {
+      try {
+        final jsonFile = await localFile(currentItem!.jsonPath);
+        final exists = await jsonFile.exists();
 
-      if (exists) {
-        gotoPlay(currentItem!);
+        if (mounted && exists && !_isNavigating) {
+          gotoPlay(currentItem!);
+        }
+      } catch (e) {
+        _log.warning('Error checking play file: $e');
       }
-    }
+    });
   }
 
   @override
   void dispose() {
+    _refreshDebouncer?.cancel();
     latestSubscription?.cancel();
     _collectionController.dispose();
     super.dispose();
   }
 
+  // ✅ 优化点6: 简化数据更新逻辑
   _onLatestDataUpdate(datalist) {
-    _log.info('_onLatestDataUpdate.... ');
-    if (datalist != null) {
-      bool check = false;
-      if (currentItem == null && datalist != null && !firstRun) {
-        check = true;
-      }
-      latest = datalist as List<ListItem>;
-      isLoading = false;
-      setState(() {});
+    _log.info('_onLatestDataUpdate....');
+    if (datalist == null) return;
 
-      // 1. 检查数据量是否达到最低要求 (>= 30)
-      final bool hasSufficientData = datalist.length >= minimumRemoteLoadCount;
+    bool shouldCheckGoPlay = false;
+    if (currentItem == null && !firstRun) {
+      shouldCheckGoPlay = true;
+    }
 
-      // 2. 检查数据是否来自最近一次成功的网络请求
-      final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch; // !!! 关键检查点
+    latest = datalist as List<ListItem>;
+    isLoading = false;
+    setState(() {});
 
-      if (hasSufficientData) {
-        // 如果数据完整,无论是否是缓存数据,都尝试初始化第三方服务(因为主页已经可以显示了)
-        if (!hasInit) {
-          initThird();
-        }
+    final bool hasSufficientData = datalist.length >= minimumRemoteLoadCount;
+    final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch;
 
-        if (!isNetworkActive) {
-          // 如果是从缓存读取的,网络状态未知,静默刷新列表即可,不触发图片下载
-          Future.delayed(Duration(seconds: 3), () => refresh());
-        }
-      } else {
-        // 数据不足 (例如,只有内置图),无论是缓存还是远程失败,都需要重试
-        _log.info('Data insufficient (only ${datalist.length} items). Attempting refresh in 3s.');
-        Future.delayed(Duration(seconds: 3), () => refresh());
+    if (hasSufficientData) {
+      if (!hasInit) {
+        initThird();
       }
-
-      if (check) {
-        checkGoPlay();
+      if (!isNetworkActive) {
+        _debouncedRefresh();
       }
+    } else {
+      _log.info('Data insufficient (only ${datalist.length} items). Attempting refresh.');
+      _debouncedRefresh();
+    }
+
+    if (shouldCheckGoPlay) {
+      checkGoPlay();
     }
   }
 
   _onLatestDataError(error) {
     _log.info('_onLatestDataError.... $error');
     if (latest == null || latest!.isEmpty || latest!.length < 20) {
-      // 列表数据如果少于20,说明只是内置图,仍然刷新远程请求
       _log.warning("_onLatestDataError, retry again");
-      // refresh();
-      Future.delayed(Duration(seconds: 3), () => refresh());
+      _debouncedRefresh();
     }
   }
 
+  // ✅ 优化点7: 防抖刷新
+  void _debouncedRefresh() {
+    _refreshDebouncer?.cancel();
+    _refreshDebouncer = Timer(const Duration(seconds: 2), () {
+      if (mounted) {
+        refresh();
+      }
+    });
+  }
+
   Future<void> refresh() async {
     _log.info('refresh...');
     await latestCachedRequest.refresh();
   }
 
-  // ListItem? get currentItem {
-  //   if (latest != null && latest!.isNotEmpty && data.currentLevel < latest!.length) {
-  //     // return latest![data.currentLevel]; // 原来的逻辑,太过简单,如果后台图片有调整顺序变了,用户可能会遇到重复的图
-  //     // todo... 改成从latest列表中查找首个 data.completedWorks 中不存在的图(即首个未完成图)
-  //   }
-  //   return null;
-  // }
-
   ListItem? get currentItem {
-    // 1. 确保 latest 数据已加载
     if (latest == null || latest!.isEmpty) {
       return null;
     }
 
-    // 2. 获取已完成作品的唯一标识符集合,方便快速查找
-    // 假设 ListItem 的 id/url/name 等属性是其唯一标识。
-    // 我们使用 id 作为唯一标识符。
     final Set<String> completedIds = data.completedWorks.value.map((work) => work.id).toSet();
 
-    // 3. 遍历 latest 列表,查找第一个未完成的 Item
     for (final item in latest!) {
-      // 假设 ListItem 有一个唯一的 id 属性。
-      // 如果 ListItem 没有 id,您需要使用其 URL 或其他唯一标识。
-      // 这里我们假设 ListItem 是 RemoteItem/AssetItem 的基类,它们有一个 String 类型的 id 属性。
       final String itemId = item.id;
-
-      // 检查这个 id 是否在已完成集合中
       if (!completedIds.contains(itemId)) {
         _log.info('Found current item: $itemId');
-        return item; // 返回找到的第一个未完成的 Item
+        return item;
       }
     }
 
-    // 4. 如果所有图片都完成了
     _log.info('All items in the latest list have been completed.');
     return null;
   }
 
-  /// 预加载未来 N 张图片到磁盘,并最后触发当前关卡下载以最大化内存缓存命中率。
-  void _preloadNextImages() async {
-    // 预加载数量 (包括当前关卡在内,共 20 个)
-    const int totalPreloadCount = 20;
+  // ✅ 优化点8: 后台预加载
+  void _preloadNextImages() {
+    Future.microtask(() async {
+      const int totalPreloadCount = 20;
 
-    // 1. 确保 latest 数据已加载
-    if (latest == null || latest!.isEmpty || latest!.length < minimumRemoteLoadCount) {
-      _log.info('Preload failed: latest list is empty.');
-      return;
-    }
+      if (latest == null || latest!.isEmpty || latest!.length < minimumRemoteLoadCount) {
+        _log.info('Preload failed: latest list is empty.');
+        return;
+      }
 
-    // 2. 查找当前未完成的第一张图片的索引 (Index of currentItem)
-    final Set<String> completedIds = data.completedWorks.value.map((work) => work.id).toSet();
-    int startIndex = -1;
-    for (int i = 0; i < latest!.length; i++) {
-      if (!completedIds.contains(latest![i].id)) {
-        startIndex = i;
-        break;
+      final Set<String> completedIds = data.completedWorks.value.map((work) => work.id).toSet();
+      int startIndex = -1;
+      for (int i = 0; i < latest!.length; i++) {
+        if (!completedIds.contains(latest![i].id)) {
+          startIndex = i;
+          break;
+        }
       }
-    }
 
-    if (startIndex == -1) {
-      _log.info('Preload: All images completed, nothing to preload.');
-      return;
-    }
+      if (startIndex == -1) {
+        _log.info('Preload: All images completed, nothing to preload.');
+        return;
+      }
 
-    // 确定预加载范围 (从当前图片startIndex到 totalPreloadCount 个图片)
-    final int endPreloadIndex = min(startIndex + totalPreloadCount, latest!.length);
+      final int endPreloadIndex = min(startIndex + totalPreloadCount, latest!.length);
+      final List<ListItem> itemsToLoad = latest!.sublist(startIndex, endPreloadIndex);
 
-    // 3. 准备要加载的列表 (从 startIndex 开始)
-    final List<ListItem> itemsToLoad = latest!.sublist(startIndex, endPreloadIndex);
+      if (itemsToLoad.isEmpty) {
+        _log.info('Preload: No items found in the range.');
+        return;
+      }
 
-    if (itemsToLoad.isEmpty) {
-      _log.info('Preload: No items found in the range.');
-      return;
-    }
+      final ListItem currentItemToLoad = itemsToLoad.removeAt(0);
+      itemsToLoad.add(currentItemToLoad);
 
-    // 4. 将当前关卡 (第一个元素) 移动到列表的末尾
-    final ListItem currentItemToLoad = itemsToLoad.removeAt(0);
-    itemsToLoad.add(currentItemToLoad);
-
-    _log.info('Preloading ${itemsToLoad.length} images. Current item: ${currentItemToLoad.id} will be loaded last.');
-
-    // 5. 循环触发 ItemLoader 加载
-    int preloadCount = 0;
-    for (final item in itemsToLoad) {
-      // 对远程图片进行预加载
-      // 我们不关心返回值或 Future,只是触发下载
-      if (item is RemoteItem) {
-        try {
-          // 使用静态 preload 方法,不再创建复杂的 Loader 实例
-          ItemLoader.preload(item, device.suggestedQuality);
-          // 稍微给一点延迟,避免瞬时并发 I/O 导致 UI 顿挫
-          await Future.delayed(const Duration(milliseconds: 100));
-          preloadCount++;
-        } catch (e) {
-          _log.warning('Failed to load item for preloading: ${item.id}, error: $e');
+      _log.info('Preloading ${itemsToLoad.length} images. Current item: ${currentItemToLoad.id} will be loaded last.');
+
+      int preloadCount = 0;
+      for (final item in itemsToLoad) {
+        if (item is RemoteItem) {
+          try {
+            ItemLoader.preload(item, device.suggestedQuality);
+            await Future.delayed(const Duration(milliseconds: 100));
+            preloadCount++;
+          } catch (e) {
+            _log.warning('Failed to load item for preloading: ${item.id}, error: $e');
+          }
         }
       }
-    }
 
-    _log.info('Preload initiated for $preloadCount remote images, current item was last.');
+      _log.info('Preload initiated for $preloadCount remote images, current item was last.');
+    });
   }
 
-  @override
-  Widget build(BuildContext context) {
-    if (isLoading) return scrollableDummy;
-
-    // 2. 计算画布尺寸(宽=屏幕宽-60,高=宽×3/2)
-    // final canvasWidth = device.screenSize.width - 30 * 2; // 左右各30px
-    // final canvasHeight = canvasWidth * 3 / 2;
+  // ✅ 优化点9: 缓存布局计算
+  void _calculateLayout() {
+    if (_layoutCalculated) return;
 
     final double availableHeight = device.screenSize.height - device.appBarHeight - device.bannerHeight - 120;
-
-    final double paddedWidth = device.screenSize.width - 2 * 30; // padding width 30
+    final double paddedWidth = device.screenSize.width - 2 * 30;
     final double paddedHeight = availableHeight;
-
     final double targetWidth = paddedWidth;
     final double targetHeight = targetWidth * device.aspectRatio;
 
-    final double canvasWidth;
-    final double canvasHeight;
-
     if (targetHeight > paddedHeight) {
-      canvasHeight = paddedHeight;
-      canvasWidth = paddedHeight / device.aspectRatio;
+      _cachedCanvasHeight = paddedHeight;
+      _cachedCanvasWidth = paddedHeight / device.aspectRatio;
     } else {
-      canvasWidth = targetWidth;
-      canvasHeight = targetHeight;
+      _cachedCanvasWidth = targetWidth;
+      _cachedCanvasHeight = targetHeight;
     }
 
+    _layoutCalculated = true;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (isLoading) return scrollableDummy;
+
+    _calculateLayout();
+
     return Scaffold(
       backgroundColor: SkinHelper.colorWhite,
       appBar: AppBar(
         backgroundColor: SkinHelper.colorWhite,
-        // elevation: 1,
         centerTitle: true,
         leading: RepaintBoundary(
-          // !!! 改造点 3: 添加 ScaleTransition
-          key: _collectionKey, // 关联 GlobalKey
+          key: _collectionKey,
           child: ScaleTransition(
-            scale: _collectionAnimation, // 使用定义的放大/缩小动画
+            scale: _collectionAnimation,
             child: IconButton(
               onPressed: () {
                 audio.playSfx(SfxType.click);
@@ -362,27 +374,20 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
             ),
           ),
         ),
-        // title: const Text(
-        //   'Jigsort Solitaire',
-        //   style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 24),
-        // ),
-        // 🚀 改造点:将 Text 标题替换为 SvgPicture
         title: SvgPicture.asset(
-          'assets/images/title.svg', // 替换为您的 SVG 文件路径
-          height: 32, // 根据您的设计调整高度,确保它在 AppBar 中显示良好
-          // colorFilter: const ColorFilter.mode(Colors.black87, BlendMode.srcIn), // 如果SVG是单色,可以设置颜色
+          'assets/images/title.svg',
+          height: 32,
           placeholderBuilder: (BuildContext context) => const Text(
-            // 占位符,以防SVG加载失败
             'Jigsort Solitaire',
             style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 24),
           ),
         ),
+        // title: // Release模式下显示内存信息
+        //     MemoryMonitor.getMemoryWidget(),
         actions: [
           IconButton(
             onPressed: () {
               audio.playSfx(SfxType.click);
-              // AppLovinMAX.showMediationDebugger();
-              // Navigator.push(context, SettingsDialog.buildRoute());
               Navigator.push(context, SettingScreen.buildRoute());
             },
             icon: const Icon(Icons.settings, color: Colors.black87),
@@ -396,22 +401,20 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
             child: Column(
               mainAxisAlignment: MainAxisAlignment.spaceEvenly,
               children: [
-                // 2. 画布区域(固定尺寸)
                 Padding(
-                  padding: const EdgeInsets.symmetric(horizontal: 30), // 左右30px
+                  padding: const EdgeInsets.symmetric(horizontal: 30),
                   child: SizedBox(
-                    width: canvasWidth,
-                    height: canvasHeight,
+                    width: _cachedCanvasWidth!,
+                    height: _cachedCanvasHeight!,
                     child: ValueListenableBuilder(
                       valueListenable: data.completedWorks,
                       builder: (context, value, child) {
                         return HomeBoardPlay(
                           key: _canvasKey,
-                          canvasWidth: canvasWidth,
-                          canvasHeight: canvasHeight,
+                          canvasWidth: _cachedCanvasWidth!,
+                          canvasHeight: _cachedCanvasHeight!,
                           collectionKey: _collectionKey,
                           onCollectionDone: () {
-                            // collection unlocking 动画结束,启动collection button 的接收反馈动画
                             _log.info('onCollectionDone, 启动合集收纳反馈动画');
                             audio.playSfx(SfxType.appear);
                             _collectionController.forward(from: 0.0);
@@ -427,10 +430,9 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
           ),
           SafeArea(
             child: SizedBox(
-              // 始终预留高度,防止 Banner 出现时下方 UI 整体上跳(Layout Jitter)
               height: context.read<Device>().bannerHeight,
               width: double.infinity,
-              child: isBannerVisible ? getBanner('home_bottom') : const SizedBox.shrink(), // 隐藏时完全不占位或保持留白
+              child: isBannerVisible ? getBanner('home_bottom') : const SizedBox.shrink(),
             ),
           ),
         ],
@@ -438,42 +440,61 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
     );
   }
 
+  // ✅ 优化点10: 导航状态管理
   void gotoPlay(ListItem item, {bool firstRun = false}) async {
+    if (_isNavigating) {
+      _log.info('Navigation already in progress, ignoring');
+      return;
+    }
+
     _log.info('goto play, firstRun = $firstRun');
 
-    // !!! 增加保护
     if (!mounted) return;
 
-    PageRouteBuilder? pageRouteBuilder = BoardPlay.buildRoute(item, firstRun: firstRun);
-    final result = await Navigator.push(context, pageRouteBuilder);
+    _isNavigating = true;
 
-    if (!mounted) return;
+    try {
+      cleanBanner();
 
-    if (result != null && result == true) {
-      // 通关返回, 展示翻牌
-      _canvasKey.currentState?.startFlipAnimation();
+      if (!mounted) return;
+
+      PageRouteBuilder? pageRouteBuilder = BoardPlay.buildRoute(item, firstRun: firstRun);
+      final result = await Navigator.push(context, pageRouteBuilder);
+
+      if (!mounted) return;
+
+      // result 是 true, 说明关卡已经完成。 但此时可能播放插屏广告中, 需要等待用户关闭广告之后才能执行翻牌等逻辑
+      if (result != null && result == true) {
+        // 打印下当下的插屏广告状态
+        _log.info('==================>interState =  $intersState');
+
+        if (intersState == AdState.ready) {
+          // 这种情况表示有插屏广告在播放,需要等待广告关闭之后才能执行翻牌动画等逻辑
+          interPending = true;
+          _log.info('Interstitial ad is currently playing, will execute post-ad logic after dismissal.');
+          return;
+        }
 
-      final bool hasSufficientData = latest != null && latest!.length >= minimumRemoteLoadCount;
-      final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch;
+        _canvasKey.currentState?.startFlipAnimation();
 
-      if (hasSufficientData) {
-        // 1. 数据完整:如果网络活跃,立即顺延预加载。
-        if (isNetworkActive) {
-          _log.info('Game finished, data complete & Network Active. Triggering sequential preloading...');
-          _preloadNextImages();
+        final bool hasSufficientData = latest != null && latest!.length >= minimumRemoteLoadCount;
+        final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch;
+
+        if (hasSufficientData) {
+          if (isNetworkActive) {
+            _log.info('Game finished, data complete & Network Active. Triggering sequential preloading...');
+            _preloadNextImages();
+          } else {
+            _log.info('Game finished, data complete but Network inactive. Attempting refresh.');
+            refresh();
+          }
         } else {
-          // 2. 数据完整但网络不活跃/状态未知:尝试刷新,让 _onLatestDataUpdate 负责后续处理
-          _log.info('Game finished, data complete but Network inactive. Attempting refresh.');
+          _log.info('Game finished, remote data incomplete. Attempting refresh...');
           refresh();
         }
-      } else {
-        // 3. 数据不完整:无论如何都需要刷新,让 _onLatestDataUpdate 重新处理
-        _log.info('Game finished, remote data incomplete. Attempting refresh...');
-        refresh();
       }
-    } else {
-      // 非关卡通关返回,在这里播放插屏广告
-      // showInterstitialAd("level_exit", currentItem!.id, data.currentLevel);
+    } finally {
+      _isNavigating = false;
     }
   }
 
@@ -504,43 +525,22 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
         children: [
           Text(
             AppLocalizations.of(context)!.play,
-            style: TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold),
-          ),
-          ValueListenableBuilder<List<Work>>(
-            valueListenable: data.completedWorks,
-            builder: (context, isSoundOn, child) {
-              return Text('${AppLocalizations.of(context)!.level} ${data.currentLevel + 1}', style: const TextStyle(color: Colors.white, fontSize: 16));
-            },
+            style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold),
           ),
+          Text('${AppLocalizations.of(context)!.level} ${data.currentLevel + 1}', style: const TextStyle(color: Colors.white, fontSize: 16)),
         ],
       ),
     );
   }
 
-  // Widget get scrollableDummy => Scaffold(
-  //   body: LayoutBuilder(
-  //     builder: (p0, p1) {
-  //       return SingleChildScrollView(
-  //         physics: const AlwaysScrollableScrollPhysics(),
-  //         child: SizedBox(
-  //           height: p1.maxHeight,
-  //           child: Center(child: ListView(shrinkWrap: true, children: [Lottie.asset('assets/lottie/loading.json', height: 100)])),
-  //         ),
-  //       );
-  //     },
-  //   ),
-  // );
-
   Widget get scrollableDummy => Scaffold(
     backgroundColor: SkinHelper.colorWhite,
     body: Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: [
-          // 使用原生最轻量的进度指示器
           SizedBox(width: 40, height: 40, child: CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(SkinHelper.coreBgColor))),
           const SizedBox(height: 20),
-          // 可选:添加一个简单的文字,让用户知道在加载
           Text(
             "Loading...",
             style: TextStyle(color: SkinHelper.slotBorderColor.withOpacity(0.7), fontSize: 14, fontWeight: FontWeight.w500),
@@ -553,82 +553,39 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
   ///////////////////////// 初始化相关 /////////////////////////
 
   static bool hasInit = false;
-  static MyMethodChannel platform = MyMethodChannel();
 
-  // 在列表刷出来后才正式初始化admod等组件
   void initThird() async {
     if (hasInit) return;
-
     hasInit = true;
 
-    // 有了UMP后, 这里的ATT就不需要了
-    // bool auth = await initATT();
-    // if (auth) {
-    //   await platform.setHasUserConsent(true);
-    //   await platform.setAdvertiserTrackingEnabled(true);
-    // }
-    // await initUMP(); // 征询欧洲用户同意 // applovin max 已经可以自动处理,这里不需要了
     TrackingStatus attStatus = await AppTrackingTransparency.trackingAuthorizationStatus;
     if (attStatus == TrackingStatus.authorized && Platform.isIOS) {
       // ATT 通过之后,ios需要调用相关的原生sdk接口做进一步的初始化
-      // await platform.setHasUserConsent(true);
-      // await platform.setAdvertiserTrackingEnabled(true);
     }
-    initFCM(); // 消息推送许可弹窗
-    initAd(); // admod 的广告加载安排在iOS ATT 之后,以便能够加载到个性化广告
-    AdjustHelper.init(Persistence().uuid); // 初始化Adjust
+    initFCM();
+    initAd();
+    AdjustHelper.init(Persistence().uuid);
 
     final idfa = await AppTrackingTransparency.getAdvertisingIdentifier();
     _log.info("idfa: $idfa");
   }
 
-  /////////////////////////// ATT ///////////////////////////
-  // Platform messages are asynchronous, so we initialize in an async method.
   Future<bool> initATT() async {
     TrackingStatus status = await AppTrackingTransparency.trackingAuthorizationStatus;
     _log.info('initATT111 $status');
-    // If the system can show an authorization request dialog
     if (status == TrackingStatus.notDetermined) {
-      // Show a custom explainer dialog before the system dialog
-      // await showCustomTrackingDialog(context);
-      // Wait for dialog popping animation
-      // await Future.delayed(const Duration(milliseconds: 200));
-      // Request system's tracking authorization dialog
       status = await AppTrackingTransparency.requestTrackingAuthorization();
       _log.info('initATT222 $status');
     }
-    if (status == TrackingStatus.authorized) {
-      return true;
-    }
-    return false;
+    return status == TrackingStatus.authorized;
   }
 
-  // no need
-  Future<void> showCustomTrackingDialog(BuildContext context) async => await showDialog<void>(
-    context: context,
-    builder: (context) => AlertDialog(
-      title: const Text('Dear User'),
-      content: const Text(
-        'We care about your privacy and data security. We keep this app free by showing ads. '
-        'Can we continue to use your data to tailor ads for you?\n\nYou can change your choice anytime in the app settings. '
-        'Our partners will collect data and use a unique identifier on your device to show you ads.',
-      ),
-      actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('Continue'))],
-    ),
-  );
-  /////////////////////////////////////////////////////////
-
-  /// 初始化广告模块
   initAd() {
     _log.info('initAd');
-    // AdsController adsController = context.read<AdsController>();
-    // adsController.initialize();
     ApplovinAdsController applovinAdsController = context.read<ApplovinAdsController>();
     applovinAdsController.initialize();
   }
 
-  /////////////////////////// FCM ///////////////////////////
-  // 消息推送许可弹框
   initFCM() async {
     try {
       final fcmToken = await FirebaseMessaging.instance.getToken();

+ 195 - 86
lib/main.dart

@@ -1,3 +1,6 @@
+// main.dart - 优化版本
+// 保留设备信息加载,但优化初始化流程
+
 import 'dart:async';
 import 'dart:developer' as dev;
 import 'dart:io';
@@ -24,6 +27,7 @@ import 'package:puzzleweave/play/board_play.dart';
 import 'package:puzzleweave/remote_config/remote_config.dart';
 import 'package:puzzleweave/settings/settings_controller.dart';
 import 'package:puzzleweave/utils/utils.dart';
+import 'package:puzzleweave/utils/memory_monitor.dart';
 
 import 'config/config.dart' as cfg;
 import 'config/device.dart';
@@ -32,7 +36,8 @@ Logger _log = Logger('main.dart');
 
 final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
 
-void main() async {
+// ✅ 优化点1: 移除所有阻塞性 await
+void main() {
   // Subscribe to log messages.
   Logger.root.onRecord.listen((record) {
     dev.log(
@@ -51,35 +56,84 @@ void main() async {
   // 强制竖屏
   SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
 
-  // 进入全屏沉浸式, 隐藏底部导航以及状态栏
+  // 进入全屏沉浸式
   if (Platform.isAndroid) {
     SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
   }
 
   SystemChrome.setSystemUIOverlayStyle(
-    const SystemUiOverlayStyle(
-      statusBarColor: Colors.transparent, // <-- SEE HERE
-      statusBarIconBrightness: Brightness.dark, //<-- For Android SEE HERE (dark icons)
-      statusBarBrightness: Brightness.light, //<-- For iOS SEE HERE (dark icons)
-    ),
+    const SystemUiOverlayStyle(statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.dark, statusBarBrightness: Brightness.light),
   );
 
-  ////////////////////// firebase relate ///////////////////////////////
+  // ✅ 立即启动 UI,不等待任何异步初始化
+  runApp(const MyApp());
+}
+
+class MyApp extends StatefulWidget {
+  const MyApp({super.key});
+
+  @override
+  State<MyApp> createState() => _MyAppState();
+}
 
-  if (!kIsWeb && (Platform.isAndroid)) {
+class _MyAppState extends State<MyApp> {
+  bool _isInitialized = false;
+  String? _initError;
+  late Directory _baseDir;
+
+  @override
+  void initState() {
+    super.initState();
+    _initializeAsync();
+  }
+
+  // ✅ 优化点2: 并行初始化,减少总时间
+  Future<void> _initializeAsync() async {
     try {
-      await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
+      // 并行执行所有初始化任务
+      final results = await Future.wait([_initFirebase(), _initPersistence(), _initBaseDirectory()], eagerError: false);
+
+      _baseDir = results[2] as Directory;
+
+      // 非阻塞性初始化(不等待完成)
+      _initRemoteConfig();
+      _prepareFirstRunData();
+
+      setState(() {
+        _isInitialized = true;
+      });
+
+      // 启动内存监控
+      MemoryMonitor().startMonitoring(
+        interval: const Duration(seconds: 3),
+        onHighMemory: () => _log.warning('High memory detected'),
+        onCriticalMemory: () => _log.severe('Critical memory - emergency cleanup triggered'),
+      );
+
+      _log.info('App initialization completed successfully');
+    } catch (e, stack) {
+      _log.severe('App initialization failed', e, stack);
+      setState(() {
+        _initError = e.toString();
+        _isInitialized = true; // 即使失败也显示 UI
+      });
+    }
+  }
+
+  // ✅ 优化点3: Firebase 初始化独立,带超时保护
+  Future<void> _initFirebase() async {
+    if (kIsWeb || !Platform.isAndroid) return;
+
+    try {
+      await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform).timeout(Duration(seconds: 10));
 
       FlutterError.onError = (errorDetails) {
         FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
       };
 
-      // Pass all uncaught asynchronous errors
-      // that aren't handled by the Flutter framework to Crashlytics.
       PlatformDispatcher.instance.onError = (error, stack) {
         final errorStr = error.toString();
 
-        // 1. 扩展网络/环境类异常的拦截范围
         final bool isEnvironmentIssue =
             error is SocketException ||
             error is HttpException ||
@@ -90,8 +144,8 @@ void main() async {
             errorStr.contains('Failed host lookup') ||
             errorStr.contains('Network is unreachable') ||
             errorStr.contains('Connection timed out') ||
-            errorStr.contains('Connection closed') || // 补充:上个日志提到的错误
-            errorStr.contains('Connection reset') || // 补充:被对方重置
+            errorStr.contains('Connection closed') ||
+            errorStr.contains('Connection reset') ||
             errorStr.contains('Unable to connect');
 
         if (isEnvironmentIssue) {
@@ -100,68 +154,136 @@ void main() async {
           return true;
         }
 
-        // 2. 拦截插件缺失类异常
         if (error is MissingPluginException) {
           _log.warning('插件未找到: $errorStr');
           return true;
         }
 
-        // 3. 剩下的才是逻辑错误(如 Null Check, Range Error)
-        // 改为 fatal: false,避免降低 Google Play 的“崩溃率评分”
         _log.severe('未处理的逻辑错误', error, stack);
         FirebaseCrashlytics.instance.recordError(error, stack, fatal: false);
 
         return true;
       };
+
+      _log.info('Firebase initialized successfully');
     } catch (e) {
-      debugPrint("Firebase couldn't be initialized: $e");
+      _log.warning("Firebase initialization failed: $e");
     }
   }
 
-  //本地参数存储初始化
-  await Persistence().initialize();
-
-  // 远程参数初始化
-  RemoteConfig().initialize();
-
-  // 记录程序运行时间
-  Persistence().lastRunTime = DateTime.now();
+  Future<void> _initPersistence() async {
+    try {
+      await Persistence().initialize().timeout(Duration(seconds: 5));
+      Persistence().lastRunTime = DateTime.now();
+      _log.info('Persistence initialized successfully');
+    } catch (e) {
+      _log.severe('Persistence initialization failed: $e');
+      rethrow;
+    }
+  }
 
-  Directory baseDir = await getApplicationDocumentsDirectory();
+  Future<Directory> _initBaseDirectory() async {
+    try {
+      return await getApplicationDocumentsDirectory();
+    } catch (e) {
+      _log.severe('Failed to get base directory: $e');
+      rethrow;
+    }
+  }
 
-  // 首次运行, 将json写入
-  if (Persistence().firstRun) {
-    final json = await loadJSONFromAsset('assets/builtin/${cfg.Config.firstId}.json');
-    await saveJson('work/${cfg.Config.firstId}.json', json);
+  void _initRemoteConfig() {
+    try {
+      RemoteConfig().initialize();
+      _log.info('RemoteConfig initialized');
+    } catch (e) {
+      _log.warning('RemoteConfig initialization failed: $e');
+    }
   }
 
-  runApp(MyApp(baseDir: baseDir));
-}
+  Future<void> _prepareFirstRunData() async {
+    if (!Persistence().firstRun) return;
 
-class MyApp extends StatelessWidget {
-  final Directory baseDir;
-  const MyApp({super.key, required this.baseDir});
+    try {
+      final json = await loadJSONFromAsset('assets/builtin/${cfg.Config.firstId}.json');
+      await saveJson('work/${cfg.Config.firstId}.json', json);
+      _log.info('First run data prepared');
+    } catch (e) {
+      _log.warning('Failed to prepare first run data: $e');
+    }
+  }
 
-  // This widget is the root of your application.
   @override
   Widget build(BuildContext context) {
-    cfg.Config config = cfg.Config(context, baseDir);
+    // 显示加载界面
+    if (!_isInitialized) {
+      return MaterialApp(
+        home: Scaffold(
+          backgroundColor: Color(0xfff4f2e9),
+          body: Center(
+            child: Column(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: [
+                // 使用原生最轻量的进度指示器
+                SizedBox(width: 40, height: 40, child: CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(Colors.green))),
+                const SizedBox(height: 20),
+                // 可选:添加一个简单的文字,让用户知道在加载
+                Text(
+                  "Loading...",
+                  style: TextStyle(color: Color.fromARGB(255, 38, 96, 12), fontSize: 14, fontWeight: FontWeight.w500),
+                ),
+              ],
+            ),
+          ),
+        ),
+      );
+    }
+
+    // 初始化失败时显示错误界面
+    if (_initError != null) {
+      return MaterialApp(
+        home: Scaffold(
+          backgroundColor: Color(0xfff4f2e9),
+          body: Center(
+            child: Padding(
+              padding: EdgeInsets.all(24),
+              child: Column(
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: [
+                  Icon(Icons.error_outline, size: 64, color: Colors.red),
+                  SizedBox(height: 16),
+                  Text('Initialization Failed', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
+                  SizedBox(height: 8),
+                  Text(
+                    _initError!,
+                    textAlign: TextAlign.center,
+                    style: TextStyle(color: Colors.grey),
+                  ),
+                  SizedBox(height: 24),
+                  ElevatedButton(
+                    onPressed: () {
+                      setState(() {
+                        _isInitialized = false;
+                        _initError = null;
+                      });
+                      _initializeAsync();
+                    },
+                    child: Text('Retry'),
+                  ),
+                ],
+              ),
+            ),
+          ),
+        ),
+      );
+    }
+
+    // 正常启动应用
+    cfg.Config config = cfg.Config(context, _baseDir);
     return AppLifecycleObserver(
       child: MultiProvider(
         providers: [
           Provider<Data>(lazy: false, create: (context) => Data(persistence: Persistence())..loadDataFromPersistence()),
           Provider<SettingsController>(lazy: false, create: (context) => SettingsController(persistence: Persistence())..loadStateFromPersistence()),
-          // ProxyProvider2<SettingsController, ValueNotifier<AppLifecycleState>, AudioController>(
-          //   lazy: false,
-          //   create: (context) => AudioController()..initialize(),
-          //   update: (context, settings, lifecycleNotifier, audio) {
-          //     if (audio == null) throw ArgumentError.notNull();
-          //     audio.attachSettings(settings);
-          //     audio.attachLifecycleNotifier(lifecycleNotifier);
-          //     return audio;
-          //   },
-          //   dispose: (context, audio) => audio.dispose(),
-          // ),
           ProxyProvider2<SettingsController, ValueNotifier<AppLifecycleState>, JcAudioController>(
             lazy: false,
             create: (context) => JcAudioController()..initialize(),
@@ -173,22 +295,17 @@ class MyApp extends StatelessWidget {
             },
             dispose: (context, audio) => audio.dispose(),
           ),
-
           Provider<ApplovinAdsController>(create: (context) => ApplovinAdsController(context)),
           Provider<cfg.Config>(lazy: false, create: (context) => config),
           Provider<Device>(lazy: false, create: (context) => config.device),
         ],
-
         child: Prepare(
           child: MaterialApp(
-            // key: GlobalKey(),
             title: 'Jigsort Solitaire',
-            // initialRoute: firstRun ? '/play' : '/', // 首次游戏直接进入游戏页面,而不是合集页
-            initialRoute: '/', // 统一先到HomeScreen, 再根据情况跳转到相应页面
+            initialRoute: '/',
             navigatorObservers: [routeObserver],
             routes: {
               '/': (context) => const HomeScreen(),
-              // '/': (context) => const GalleryScreen(),
               '/play': (context) => BoardPlay(
                 item: AssetItem(
                   cfg.Config.firstId,
@@ -203,18 +320,9 @@ class MyApp extends StatelessWidget {
                 firstRun: true,
               ),
             },
-            theme: ThemeData(
-              // textTheme: GoogleFonts.nunitoSansTextTheme(Theme.of(context).textTheme),
-              brightness: Brightness.light,
-              primaryColor: Colors.green,
-              primarySwatch: Colors.blue,
-            ),
-
-            ///多语言设置
+            theme: ThemeData(brightness: Brightness.light, primaryColor: Colors.green, primarySwatch: Colors.blue),
             localizationsDelegates: AppLocalizations.localizationsDelegates,
             supportedLocales: AppLocalizations.supportedLocales,
-
-            /// 设置默认语言为英语
             localeResolutionCallback: (Locale? locale, Iterable<Locale> supportedLocales) {
               var result = supportedLocales.where((element) => element.languageCode == locale?.languageCode);
               if (result.isNotEmpty) {
@@ -245,33 +353,34 @@ class _PrepareState extends State<Prepare> {
   @override
   void initState() {
     super.initState();
-    loadDeviceInfo();
+    // ✅ 保留:异步加载设备信息(不阻塞 UI)
+    _loadDeviceInfo();
   }
 
-  /// 获取android平台信息,用户判断是否低端机
-  loadDeviceInfo() async {
-    DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
-    if (Platform.isAndroid) {
-      try {
-        context.read<Device>().androidDeviceInfo = await deviceInfoPlugin.androidInfo;
-      } catch (e) {}
+  /// ✅ 优化:异步加载设备信息,不阻塞 UI
+  Future<void> _loadDeviceInfo() async {
+    if (!Platform.isAndroid) return;
+
+    try {
+      final deviceInfoPlugin = DeviceInfoPlugin();
+      final androidInfo = await deviceInfoPlugin.androidInfo;
+
+      if (mounted) {
+        context.read<Device>().androidDeviceInfo = androidInfo;
+        _log.info(
+          'Device info loaded: SDK ${androidInfo.version.sdkInt}, '
+          'LowRAM: ${androidInfo.isLowRamDevice}, '
+          'CPUs: ${Platform.numberOfProcessors}',
+        );
+      }
+    } catch (e) {
+      _log.warning('Failed to load device info: $e');
+      // 失败不影响应用运行
     }
   }
 
   @override
   Widget build(BuildContext context) {
-    // applovin max 没有类似的api可以获取banner高度,以下代码注释掉
-    //Update ad banner size
-    // AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(
-    //   MediaQuery.of(context).size.width.truncate(),
-    // ).then((value) {
-    //   if (value != null) {
-    //     context.read<Device>().bannerHeight = value.height.toDouble();
-    //   }
-    // }).catchError((err) {
-    //   //todo
-    // });
-
     return widget.child;
   }
 }

+ 114 - 65
lib/models/cached_request.dart

@@ -1,10 +1,9 @@
 import 'dart:async';
 import 'dart:convert';
 
-import 'package:flutter/material.dart'; // 引入 Flutter 核心包
+import 'package:flutter/material.dart';
 import 'package:http/http.dart' as http;
 import 'package:puzzleweave/models/api_helper.dart';
-import 'package:puzzleweave/models/data.dart';
 import 'package:puzzleweave/utils/utils.dart';
 import 'package:logging/logging.dart';
 
@@ -12,83 +11,85 @@ final Logger _log = Logger('cached_request.dart');
 
 typedef TransformFunction = Future<dynamic> Function(dynamic json);
 
-// 混入 WidgetsBindingObserver 以监听应用生命周期
 class CachedRequest with WidgetsBindingObserver {
-  // !!! 新增属性 1: 标记最近一次请求是否通过网络成功完成
+  /// ✅ 优化:网络状态跟踪
   bool _hasRecentSuccessfulFetch = false;
   bool get hasRecentSuccessfulFetch => _hasRecentSuccessfulFetch;
 
+  /// ✅ 优化:防抖机制
+  Timer? _refreshDebouncer;
+  static const _refreshDebounceDelay = Duration(seconds: 2);
+
+  /// ✅ 优化:重试机制
+  int _retryCount = 0;
+  static const _maxRetries = 3;
+  static const _retryDelay = Duration(seconds: 5);
+
   static final Map<String, CachedRequest> _cache = {};
 
   final String url;
   TransformFunction? transformFunction;
-  // 仅使用 .broadcast(),但 onListen 只会触发一次
   final StreamController _streamController = StreamController.broadcast();
 
   dynamic _transformed;
-  // --- 【新增】Getter:允许外部同步访问最新的缓存数据 ---
   dynamic get cachedData => _transformed;
 
+  /// ✅ 优化:请求状态跟踪
+  bool _isLoading = false;
+  bool get isLoading => _isLoading;
+
   CachedRequest._internal(this.url, this.transformFunction) {
     _log.info('New cached request: $url');
-    // 注册生命周期监听器
     WidgetsBinding.instance.addObserver(this);
     _init();
   }
 
   factory CachedRequest.fromUrl(String url, {TransformFunction? transformFunction}) {
-    // 确保单例模式下,只注册一次监听器
     if (!_cache.containsKey(url)) {
       _cache[url] = CachedRequest._internal(url, transformFunction);
     }
     return _cache[url]!;
   }
 
-  // --- 关键修改:生命周期监听 ---
   @override
   void didChangeAppLifecycleState(AppLifecycleState state) {
     if (state == AppLifecycleState.resumed) {
-      _log.info('App Resumed from background. Forcing refresh for $url');
-      // 应用程序从后台恢复到前台时,强制刷新数据
-      refresh();
+      _log.info('App Resumed from background. Scheduling refresh for $url');
+      /// ✅ 优化:使用防抖刷新而不是立即刷新
+      _debouncedRefresh();
     }
   }
 
-  // 由于 CachedRequest 是一个单例,它不会被销毁,除非应用完全关闭。
-  // 但是,为了严谨性,如果添加了 Dispose 逻辑,应记得移除 Observer。
-  // 注意:在 Flutter Provider 或 InheritedWidget 依赖的单例中,通常不需要手动调用 dispose。
-  // 如果需要清理:
-  /*
-  void dispose() {
-    WidgetsBinding.instance.removeObserver(this);
-    _streamController.close();
-    // ... clean up
-  }
-  */
-  // ---------------------------------
-
   _init() {
-    // FIX: 首次初始化时,立即尝试加载缓存(如果有)
     _cacheLoad();
     _remoteLoad();
-    // _streamController.onListen = () {
-    //   if (_transformed != null) {
-    //     // 如果有缓存数据,立即发送给新的监听者
-    //     _streamController.add(_transformed);
-    //   }
-    // };
 
     _streamController.onListen = () {
       _log.info('Stream listener added for $url. Current data available? ${_transformed != null}');
       if (_transformed != null) {
-        // 确保新订阅者立即获得缓存数据 (用于热重载或首次进入)
-        // 使用 Future.microtask 确保在 onListen 结束后再触发 add,避免同步递归
         Future.microtask(() => _streamController.add(_transformed));
       }
     };
   }
 
+  /// ✅ 优化:防抖刷新
+  void _debouncedRefresh() {
+    _refreshDebouncer?.cancel();
+    _refreshDebouncer = Timer(_refreshDebounceDelay, () {
+      if (!_isLoading) {
+        refresh();
+      }
+    });
+  }
+
   Future<void> refresh() async {
+    /// ✅ 优化:防止重复请求
+    if (_isLoading) {
+      _log.info('Refresh already in progress for $url, skipping');
+      return;
+    }
+    
+    _retryCount = 0;
     await _remoteLoad();
   }
 
@@ -96,39 +97,80 @@ class CachedRequest with WidgetsBindingObserver {
     await _cacheLoad();
   }
 
+  /// ✅ 优化:带重试机制的远程加载
   _remoteLoad() async {
+    if (_isLoading) return;
+    
+    _isLoading = true;
+    
     try {
-      final response = await http.get(Uri.parse(url));
+      _log.info('Starting remote load for $url (attempt ${_retryCount + 1})');
+      
+      /// ✅ 优化:添加超时控制
+      final response = await http.get(Uri.parse(url)).timeout(
+        const Duration(seconds: 30),
+        onTimeout: () => throw TimeoutException('Request timeout for $url', const Duration(seconds: 30)),
+      );
+      
       if (response.statusCode != 200) {
-        // 如果状态码失败,则标记为失败,并抛出异常
-        _hasRecentSuccessfulFetch = false; // !!! 关键:网络失败
+        _hasRecentSuccessfulFetch = false;
         throw Exception('Invalid status code: ${response.statusCode} when fetching: $url');
       }
 
-      // !!! 关键:网络请求成功,标记为成功
       _hasRecentSuccessfulFetch = true;
+      _retryCount = 0; // 重置重试计数
       _log.info('${response.statusCode}, $url, Network Success: true');
 
       final data = jsonDecode(response.body);
-      _emit(data);
-
+      await _emit(data);
       await saveString(cachePath, response.body);
+      
     } catch (error) {
-      _streamController.addError(error);
-      _log.severe('Remote load failed for $url: $error');
-      // 即使在 catch 块中,也再次确认标记为失败(以防万一)
       _hasRecentSuccessfulFetch = false;
+      _log.severe('Remote load failed for $url (attempt ${_retryCount + 1}): $error');
+      
+      /// ✅ 优化:智能重试机制
+      if (_retryCount < _maxRetries && _shouldRetry(error)) {
+        _retryCount++;
+        _log.info('Scheduling retry ${_retryCount}/$_maxRetries for $url in ${_retryDelay.inSeconds}s');
+        
+        Timer(_retryDelay, () {
+          if (!_isLoading) { // 确保没有其他请求在进行
+            _remoteLoad();
+          }
+        });
+      } else {
+        _streamController.addError(error);
+      }
+    } finally {
+      _isLoading = false;
+    }
+  }
+
+  /// ✅ 优化:判断是否应该重试
+  bool _shouldRetry(dynamic error) {
+    if (error is TimeoutException) return true;
+    if (error is Exception) {
+      final message = error.toString().toLowerCase();
+      // 网络相关错误可以重试
+      if (message.contains('timeout') || 
+          message.contains('connection') || 
+          message.contains('network') ||
+          message.contains('socket')) {
+        return true;
+      }
     }
+    return false;
   }
 
-  _emit(dynamic data) async {
-    _log.info('Emiting data..... ');
+  /// ✅ 优化:异步数据发射
+  Future<void> _emit(dynamic data) async {
+    _log.info('Emitting data for $url');
     try {
       if (transformFunction != null) {
         data = await transformFunction?.call(data);
       }
 
-      // 仅当有监听者时才尝试添加数据
       if (_streamController.hasListener) {
         _streamController.add(data);
       }
@@ -139,37 +181,44 @@ class CachedRequest with WidgetsBindingObserver {
     }
   }
 
+  /// ✅ 优化:缓存加载错误处理
   _cacheLoad() async {
     try {
       final file = await localFile(cachePath);
       if (await file.exists()) {
-        _log.info('File $file exists, try loading from cache..');
+        _log.info('Loading from cache: $cachePath');
         final data = await loadJson(cachePath);
-        _emit(data);
-      } else {
-        if (url == ApiHelper.latestUri) {
-          _log.info('Loading builtin latest asset data...');
-          final data = await loadJSONFromAsset('assets/builtin/latest.json');
-          _emit(data);
-        }
-        if (url == ApiHelper.collectionUri) {
-          _log.info('Loading builtin collection asset data...');
-          final data = await loadJSONFromAsset('assets/builtin/collection.json');
-          _emit(data);
-        }
+        await _emit(data);
+        return;
+      }
+
+      /// ✅ 优化:内置资源加载
+      if (url == ApiHelper.latestUri) {
+        _log.info('Loading builtin latest asset data...');
+        final data = await loadJSONFromAsset('assets/builtin/latest.json');
+        await _emit(data);
+      } else if (url == ApiHelper.collectionUri) {
+        _log.info('Loading builtin collection asset data...');
+        final data = await loadJSONFromAsset('assets/builtin/collection.json');
+        await _emit(data);
       }
     } catch (error) {
-      // 缓存加载失败不应该阻止远程加载
-      _streamController.addError(error);
+      _log.warning('Cache load failed for $url: $error');
+      // 缓存加载失败不应该阻止远程加载,只记录警告
     }
   }
 
+  /// ✅ 优化:清理资源
+  void dispose() {
+    _refreshDebouncer?.cancel();
+    WidgetsBinding.instance.removeObserver(this);
+    _streamController.close();
+  }
+
   String get hash => md5Hash(url);
   String get cachePath => 'api_cache/$hash';
   Stream get stream => _streamController.stream;
 
   @override
-  String toString() {
-    return 'CachedRequest(url=$url)';
-  }
-}
+  String toString() => 'CachedRequest(url=$url, loading=$_isLoading, hasRecentFetch=$_hasRecentSuccessfulFetch)';
+}

+ 5 - 0
lib/models/data.dart

@@ -90,6 +90,11 @@ class Data {
         } else {
           _log.warning('Cache file not found for ${item.id} at path: ${item.cachePath}');
         }
+        final tmpfile = await localFile('${item.cachePath}.tmp');
+        if (await tmpfile.exists()) {
+          await tmpfile.delete();
+          _log.info('Successfully cleared cache tmp file for ${item.id}');
+        }
       } catch (e) {
         _log.severe('Failed to clear cache for item ${item.id}, error: $e');
       }

+ 93 - 34
lib/models/download.dart

@@ -5,19 +5,20 @@ import 'package:flutter/foundation.dart';
 import 'package:http/http.dart';
 import 'package:logging/logging.dart';
 import 'package:puzzleweave/models/api_helper.dart';
-import 'package:puzzleweave/models/data.dart';
 import 'package:puzzleweave/models/items.dart';
 import 'package:puzzleweave/utils/utils.dart';
+import 'package:puzzleweave/utils/image_decoder.dart';
+
+import 'data.dart';
 
 final Logger _log = Logger('download.dart');
 
-/// 最多缓存/并发下载n个图到内存
-const maxCachedItems = 1;
+/// ✅ 优化:增加缓存数量到3个
+const maxCachedItems = 3;
 
 /// Singleton
 class Download {
   static final Download _instance = Download._internal();
-
   factory Download() => _instance;
   Download._internal();
 
@@ -39,11 +40,8 @@ class Download {
   _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);
     }
@@ -52,11 +50,9 @@ class Download {
   _clean() {
     final list = _cache.values.where((item) => item.loadCompleter.isCompleted).toList();
     if (list.length <= maxCachedItems) return;
-    _log.info('cleaning...');
+    _log.info('Cleaning download cache...');
 
-    // 2. 按最近使用时间排序(时间越早越应该被清理)
     list.sort((a, b) => a.lastUsed.compareTo(b.lastUsed));
-    // 3. 清理到只剩下 maxCachedItems 个
     while (list.length > maxCachedItems) {
       final item = list.removeAt(0);
       _log.info('Cleaning item from memory: $item');
@@ -65,6 +61,27 @@ class Download {
     }
   }
 
+  /// ✅ 新增:清理所有内存中的数据(保留下载任务)
+  void clearAllData() {
+    _log.info('Clearing all download data from memory');
+    int count = 0;
+    _cache.forEach((key, item) {
+      if (item.data != null) {
+        item.clearData();
+        count++;
+      }
+    });
+    _log.info('Cleared $count download items from memory');
+  }
+
+  /// ✅ 新增:获取当前缓存统计
+  Map<String, dynamic> getCacheStats() {
+    final total = _cache.length;
+    final completed = _cache.values.where((item) => item.loadCompleter.isCompleted).length;
+    final withData = _cache.values.where((item) => item.data != null).length;
+    return {'total': total, 'completed': completed, 'withData': withData};
+  }
+
   clearAllCached() async {
     final file = await localFile('cache');
     await file.delete(recursive: true);
@@ -75,11 +92,22 @@ class DownloadItem {
   final String url;
   final String cachePath;
   ValueNotifier<double> progress = ValueNotifier(0.0);
-  final Completer<void> loadCompleter = Completer(); // 仅作为完成信号
+  final Completer<void> loadCompleter = Completer();
   Client? client;
   StreamSubscription? subscription;
+
+  /// ✅ 新增:最后访问时间
+  DateTime? _lastAccessTime;
+
+  /// ✅ 新增:数据保留时长(5分钟未使用则自动清理)
+  static const _dataRetentionDuration = Duration(minutes: 5);
+
+  /// ✅ 新增:自动清理定时器
+  Timer? _cleanupTimer;
+
   int lastUsed;
   Uint8List? _data;
+  bool _isDisposed = false;
 
   DownloadItem(this.url, this.cachePath) : lastUsed = DateTime.now().millisecondsSinceEpoch {
     _log.info('New download item for: $url');
@@ -89,7 +117,10 @@ class DownloadItem {
   Uint8List? get data => _data;
   set data(Uint8List? val) => _data = val;
 
-  touch() => lastUsed = DateTime.now().millisecondsSinceEpoch;
+  touch() {
+    lastUsed = DateTime.now().millisecondsSinceEpoch;
+    _lastAccessTime = DateTime.now();
+  }
 
   _start() async {
     try {
@@ -105,14 +136,12 @@ class DownloadItem {
     final file = await localFile(cachePath);
     _checkDispose();
 
-    // 核心改造:如果文件存在,只报完成,不读数据 (Lazy Load)
     if (await file.exists()) {
       _log.info('Disk cache hit (Metadata only) for $cachePath');
       progress.value = 1.0;
       return;
     }
 
-    // 网络下载逻辑
     final List<int> bytes = [];
     client = Client();
     final response = await client!.send(Request('GET', Uri.parse(url)));
@@ -135,35 +164,73 @@ class DownloadItem {
     await streamCompleter.future;
     _checkDispose();
 
-    // 剔除 24 字节干扰码
-    if (bytes.length > 24) {
-      bytes.removeRange(0, 24);
+    if (bytes.length < 1024 + 24) {
+      throw Exception('Downloaded data is too small (${bytes.length} bytes)');
     }
 
-    _data = Uint8List.fromList(bytes);
-    await saveBytes(cachePath, _data!); // 写入磁盘
-    _log.info('Download and save complete for $url');
+    final cleanBytes = Uint8List.fromList(bytes.sublist(24));
+    _data = cleanBytes;
 
+    /// ✅ 原子性写入优化
+    final tempPath = '$cachePath.tmp';
+    await saveBytes(tempPath, _data!);
+    final tempFile = await localFile(tempPath);
+    final finalFile = await localFile(cachePath);
+    await tempFile.rename(finalFile.path);
+
+    _log.info('Download and save complete for $url');
     client?.close();
+
+    /// ✅ 调度自动清理
+    _scheduleCleanup();
   }
 
-  /// 供 Loader 真正需要数据时调用
+  /// 供 Loader 真正需要数据时调用
   Future<Uint8List> ensureDataLoaded() async {
-    if (_data != null) return _data!;
+    _lastAccessTime = DateTime.now();
+
+    if (_data != null) {
+      _log.info('Data cache hit for $cachePath');
+      _scheduleCleanup();
+      return _data!;
+    }
+
     _log.info('Performing late read from disk: $cachePath');
     final file = await localFile(cachePath);
     _data = await file.readAsBytes();
+    _scheduleCleanup();
     return _data!;
   }
 
-  bool _isDisposed = false;
+  /// ✅ 新增:调度自动清理
+  void _scheduleCleanup() {
+    _cleanupTimer?.cancel();
+    _cleanupTimer = Timer(_dataRetentionDuration, () {
+      if (_data != null && _lastAccessTime != null) {
+        final timeSinceLastAccess = DateTime.now().difference(_lastAccessTime!);
+        if (timeSinceLastAccess >= _dataRetentionDuration) {
+          _log.info('Auto-cleaning unused data: $cachePath');
+          _data = null;
+        }
+      }
+    });
+  }
+
+  /// ✅ 新增:手动清理数据
+  void clearData() {
+    _cleanupTimer?.cancel();
+    _data = null;
+    _log.info('Manually cleared data: $cachePath');
+  }
+
   _checkDispose() {
     if (_isDisposed) throw Exception('Disposed');
   }
 
   dispose() {
     _isDisposed = true;
-    _data = null; // 释放内存
+    _cleanupTimer?.cancel();
+    _data = null;
     subscription?.cancel();
     client?.close();
   }
@@ -179,14 +246,13 @@ abstract class ItemLoader {
 
   ItemLoader();
 
-  // 辅助方法:确保数据就绪后再解码
   Future<Uint8List> _prepareData() async {
     await completer.future;
     if (data == null) throw 'Data missing after completion';
     return data!;
   }
 
-  Future<ui.Image> getImageBySize(int width, int height, {allowUpscaling = true}) async {
+  Future<ui.Image> getImageBySize(int width, int height, {allowUpscaling = false}) async {
     final bytes = await _prepareData();
     final codec = await ui.instantiateImageCodec(bytes, targetHeight: height, targetWidth: width, allowUpscaling: allowUpscaling);
     return (await codec.getNextFrame()).image;
@@ -198,14 +264,12 @@ abstract class ItemLoader {
     return (await codec.getNextFrame()).image;
   }
 
-  /// 专门用于预加载的静态方法,不创建 Loader 实例
   static void preload(ListItem item, String quality) {
     if (BuiltinRegistry.contains(item.id)) {
       _log.info('Preload: Skipping builtin ${item.id}');
       return;
     }
     if (item is RemoteItem) {
-      // 触发 Download 但不等待读入内存
       Download().download(ApiHelper.imageUri(item.id, quality), item.cachePath);
     }
   }
@@ -264,7 +328,6 @@ class RemoteItemLoader extends ItemLoader {
   _load() async {
     try {
       await downloadItem.loadCompleter.future;
-      // 在这里不读 data,getImage 时才读
       completer.complete();
     } catch (err) {
       completer.completeError(err);
@@ -272,15 +335,11 @@ class RemoteItemLoader extends ItemLoader {
   }
 
   @override
-  Uint8List? get data {
-    // 同步获取(如果已读入内存),如果没读,需通过 getImage 异步触发
-    return downloadItem.data;
-  }
+  Uint8List? get data => downloadItem.data;
 
   @override
   Future<Uint8List> _prepareData() async {
     await completer.future;
-    // 关键点:如果内存里没数据(预加载命中的缓存),在此处执行补读
     return await downloadItem.ensureDataLoaded();
   }
 

+ 5 - 10
lib/play/board_painter.dart

@@ -110,7 +110,6 @@ class BoardPainter extends CustomPainter {
       // if (piece.group != null) {
       //   if (!drawnGroups.contains(piece.group)) {
       //     drawnGroups.add(piece.group!);
-      //     // 调用更新后的 _drawGroup 方法
       //     _drawGroup(canvas, size, piece.group!);
       //   }
       // } else {
@@ -355,14 +354,10 @@ class BoardPainter extends CustomPainter {
 
   @override
   bool shouldRepaint(covariant BoardPainter oldDelegate) {
-    // 检查不可变的核心数据是否发生变化
-    if (oldDelegate.board != board || oldDelegate.prepareAnimation != prepareAnimation) {
-      return true;
-    }
-
-    // 如果 board 和 animation controller 引用没有变化,
-    // 由于构造函数中已经添加了监听器,理论上只需要在监听器触发时重绘。
-    // 但为了确保,可以比较 board 的状态,因为它决定了执行哪个绘制逻辑。
-    return oldDelegate.board.status != board.status;
+    // 优化:只在必要时重绘
+    return oldDelegate.board != board || oldDelegate.prepareAnimation != prepareAnimation || oldDelegate.board.status != board.status;
   }
+
+  @override
+  bool shouldRebuildSemantics(covariant BoardPainter oldDelegate) => false;
 }

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 296 - 448
lib/play/board_play.dart


+ 135 - 0
lib/play/overlayer.bak.dart

@@ -0,0 +1,135 @@
+import 'dart:ui' as ui;
+
+import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
+import 'package:puzzleweave/play/board.dart';
+
+final _log = Logger('overlayer.dart');
+
+class HintItem {
+  final ui.Image finger;
+  final Rect srcRect;
+  final Rect destRect;
+  final CurveTween tween;
+
+  HintItem(this.finger, this.srcRect, this.destRect) : tween = CurveTween(curve: Curves.easeInOut);
+
+  Rect getCurrent(double t) {
+    return Rect.lerp(srcRect, destRect, tween.transform(t))!;
+  }
+
+  // 动画的前 10% 和后 10% 渐入渐出,中间 80% 完全不透明
+  double getOpacity(double t) {
+    if (t < 0.1) {
+      return t * 10;
+    } else if (t >= 0.1 && t <= 0.9) {
+      return 1;
+    } else {
+      return (1 - t) * 10;
+    }
+  }
+}
+
+class OverLayer {
+  final Board board;
+  final TickerProvider tickerProvider;
+  OverlayEntry? _overlayEntry;
+  bool _isDisposed = false;
+
+  final ValueNotifier<int> _notifier = ValueNotifier(0);
+
+  late AnimationController hintAnimation;
+  Listenable get notifiers => Listenable.merge([_notifier, hintAnimation]);
+
+  OverLayer(this.board, this.tickerProvider) {
+    hintAnimation = AnimationController(value: 0, vsync: tickerProvider, duration: const Duration(milliseconds: 1200));
+  }
+
+  void invalidate() {
+    _notifier.value++;
+  }
+
+  void setup(BuildContext context) {
+    if (_overlayEntry != null) return;
+
+    // 创建并插入 OverlayEntry
+    _overlayEntry = OverlayEntry(
+      builder: (context) {
+        return Positioned.fill(
+          // 关键:IgnorePointer 确保手势引导动画不会阻碍底层的 BoardPlay 交互
+          child: IgnorePointer(
+            child: RepaintBoundary(child: CustomPaint(painter: OverLayerPainter(this))),
+          ),
+        );
+      },
+    );
+    Overlay.of(context).insert(_overlayEntry!);
+  }
+
+  HintItem? _hintItem;
+  HintItem? get hintItem => _hintItem;
+
+  void doHint(HintItem hintItem) {
+    ///上一次hint没有完成
+    if (_isDisposed || _hintItem != null) {
+      return;
+    }
+
+    _hintItem = hintItem;
+    hintAnimation.reset();
+    hintAnimation.repeat(); // 重复播放动画
+  }
+
+  void stopHint() {
+    if (!_isDisposed && _hintItem != null) {
+      hintAnimation.stop();
+      _hintItem = null;
+
+      invalidate();
+    }
+  }
+
+  bool get isHinting => _hintItem != null && hintAnimation.isAnimating;
+
+  destroy() {
+    _isDisposed = true; // 标记已销毁
+    _overlayEntry?.remove();
+    _overlayEntry = null;
+    hintAnimation.dispose();
+  }
+}
+
+class OverLayerPainter extends CustomPainter {
+  final OverLayer overLayer;
+
+  OverLayerPainter(this.overLayer) : super(repaint: overLayer.notifiers);
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    // canvas.clipRect(Offset.zero & size);
+    if (overLayer.hintItem != null) {
+      _paintHintItem(canvas, overLayer.hintItem!);
+    }
+  }
+
+  void _paintHintItem(Canvas canvas, HintItem hintItem) {
+    Rect srcRect = Offset.zero & Size(hintItem.finger.width.toDouble(), hintItem.finger.height.toDouble());
+    Rect currentRect = hintItem.getCurrent(overLayer.hintAnimation.value);
+
+    // 完善点: 计算并应用透明度
+    double opacity = hintItem.getOpacity(overLayer.hintAnimation.value);
+
+    // 创建 Paint,通过 color.withOpacity 控制透明度
+    final paint = Paint()
+      ..color = Colors.white.withOpacity(opacity)
+      ..isAntiAlias = true;
+
+    // 绘制图片
+    canvas.drawImageRect(hintItem.finger, srcRect, currentRect, paint);
+  }
+
+  @override
+  bool shouldRepaint(covariant CustomPainter oldDelegate) {
+    return false;
+  }
+}

+ 6 - 2
lib/play/overlayer.dart

@@ -34,6 +34,7 @@ class OverLayer {
   final Board board;
   final TickerProvider tickerProvider;
   OverlayEntry? _overlayEntry;
+  bool _isDisposed = false;
 
   final ValueNotifier<int> _notifier = ValueNotifier(0);
 
@@ -70,16 +71,17 @@ class OverLayer {
 
   void doHint(HintItem hintItem) {
     ///上一次hint没有完成
-    if (_hintItem != null) {
+    if (_isDisposed || _hintItem != null) {
       return;
     }
+
     _hintItem = hintItem;
     hintAnimation.reset();
     hintAnimation.repeat(); // 重复播放动画
   }
 
   void stopHint() {
-    if (_hintItem != null) {
+    if (!_isDisposed && _hintItem != null) {
       hintAnimation.stop();
       _hintItem = null;
 
@@ -90,7 +92,9 @@ class OverLayer {
   bool get isHinting => _hintItem != null && hintAnimation.isAnimating;
 
   destroy() {
+    _isDisposed = true; // 标记已销毁
     _overlayEntry?.remove();
+    _overlayEntry = null;
     hintAnimation.dispose();
   }
 }

+ 233 - 0
lib/utils/image_decoder.dart

@@ -0,0 +1,233 @@
+// 图片解码优化工具类
+// 位置: lib/utils/image_decoder.dart
+//
+// 主要优化:
+// 1. 使用 compute 在 isolate 中解码,避免阻塞主线程
+// 2. 添加解码超时保护
+// 3. 支持批量解码
+// 4. 内存优化:解码后立即释放原始数据
+
+import 'dart:async';
+import 'dart:io';
+import 'dart:ui' as ui;
+import 'package:flutter/foundation.dart';
+import 'package:logging/logging.dart';
+
+final Logger _log = Logger('image_decoder.dart');
+
+class ImageDecoder {
+  // ✅ 在 isolate 中解码单张图片
+  static Future<ui.Image> decodeImage({
+    required Uint8List bytes,
+    int? targetWidth,
+    int? targetHeight,
+    bool allowUpscaling = true,
+    Duration timeout = const Duration(seconds: 30),
+  }) async {
+    try {
+      // 使用 compute 在独立 isolate 中执行解码
+      final image = await compute(
+        _decodeImageInIsolate,
+        _DecodeParams(bytes: bytes, targetWidth: targetWidth, targetHeight: targetHeight, allowUpscaling: allowUpscaling),
+      ).timeout(timeout);
+
+      _log.info('Image decoded successfully: ${image.width}x${image.height}');
+
+      return image;
+    } on TimeoutException {
+      _log.severe('Image decode timeout after ${timeout.inSeconds}s');
+      throw Exception('Image decode timeout');
+    } catch (e, stack) {
+      _log.severe('Image decode failed', e, stack);
+      rethrow;
+    }
+  }
+
+  // ✅ 批量解码多张图片(并行)
+  static Future<List<ui.Image>> decodeImages({
+    required List<Uint8List> bytesList,
+    List<int?>? targetWidths,
+    List<int?>? targetHeights,
+    bool allowUpscaling = true,
+    Duration timeout = const Duration(seconds: 60),
+  }) async {
+    try {
+      final futures = <Future<ui.Image>>[];
+
+      for (int i = 0; i < bytesList.length; i++) {
+        futures.add(
+          decodeImage(bytes: bytesList[i], targetWidth: targetWidths?[i], targetHeight: targetHeights?[i], allowUpscaling: allowUpscaling, timeout: timeout),
+        );
+      }
+
+      return await Future.wait(futures);
+    } catch (e, stack) {
+      _log.severe('Batch image decode failed', e, stack);
+      rethrow;
+    }
+  }
+
+  // ✅ 从文件路径解码(自动读取文件)
+  static Future<ui.Image> decodeImageFromFile({required String filePath, int? targetWidth, int? targetHeight, bool allowUpscaling = true}) async {
+    try {
+      final bytes = await compute(_readFileBytes, filePath);
+      return await decodeImage(bytes: bytes, targetWidth: targetWidth, targetHeight: targetHeight, allowUpscaling: allowUpscaling);
+    } catch (e, stack) {
+      _log.severe('Failed to decode image from file: $filePath', e, stack);
+      rethrow;
+    }
+  }
+
+  // ✅ 预解码(用于预加载,不返回结果)
+  static Future<void> preDecodeImage({required Uint8List bytes, int? targetWidth, int? targetHeight}) async {
+    try {
+      final image = await decodeImage(bytes: bytes, targetWidth: targetWidth, targetHeight: targetHeight);
+      // 立即释放,只是为了触发解码缓存
+      image.dispose();
+    } catch (e) {
+      _log.warning('Pre-decode failed (non-critical): $e');
+    }
+  }
+}
+
+// ===== Isolate 函数(必须是顶层函数或静态函数)=====
+
+// 解码参数类
+class _DecodeParams {
+  final Uint8List bytes;
+  final int? targetWidth;
+  final int? targetHeight;
+  final bool allowUpscaling;
+
+  _DecodeParams({required this.bytes, this.targetWidth, this.targetHeight, this.allowUpscaling = true});
+}
+
+// ✅ 在 isolate 中执行的解码函数
+Future<ui.Image> _decodeImageInIsolate(_DecodeParams params) async {
+  final codec = await ui.instantiateImageCodec(
+    params.bytes,
+    targetWidth: params.targetWidth,
+    targetHeight: params.targetHeight,
+    allowUpscaling: params.allowUpscaling,
+  );
+
+  final frameInfo = await codec.getNextFrame();
+  return frameInfo.image;
+}
+
+// ✅ 在 isolate 中读取文件
+Future<Uint8List> _readFileBytes(String filePath) async {
+  final file = File(filePath);
+  return await file.readAsBytes();
+}
+
+// ===== 使用示例 =====
+
+/*
+// 在 BoardPlay._init() 中使用:
+
+_init() async {
+  Device device = context.read<Device>();
+
+  setState(() {
+    _isLoading = true;
+  });
+
+  try {
+    final dpr = device.effectivePixelRatio;
+    final targetRect = device.targetRect;
+    final bestImageSize = device.bestImageSize;
+
+    // ✅ 并行解码主图和卡牌图
+    final imageBytes = await itemLoader.ensureDataLoaded();
+    final cardBytes = await rootBundle.load(
+      widget.item.hard 
+        ? 'assets/images/backcard_red.png' 
+        : 'assets/images/backcard_blue.png'
+    );
+
+    final Size bestCardImageSize = Size(
+      targetRect.width * dpr / widget.item.rows,
+      targetRect.height * dpr / widget.item.cols,
+    );
+
+    // ✅ 使用优化的解码器
+    final images = await ImageDecoder.decodeImages(
+      bytesList: [
+        imageBytes,
+        cardBytes.buffer.asUint8List(),
+      ],
+      targetWidths: [
+        bestImageSize.width.round(),
+        bestCardImageSize.width.round(),
+      ],
+      targetHeights: [
+        bestImageSize.height.round(),
+        bestCardImageSize.height.round(),
+      ],
+    );
+
+    final image = images[0];
+    final cardImage = images[1];
+
+    _log.info('Images decoded: main=${image.width}x${image.height}, card=${cardImage.width}x${cardImage.height}');
+
+    // 3. 构建或恢复 Board 实例
+    if (widget.reset) {
+      board = await Board.create(
+        this, image, cardImage, 
+        widget.item.rows, widget.item.cols, 
+        widget.item.hard, targetRect, device
+      );
+    } else {
+      final jsonFile = await localFile(widget.item.jsonPath);
+      if (await jsonFile.exists()) {
+        showDealing = false;
+        board = await Board.restore(
+          this, image, cardImage,
+          widget.item.rows, widget.item.cols,
+          widget.item.hard, targetRect, device,
+          widget.item.jsonPath
+        );
+      } else {
+        board = await Board.create(
+          this, image, cardImage,
+          widget.item.rows, widget.item.cols,
+          widget.item.hard, targetRect, device
+        );
+        _reportLevelStart();
+      }
+    }
+
+    // 4. 初始化准备
+    board!.prepare();
+    _loadFingerImageAndSetupHint();
+
+    if (mounted) {
+      setState(() {
+        _isLoading = false;
+      });
+    }
+
+    // 5. 启动入场动画
+    if (showDealing) {
+      _prepareAnimationController.forward(from: 0.0);
+    } else {
+      board!.start();
+    }
+  } catch (error, stack) {
+    _log.severe('Board _init critical error', error, stack);
+
+    if (mounted) {
+      Fluttertoast.showToast(
+        msg: AppLocalizations.of(context)!.networkNotGood,
+        toastLength: Toast.LENGTH_LONG,
+        gravity: ToastGravity.CENTER,
+        backgroundColor: SkinHelper.slotBorderColor,
+        textColor: Colors.white,
+      );
+      Navigator.pop(context);
+    }
+  }
+}
+*/

+ 216 - 0
lib/utils/memory_monitor.dart

@@ -0,0 +1,216 @@
+// memory_monitor.dart
+// 强化版内存监控工具类
+
+import 'dart:async';
+import 'dart:io';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
+import 'package:puzzleweave/firebase/firebase_helper.dart';
+import 'package:puzzleweave/models/download.dart';
+
+final Logger _log = Logger('MemoryMonitor');
+
+/// 内存阶段定义
+enum MemoryState { normal, warning, critical }
+
+class MemoryMonitor with WidgetsBindingObserver {
+  static final MemoryMonitor _instance = MemoryMonitor._internal();
+  factory MemoryMonitor() => _instance;
+  MemoryMonitor._internal();
+
+  Timer? _monitorTimer;
+  int _warningCount = 0;
+
+  // 防止系统内存压力信号死循环
+  DateTime? _lastSystemPressureTime;
+  static const _systemPressureCooldown = Duration(seconds: 10);
+
+  // 动态生成的阈值
+  double _warningThreshold = 180.0;
+  double _criticalThreshold = 250.0;
+
+  VoidCallback? _onHighMemory;
+  VoidCallback? _onCriticalMemory;
+  
+  // Release模式下的内存显示
+  static final ValueNotifier<String> memoryDisplay = ValueNotifier<String>('Memory: 0MB');
+
+  /// 初始化并启动监控
+  void startMonitoring({Duration interval = const Duration(seconds: 30), VoidCallback? onHighMemory, VoidCallback? onCriticalMemory}) {
+    _onHighMemory = onHighMemory;
+    _onCriticalMemory = onCriticalMemory;
+
+    // 1. 注册系统内存压力监听 (WidgetsBindingObserver)
+    WidgetsBinding.instance.addObserver(this);
+
+    // 2. 根据设备情况初始化动态阈值
+    _initializeDynamicThresholds();
+
+    // 3. 启动定时轮询
+    _monitorTimer?.cancel();
+    _monitorTimer = Timer.periodic(interval, (_) => _checkMemory());
+
+    _log.info('Memory monitoring started. Warning: ${_warningThreshold}MB, Critical: ${_criticalThreshold}MB');
+  }
+
+  /// 停止监控
+  void stopMonitoring() {
+    WidgetsBinding.instance.removeObserver(this);
+    _monitorTimer?.cancel();
+    _log.info('Memory monitoring stopped');
+  }
+
+  /// 这是进程被杀前的最后一次清理机会
+  @override
+  void didHaveMemoryPressure() {
+    final now = DateTime.now();
+
+    // 防止频繁触发,增加冷却时间
+    if (_lastSystemPressureTime != null && now.difference(_lastSystemPressureTime!) < _systemPressureCooldown) {
+      _log.info('System memory pressure ignored (cooldown)');
+      return;
+    }
+
+    _lastSystemPressureTime = now;
+    _log.severe('!!! SYSTEM MEMORY PRESSURE: ${getCurrentMemoryMB().toStringAsFixed(1)}MB !!!');
+
+    _emergencyCleanup(reason: 'system_pressure');
+    _reportMemoryEvent('system_low_memory_signal', getCurrentMemoryMB());
+  }
+
+  /// 动态计算阈值:防止在低端机上设置过高,或在高端机上太敏感
+  void _initializeDynamicThresholds() {
+    if (Platform.isAndroid) {
+      if (kDebugMode) {
+        // Debug模式更保守,避免频繁清理
+        _warningThreshold = 400.0;
+        _criticalThreshold = 500.0;
+      } else {
+        // Release模式预计内存减半
+        _warningThreshold = 120.0;
+        _criticalThreshold = 160.0;
+      }
+    } else if (Platform.isIOS) {
+      if (kDebugMode) {
+        _warningThreshold = 300.0;
+        _criticalThreshold = 400.0;
+      } else {
+        _warningThreshold = 150.0;
+        _criticalThreshold = 200.0;
+      }
+    } else {
+      _warningThreshold = 500.0;
+      _criticalThreshold = 700.0;
+    }
+  }
+
+  void _checkMemory() {
+    if (!Platform.isAndroid && !Platform.isIOS) return;
+
+    try {
+      final memoryMB = getCurrentMemoryMB();
+      
+      // 更新UI显示
+      memoryDisplay.value = 'Memory: ${memoryMB.toStringAsFixed(1)}MB';
+
+      // 只在超过warning时才记录,减少日志噪音
+      if (memoryMB > _warningThreshold) {
+        _log.info('Memory: ${memoryMB.toStringAsFixed(1)} MB');
+      }
+
+      if (memoryMB > _criticalThreshold) {
+        _handleCriticalMemory(memoryMB);
+      } else if (memoryMB > _warningThreshold) {
+        _handleHighMemory(memoryMB);
+      } else {
+        _warningCount = 0;
+      }
+    } catch (e) {
+      _log.warning('Memory check failed: $e');
+    }
+  }
+
+  void _handleCriticalMemory(double memoryMB) {
+    _log.severe('CRITICAL memory usage: ${memoryMB.toStringAsFixed(1)} MB');
+    _warningCount++;
+    _onCriticalMemory?.call();
+    _emergencyCleanup(reason: 'critical_threshold');
+    _reportMemoryEvent('critical_memory_usage', memoryMB);
+  }
+
+  void _handleHighMemory(double memoryMB) {
+    _log.warning('HIGH memory usage: ${memoryMB.toStringAsFixed(1)} MB');
+    _warningCount++;
+    _onHighMemory?.call();
+    _reportMemoryEvent('high_memory_usage', memoryMB);
+  }
+
+  /// 核心清理逻辑:物理释放显存和缓存
+  void _emergencyCleanup({String reason = 'unknown'}) {
+    final beforeMB = getCurrentMemoryMB();
+    _log.warning('Emergency cleanup: ${beforeMB.toStringAsFixed(1)}MB ($reason)');
+
+    try {
+      // 清理顺序很重要,先清理大头
+      Download().clearAllData();
+      PaintingBinding.instance.imageCache.clear();
+      PaintingBinding.instance.imageCache.clearLiveImages();
+      PaintingBinding.instance.handleMemoryPressure();
+
+      // 立即检查效果
+      final afterMB = getCurrentMemoryMB();
+      final freedMB = beforeMB - afterMB;
+      _log.info('Cleanup result: ${afterMB.toStringAsFixed(1)}MB (${freedMB > 0 ? "freed" : "increased"}: ${freedMB.abs().toStringAsFixed(1)}MB)');
+    } catch (e) {
+      _log.severe('Emergency cleanup failed: $e');
+    }
+  }
+
+  void _reportMemoryEvent(String eventName, double memoryMB) {
+    if (kReleaseMode) {
+      FirebaseHelper.logEvent(eventName, {'memory_mb': memoryMB.round(), 'warning_count': _warningCount, 'threshold_c': _criticalThreshold.round()});
+    }
+  }
+
+  static double getCurrentMemoryMB() {
+    try {
+      // RSS 包含代码段、栈、堆以及共享库。
+      // 在三星等低端机上,这是判断 OOM 最直接的指标。
+      return ProcessInfo.currentRss / (1024 * 1024);
+    } catch (e) {
+      return 0;
+    }
+  }
+
+  /// 供外部在关键节点(如播广告前)手动清理
+  void manualCleanup() {
+    _emergencyCleanup(reason: 'manual_call');
+  }
+
+  static void logMemoryUsage(String label) {
+    final memoryMB = getCurrentMemoryMB();
+    memoryDisplay.value = '[$label] ${memoryMB.toStringAsFixed(1)}MB';
+    _log.info('[$label] RSS Memory: ${memoryMB.toStringAsFixed(1)} MB');
+  }
+  
+  /// 获取内存显示组件(供Release模式使用)
+  static Widget getMemoryWidget() {
+    return ValueListenableBuilder<String>(
+      valueListenable: memoryDisplay,
+      builder: (context, value, child) {
+        return Container(
+          padding: EdgeInsets.all(4),
+          decoration: BoxDecoration(
+            color: Colors.black54,
+            borderRadius: BorderRadius.circular(4),
+          ),
+          child: Text(
+            value,
+            style: TextStyle(color: Colors.white, fontSize: 12),
+          ),
+        );
+      },
+    );
+  }
+}

+ 0 - 8
pubspec.lock

@@ -518,14 +518,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.3.0"
-  lottie:
-    dependency: "direct main"
-    description:
-      name: lottie
-      sha256: c5fa04a80a620066c15cf19cc44773e19e9b38e989ff23ea32e5903ef1015950
-      url: "https://pub.dev"
-    source: hosted
-    version: "3.3.1"
   matcher:
     dependency: transitive
     description:

+ 0 - 2
pubspec.yaml

@@ -52,7 +52,6 @@ dependencies:
   intl: any
   path: ^1.9.1
   fluttertoast: ^9.0.0
-  lottie: ^3.3.1
   cached_network_image: ^3.4.1
   applovin_max: ^3.10.0
   firebase_core: ^4.2.1
@@ -110,7 +109,6 @@ flutter:
     - assets/audio/sfx/
     - assets/audio/bgm/
     - assets/builtin/
-    - assets/lottie/
 
   # An image asset can refer to one or more resolution-specific "variants", see
   # https://flutter.dev/to/resolution-aware-images

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác