You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

后台线程更新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;
    }
}

问题原因及修复方案

核心原因

  1. 后台任务堆积与UI线程过载
    36个元素在窗口 resize 时会同时触发OnRenderSizeChanged,每个元素都会启动一个后台Task,瞬间产生36个任务争抢SemaphoreSlim锁,导致任务排队等待。任务结束后又会批量发送InvalidateVisual请求,挤占UI线程资源,部分元素的渲染被延迟,表现为闪烁。

  2. BackBuffer引用失效
    ResizeBitmap更新_pBackBuffer时,若有后台任务正处于锁等待状态,这些任务恢复后会写入已被回收的旧BackBuffer,导致绘制异常。

  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 08:52:04