后台线程更新WriteableBitmap引发WPF元素闪烁问题排查
我正在开发一个自定义WPF Framework元素,该元素会写入WriteableBitmap,然后在元素的OnRender()方法中显示该位图。由于写入WriteableBitmap的操作(涉及算法计算)较慢,且需要展示36个此类元素,我希望在后台线程中完成WriteableBitmap的更新工作。
我设计了如下实现方案,性能得到大幅提升。但问题是,当仅创建8个或更少元素时运行正常,而创建36个元素时,调整窗口大小后,第8个之后的所有元素都会出现闪烁现象。请问可能是什么原因导致的?
Renderer元素代码
public class Renderer : FrameworkElement { private WriteableBitmap? _bitmap, _previousBitmap; private long _pBackBuffer = 0; private int _backBufferStride = 0, _backBufferWidth = 0, _backBufferHeight = 0; private SemaphoreSlim _semaphore = new(1, 1); private void ResizeBitmap() { int width = (int)ActualWidth; int height = (int)ActualHeight; if (width <= 0 || height <= 0) return; if (_bitmap == null || width != _bitmap.PixelWidth || height != _bitmap.PixelHeight) { _previousBitmap = _bitmap; _bitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Pbgra32, null); _pBackBuffer = _bitmap.BackBuffer; _backBufferStride = _bitmap.BackBufferStride; _backBufferWidth = width; _backBufferHeight = height; // 调试用,填充蓝色 byte[] data = new byte[_bitmap.BackBufferStride * _bitmap.PixelHeight]; for (int i = 0; i < _bitmap.PixelHeight; ++i) { for (int j = 0; j < _bitmap.PixelWidth; ++j) { data[i * _bitmap.BackBufferStride + j * sizeof(int) + 0] = 255; data[i * _bitmap.BackBufferStride + j * sizeof(int) + 1] = 0; data[i * _bitmap.BackBufferStride + j * sizeof(int) + 2] = 0; data[i * _bitmap.BackBufferStride + j * sizeof(int) + 3] = 255; } } _bitmap.WritePixels(new Int32Rect(0, 0, _bitmap.PixelWidth, _bitmap.PixelHeight), data, _bitmap.BackBufferStride, 0); } } public void InvalidateRender() => WriteToBitmapInWorkerThread(); protected override void OnRender(DrawingContext drawingContext) { base.OnRender(drawingContext); if (_bitmap == null) return; _semaphore.Wait(); _bitmap.Lock(); _bitmap.AddDirtyRect(new Int32Rect(0, 0, (int)_bitmap.Width, (int)_bitmap.Height)); _bitmap.Unlock(); drawingContext.DrawImage(_bitmap, new Rect(0, 0, ActualWidth, ActualHeight)); _semaphore.Release(); } protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) { base.OnRenderSizeChanged(sizeInfo); _semaphore.Wait(); ResizeBitmap(); _semaphore.Release(); WriteToBitmapInWorkerThread(); } private void WriteToBitmapInWorkerThread() { // 模拟耗时的位图写入操作,放在后台线程执行 Task.Run(() => { _semaphore.Wait(); unsafe { int* pPointer = (int*)_pBackBuffer; int stride = _backBufferStride / 4; // 模拟操作耗时 Thread.Sleep(10); // 演示用,绘制渐变 for (int i = 0; i < _backBufferHeight; ++i) { byte x = (byte)(255d / _backBufferHeight * i); for (int j = 0; j < _backBufferWidth; ++j) { pPointer[i * stride + j] = 255 << 24 | x << 16 | x << 8 | 255; } } } _semaphore.Release(); Dispatcher.BeginInvoke(DispatcherPriority.Render, () => InvalidateVisual()); }); } }
MainWindow代码
XAML部分
<Grid Background="Orange"> <ItemsControl ItemsSource="{Binding Items}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <UniformGrid /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <Border Margin="2" Padding="2" BorderThickness="1" BorderBrush="Red"> <local:Renderer /> </Border> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Grid>
C#代码
public partial class MainWindow : Window { public List<string> Items { get; } = Enumerable.Range(0, 36).Select(e => e.ToString()).ToList(); public MainWindow() { InitializeComponent(); DataContext = this; } }
核心原因
后台任务堆积与UI线程过载
36个元素在窗口 resize 时会同时触发OnRenderSizeChanged,每个元素都会启动一个后台Task,瞬间产生36个任务争抢SemaphoreSlim锁,导致任务排队等待。任务结束后又会批量发送InvalidateVisual请求,挤占UI线程资源,部分元素的渲染被延迟,表现为闪烁。BackBuffer引用失效
ResizeBitmap更新_pBackBuffer时,若有后台任务正处于锁等待状态,这些任务恢复后会写入已被回收的旧BackBuffer,导致绘制异常。OnRender逻辑错误
OnRender中执行Lock/AddDirtyRect/Unlock属于多余操作,DirtyRect应该在后台写入完成后标记,而非渲染阶段。频繁在渲染时修改DirtyRect会干扰WPF的渲染调度。
修复方案
1. 避免后台任务堆积
改用异步锁确保每个Renderer同一时间只有一个后台任务执行,避免任务排队:
private readonly AsyncLock _asyncLock = new AsyncLock(); // 可使用Nito.AsyncEx库,或自行实现简易版本 private async void WriteToBitmapInWorkerThread() { using (await _asyncLock.LockAsync()) { if (_bitmap == null) return; var currentBitmap = _bitmap; int width = currentBitmap.PixelWidth; int height = currentBitmap.PixelHeight; int stride = currentBitmap.BackBufferStride; await Task.Delay(10); // 模拟耗时操作 currentBitmap.Lock(); unsafe { int* pPointer = (int*)currentBitmap.BackBuffer; int pixelStride = stride / 4; for (int i = 0; i < height; ++i) { byte x = (byte)(255d / height * i); for (int j = 0; j < width; ++j) { pPointer[i * pixelStride + j] = 255 << 24 | x << 16 | x << 8 | 255; } } } currentBitmap.AddDirtyRect(new Int32Rect(0, 0, width, height)); currentBitmap.Unlock(); Dispatcher.Invoke(() => InvalidateVisual()); } }
2. 确保BackBuffer引用有效
移除_pBackBuffer等缓存变量,直接在后台任务中使用当前_bitmap的属性,避免引用旧的已回收内存。
3. 修正OnRender逻辑
简化OnRender,直接绘制当前位图即可:
protected override void OnRender(DrawingContext drawingContext) { base.OnRender(drawingContext); if (_bitmap == null) return; drawingContext.DrawImage(_bitmap, new Rect(0, 0, ActualWidth, ActualHeight)); }
4. 优化ResizeBitmap内存管理
创建新位图后释放旧位图资源,避免内存泄漏:
private void ResizeBitmap() { int width = (int)ActualWidth; int height = (int)ActualHeight; if (width <= 0 || height <= 0) return; if (_bitmap == null || width != _bitmap.PixelWidth || height != _bitmap.PixelHeight) { _previousBitmap?.Dispose(); // 释放旧位图资源 _bitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Pbgra32, null); // 初始化蓝色背景 byte[] data = new byte[_bitmap.BackBufferStride * _bitmap.PixelHeight]; for (int i = 0; i < _bitmap.PixelHeight; ++i) { for (int j = 0; j < _bitmap.PixelWidth; ++j) { int index = i * _bitmap.BackBufferStride + j * sizeof(int); data[index] = 255; data[index + 1] = 0; data[index + 2] = 0; data[index + 3] = 255; } } _bitmap.WritePixels(new Int32Rect(0, 0, width, height), data, _bitmap.BackBufferStride, 0); } }
内容的提问来源于stack exchange,提问作者Vastar

