Files
nj/app/lib/features/editor/widgets/stroke_cache.dart
iven 9ce300ddb9 fix(app): 修复笔画缓存 use-after-dispose — 移除增量合成时的提前 dispose
- _compositeIncremental 中不再 dispose strokeImage,因为 _cache 持有同一引用
- 提前 dispose 导致 syncStrokes/clear/dispose 时 double-dispose(use-after-free)
- 单笔画 image 生命周期由缓存统一管理:移除/清除/销毁时释放
- 更新 _rebuildComposite 注释,移除过时说明

审计 ID: 8b-R01
2026-06-03 01:06:34 +08:00

299 lines
8.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 笔画光栅化缓存 — 将已完成笔画渲染为 ui.Image避免每帧重算
//
// 核心思路:
// 1. 每条笔画完成时,通过 ui.PictureRecorder 光栅化为独立 ui.Image
// 2. 将所有单笔画图像合成为一个 compositeImage在 saveLayer 中正确处理橡皮擦)
// 3. CachedStrokesPainter 每帧仅 drawImage(compositeImage)O(1) 操作
// 4. 新笔画完成 → 光栅化 → 合成到 compositeImage增量不需全部重算
// 5. 撤销/重做/清除 → 移除对应缓存条目 → 重建 compositeImage
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'stroke_model.dart';
import 'stroke_renderer.dart';
// ===== 缓存条目 =====
/// 单笔画光栅化缓存条目
class _CacheEntry {
final ui.Image image;
final Stroke stroke;
const _CacheEntry({required this.image, required this.stroke});
}
// ===== 光栅化缓存 =====
/// 笔画光栅化缓存管理器
///
/// 由 _HandwritingCanvasState 持有,生命周期与 State 相同。
/// dispose() 时释放所有 ui.Image GPU 资源。
class StrokeRasterCache {
final Map<String, _CacheEntry> _cache = {};
/// 所有已完成笔画的合成位图
ui.Image? _compositeImage;
/// 合成图版本号,每次重建递增,驱动 CachedStrokesPainter.shouldRepaint
int _layerVersion = 0;
/// 当前画布尺寸
Size _canvasSize = Size.zero;
/// 是否需要重建合成图
bool _dirty = false;
// ===== Getters =====
/// 合成位图(可能为 null表示画布为空
ui.Image? get compositeImage => _compositeImage;
/// 合成图版本号
int get layerVersion => _layerVersion;
/// 当前画布尺寸
Size get canvasSize => _canvasSize;
/// 缓存的笔画数量
int get length => _cache.length;
/// 已缓存的笔画 ID 集合
Set<String> get cachedStrokeIds => _cache.keys.toSet();
// ===== 尺寸管理 =====
/// 确保画布尺寸正确,变化时整体失效
void ensureSize(Size size) {
if (_canvasSize != size) {
_canvasSize = size;
invalidateAll();
}
}
// ===== 笔画操作 =====
/// 添加一条已完成笔画到缓存
///
/// 光栅化该笔画为 ui.Image然后增量合成到 compositeImage。
Future<void> addStroke(Stroke stroke) async {
if (_canvasSize == Size.zero) return;
// 光栅化单笔画
final image = await _rasterizeStroke(stroke);
if (image == null) return;
_cache[stroke.id] = _CacheEntry(image: image, stroke: stroke);
// 增量合成:将新笔画绘制到现有 compositeImage 之上
await _compositeIncremental(stroke, image);
}
/// 同步笔画列表(用于撤销/重做后与 BLoC 状态对齐)
///
/// 比较当前缓存和传入的笔画列表,移除多余的、标记需要重建的。
Future<void> syncStrokes(List<Stroke> strokes) async {
final currentIds = strokes.map((s) => s.id).toSet();
final cachedIds = _cache.keys.toSet();
// 移除缓存中多余的条目(撤销的笔画)
final toRemove = cachedIds.difference(currentIds);
for (final id in toRemove) {
final entry = _cache.remove(id);
entry?.image.dispose();
}
// 添加缺失的条目(重做的笔画)
final toAdd = currentIds.difference(cachedIds);
for (final stroke in strokes) {
if (toAdd.contains(stroke.id)) {
final image = await _rasterizeStroke(stroke);
if (image != null) {
_cache[stroke.id] = _CacheEntry(image: image, stroke: stroke);
}
}
}
// 任何变更都需要重建合成图
if (toRemove.isNotEmpty || toAdd.isNotEmpty) {
_dirty = true;
await _rebuildComposite();
}
}
/// 清除所有缓存
Future<void> clear() async {
for (final entry in _cache.values) {
entry.image.dispose();
}
_cache.clear();
_compositeImage?.dispose();
_compositeImage = null;
_layerVersion++;
_dirty = false;
}
/// 整体失效(画布尺寸变化时)
Future<void> invalidateAll() async {
final strokes = _cache.values.map((e) => e.stroke).toList();
await clear();
for (final stroke in strokes) {
await addStroke(stroke);
}
}
/// 释放所有 GPU 资源
void dispose() {
for (final entry in _cache.values) {
entry.image.dispose();
}
_cache.clear();
_compositeImage?.dispose();
_compositeImage = null;
}
// ===== 光栅化 =====
/// 将单条笔画光栅化为 ui.Image
Future<ui.Image?> _rasterizeStroke(Stroke stroke) async {
final outlinePoints = pointsToOutline(
stroke.points,
stroke.brushType,
stroke.width,
isComplete: true,
);
if (outlinePoints.isEmpty) return null;
final path = buildStrokePath(outlinePoints);
final paint = createPaintForStroke(stroke);
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
// 橡皮擦需要 saveLayer 保护,避免穿透
if (stroke.brushType == BrushType.eraser) {
canvas.saveLayer(
Rect.fromLTWH(0, 0, _canvasSize.width, _canvasSize.height),
Paint(),
);
}
canvas.drawPath(path, paint);
if (stroke.brushType == BrushType.eraser) {
canvas.restore();
}
final picture = recorder.endRecording();
return picture.toImage(
_canvasSize.width.toInt().clamp(1, 4096),
_canvasSize.height.toInt().clamp(1, 4096),
);
}
// ===== 合成 =====
/// 增量合成:将新笔画图像绘制到现有 compositeImage 上
Future<void> _compositeIncremental(Stroke stroke, ui.Image strokeImage) async {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
// saveLayer 创建离屏缓冲区,橡皮擦 dstOut 在其中正确合成
canvas.saveLayer(
Rect.fromLTWH(0, 0, _canvasSize.width, _canvasSize.height),
Paint(),
);
// 先绘制现有的合成图
if (_compositeImage != null) {
canvas.drawImage(_compositeImage!, Offset.zero, Paint());
}
// 再绘制新笔画(橡皮擦用 dstOut
if (stroke.brushType == BrushType.eraser) {
canvas.drawImage(
strokeImage,
Offset.zero,
Paint()..blendMode = BlendMode.dstOut,
);
} else {
canvas.drawImage(strokeImage, Offset.zero, Paint());
}
canvas.restore();
// 释放旧的合成图
_compositeImage?.dispose();
final picture = recorder.endRecording();
_compositeImage = await picture.toImage(
_canvasSize.width.toInt().clamp(1, 4096),
_canvasSize.height.toInt().clamp(1, 4096),
);
// 注意:不在此处 dispose strokeImage
// _cache 和此方法持有同一 image 引用,提前 dispose 会导致
// syncStrokes/clear/dispose 时 use-after-dispose审计 ID: 8b-R01
// 单笔画 image 由缓存统一管理生命周期(移除/清除/销毁时释放)。
_layerVersion++;
}
/// 全量重建合成图(撤销/清除后使用)
Future<void> _rebuildComposite() async {
if (!_dirty || _cache.isEmpty) {
if (_cache.isEmpty) {
_compositeImage?.dispose();
_compositeImage = null;
_layerVersion++;
}
_dirty = false;
return;
}
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
canvas.saveLayer(
Rect.fromLTWH(0, 0, _canvasSize.width, _canvasSize.height),
Paint(),
);
// 按笔画顺序重新绘制所有单笔画
// 直接从 stroke 数据重绘路径,确保与增量合成结果一致
for (final entry in _cache.values) {
final stroke = entry.stroke;
final outlinePoints = pointsToOutline(
stroke.points,
stroke.brushType,
stroke.width,
isComplete: true,
);
if (outlinePoints.isEmpty) continue;
final path = buildStrokePath(outlinePoints);
final paint = createPaintForStroke(stroke);
if (stroke.brushType == BrushType.eraser) {
canvas.drawPath(path, Paint()..blendMode = BlendMode.dstOut);
} else {
canvas.drawPath(path, paint);
}
}
canvas.restore();
_compositeImage?.dispose();
final picture = recorder.endRecording();
_compositeImage = await picture.toImage(
_canvasSize.width.toInt().clamp(1, 4096),
_canvasSize.height.toInt().clamp(1, 4096),
);
_layerVersion++;
_dirty = false;
}
}