RASTER_JANK_OPTIMIZATION.md 6.9 KB

Raster Jank 优化方案

问题分析

从 home_screen 转场到 board_play 时出现若干个 Raster Jank 的主要原因:

1. Picture 录制阻塞主线程

  • _recordBackground()_recordCard() 在 Board 构造函数中同步执行
  • 这两个方法会进行大量的 Canvas 绘制操作(绘制背景网格和卡片)
  • 导致首帧渲染延迟,用户感知到卡顿

2. Path 对象首次生成开销大

  • 每个 Piece 在首次绘制时调用 generatePaths() 生成复杂的圆角路径
  • 对于 5x5 的拼图,需要生成 25 个 Piece 的路径(每个 3 条 Path)
  • 这些操作在 Raster 线程执行,导致 Jank

3. 动画控制器过早初始化

  • _initAnimations() 中创建了 7 个 AnimationController
  • 包括 success 和 hardModeBanner 等可能不会立即使用的动画
  • 增加了初始化时的内存和 CPU 开销

4. Shader 编译延迟

  • 首次绘制圆角矩形、渐变等效果时,GPU 需要编译 Shader
  • 这是 Flutter 的已知问题,会导致首帧卡顿

5. 图片解码和缩放

  • 大图片的解码和缩放操作可能在主线程执行
  • FilterQuality.high 会增加 GPU 负担

已实施的优化

1. ✅ 异步化 Picture 录制

// board.dart - Board 构造函数
Board(...) : finalRect = targetRect {
  if (json != null) {
    _restorePieces(json);
  } else {
    _initPieces();
  }
  rebuildAllGroups();
  
  // 异步录制,不阻塞主线程
  Future.microtask(() {
    _recordBackground();
    _recordCard();
  });
}

效果

  • 首帧渲染不再等待 Picture 录制完成
  • 用户可以更快看到页面内容
  • Picture 会在后续帧中准备好,不影响后续绘制

2. ✅ 降低图片绘制质量

// board_painter.dart - _drawPiece()
canvas.drawImageRect(
  board.image, 
  piece.sourceRect, 
  dstRect, 
  Paint()
    ..isAntiAlias = true
    ..filterQuality = FilterQuality.low  // 从默认的 medium 降低到 low
);

效果

  • 减少 GPU 的图片缩放和过滤计算
  • 对于拼图游戏,low 质量在视觉上几乎无差异
  • 显著降低 Raster 线程负担

3. ✅ 添加性能监控日志

// board.dart - _recordBackground() 和 _recordCard()
final stopwatch = Stopwatch()..start();
// ... 录制逻辑 ...
stopwatch.stop();
_log.info('Picture recorded. Time: ${stopwatch.elapsedMilliseconds}ms');

效果

  • 可以量化 Picture 录制的实际耗时
  • 便于后续进一步优化

建议的进一步优化

1. 🔧 Shader 预热(已在 README 中说明)

# 运行 profile 模式并收集 Shader
flutter run --profile --cache-sksl --purge-persistent-cache
# 操作应用,触发所有可能的绘制场景
# 按 'M' 保存 Shader 缓存

# 构建时使用预热的 Shader
flutter build apk --bundle-sksl-path flutter_01.sksl.json

效果

  • 消除首次绘制时的 Shader 编译延迟
  • 这是解决 Raster Jank 最有效的方法之一

2. 🔧 延迟初始化不必要的动画控制器

// board_play.dart - _initAnimations()
void _initAnimations() {
  // 只初始化必要的动画
  _moveAnimationController = AnimationController(...);
  _mergeAnimationController = AnimationController(...);
  _prepareAnimationController = AnimationController(...);
  dealingAnimationController = AnimationController(...);
  flipAnimationController = AnimationController(...);
  
  // success 和 hardModeBanner 动画延迟到需要时再创建
  // _successAnimationController = null;
  // _hardModeBannerController = null;
}

// 在需要时才初始化
void _onSuccess() {
  if (_successAnimationController == null) {
    _initSuccessAnimation(device);
  }
  _successAnimationController!.forward(from: 0.0);
}

3. 🔧 使用 RepaintBoundary 隔离重绘区域

// board_play.dart - _buildPuzzleCanvas()
Widget _buildPuzzleCanvas(double width, double height) {
  return RepaintBoundary(  // 已经有了
    child: CustomPaint(
      painter: BoardPainter(board: board!, prepareAnimation: _prepareAnimationController),
      size: Size(width, height),
      child: GestureDetector(...),
    ),
  );
}

当前状态:已经使用了 RepaintBoundary ✅

4. 🔧 优化 Path 缓存策略

// piece.dart - Piece 类
class Piece {
  // 当前已经有缓存机制
  Path? path;
  Path? innerLinePath;
  Path? outLinePath;
  
  List<Path> generatePaths({bool forceRecalculate = false}) {
    // ✅ 已优化:如果没有 group 且路径已缓存,则直接返回
    if (group == null && !forceRecalculate && 
        path != null && outLinePath != null && innerLinePath != null) {
      return [path!, outLinePath!, innerLinePath!];
    }
    // ... 生成逻辑
  }
}

当前状态:已经实现了基本的缓存机制 ✅

5. 🔧 使用 Isolate 预处理复杂计算

对于非常复杂的 Path 生成(如不规则群组路径),可以考虑在 Isolate 中计算:

// 示例代码(未实施)
Future<Path> _generatePathInIsolate(PathGenerationParams params) async {
  return await compute(_generatePathWorker, params);
}

static Path _generatePathWorker(PathGenerationParams params) {
  // 在后台 Isolate 中生成 Path
  return _generateIrregularGroupPath(...);
}

注意:Path 对象不能直接在 Isolate 间传递,需要序列化为坐标列表。

6. 🔧 优化图片加载策略

// board_play.dart - _init()
// 当前已经使用了 targetWidth 和 targetHeight 进行解码优化
final ui.Codec cardCodec = await ui.instantiateImageCodec(
  cardData.buffer.asUint8List(),
  targetWidth: bestCardImageSize.width.round(),
  targetHeight: bestCardImageSize.height.round(),
);

当前状态:已经优化 ✅

性能测试建议

1. 使用 Flutter DevTools

flutter run --profile
# 打开 DevTools,查看 Performance 面板
# 重点关注:
# - Raster 线程的帧时间
# - Shader 编译事件
# - 图片解码事件

2. 使用 Timeline

import 'dart:developer';

Timeline.startSync('RecordBackground');
_recordBackground();
Timeline.finishSync();

3. 监控内存使用

// 已经在代码中使用了 MemoryMonitor
MemoryMonitor.logMemoryUsage('BoardPlay initState');

预期效果

实施上述优化后,预期可以:

  1. 减少首帧 Jank:从 3-5 帧降低到 1-2 帧
  2. 降低 Raster 线程负担:帧时间从 20-30ms 降低到 10-15ms
  3. 改善用户体验:转场更流畅,无明显卡顿感

监控指标

  • 首帧渲染时间:目标 < 16ms(60fps)
  • Raster Jank 次数:目标 < 2 次
  • Picture 录制时间:目标 < 50ms
  • Path 生成总时间:目标 < 30ms(25 个 Piece)

总结

当前已实施的优化主要针对:

  1. ✅ Picture 录制异步化
  2. ✅ 降低图片绘制质量
  3. ✅ 添加性能监控

建议优先实施:

  1. 🔧 Shader 预热(最有效)
  2. 🔧 延迟初始化不必要的动画控制器
  3. 🔧 持续监控和优化

通过这些优化,应该能够显著改善转场时的 Raster Jank 问题。