WPF 使用不安全代码快速从数组转 WriteableBitmap

本文介绍了一种使用不安全代码在WPF中快速将数组转换为WriteableBitmap的方法,对比传统的BitmapSource方法,此方法能显著降低内存占用并提高性能。
原文: WPF 使用不安全代码快速从数组转 WriteableBitmap

本文告诉大家一个快速的方法,直接把数组转 WriteableBitmap

先来说下以前的方法,以前使用的是 BitmapSource ,这个方法是大法官方提供的。

BitmapSource.Create(LogicalWidth, LogicalHeight, 96, 96, PixelFormats.Bgra32, null,
                    dest,
                    stride);

其中 dest 是一个大数组,数据大小为 ,经常在转换的时候出现内存不足异常。假如现在内存占用是 1.5G ,转换的图片大小是 2000*2000 ,于是几乎一跑就出现内存不足。

为何还有 500 M 内存却出现内存不足?因为图片转换需要的是一段大的连续内存空间。砸桌子,再说一次,图片转换需要一段【大的连续】内存空间,虽然有500M内存,但是连续的空间没有那么多。

这就是以前方法的缺点。

使用不安全代码转换是把数组直接复制到WriteableBitmap,请看使用不安全代码将 Bitmap 位图转为 WPF 的 ImageSource 以获得高性能和持续小的内存占用 - walterlv,这里讲了如何从 Bitmap 转 WriteableBitmap ,于是下面只需要把数组转 Bitmap 就可以了。

我在最后会给大家全部代码,所以现在讲原理。

如果已经拿到了数组,知道数组的存放,那么就可以直接把数组复制到 WriteableBitmap 就可以显示。数组的存放就是数组是如何放数据的,是不是还在想,上面的 dest 是一个大数组,他的计算是 为什么是*4,因为存放的数据是 A R B G 一个点需要4个int来放。那么放的顺序是什么?这就是PixelFormat指定的类型,可以使用Bgra32或者其他的格式,不过指定了格式就需要数组存放和指定一样

因为没有直接从数组转 WriteableBitmap 所以需要先把数组转 Bitmap ,可以使用的方法请看下面

                unsafe
                {
                    fixed (int* ptr = _dest)
                    {

                        try
                        {
                            using (Bitmap image = new Bitmap(LogicalWidth, LogicalHeight, LogicalWidth * 4,
                                PixelFormat.Format16bppArgb1555, new IntPtr(ptr)))
                            {
                               
                            }
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine(e);
                        }
                    }
                }

Bitmap 的数据类型可以是任意,因为只是把他的数据转换到 WriteableBitmap 所以不需要指定他的数据

获得 Bitmap 就可以把他转 WriteableBitmap ,请看下面的代码

                unsafe
                {
                    fixed (int* ptr = _dest)
                    {

                        try
                        {
                            using (Bitmap image = new Bitmap(LogicalWidth, LogicalHeight, LogicalWidth * 4,
                                PixelFormat.Format16bppArgb1555, new IntPtr(ptr)))
                            {
                                CopyFrom(source,image);
                            }
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine(e);
                        }
                    }
                }

代码的 CopyFrom 就是吕毅提供的方法。

使用这个函数更新,不需要在更新了修改 Image 的 Source 因为会自动更新,用这个方法播放 gif 的性能比找到的Magick.NET库的性能都好。

对比一下性能,这时原先的 BitmapSource 方法占用内存

这是使用不安全代码占用内存

实际跑一张 gif 图的性能

可以看到这个方法可以节省很多的内存,而且占用的 cpu 很低,因为没有很多gc

但是不要太高兴,因为不安全代码的exception是接不住的,下面请修改一下代码,让他输入错误,于是就出现异常,结果程序就关了。

所以使用这个方法还是很大的坑。

全部的代码:

           Application.Current?.Dispatcher.BeginInvoke((Action) (() =>
            {
                unsafe
                {
                    fixed (int* ptr = _dest)
                    {
                       
                        try
                        {
                            using (Bitmap image = new Bitmap(LogicalWidth, LogicalHeight, LogicalWidth * 4,
                                PixelFormat.Format16bppArgb1555, new IntPtr(ptr)))
                            {
                                CopyFrom(_source, image);
                            }
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine(e);
                        }
                    }
                }
            }));


          private void UpdateSource()
        {
            Application.Current?.Dispatcher.Invoke(() =>
            {
                 _source = new WriteableBitmap(LogicalWidth, LogicalHeight, 96, 96,
                        System.Windows.Media.PixelFormats.Bgra32, null);
            });
        }

        public static void CopyFrom(WriteableBitmap wb, Bitmap bitmap)
        {
            if (wb == null)
                throw new ArgumentNullException(nameof(wb));
            if (bitmap == null)
                throw new ArgumentNullException(nameof(bitmap));

            var ws = wb.PixelWidth;
            var hs = wb.PixelHeight;
            var wt = bitmap.Width;
            var ht = bitmap.Height;
            if (ws != wt || hs != ht)
                throw new ArgumentException("暂时只支持相同尺寸图片的复制。");

            var width = ws;
            var height = hs;
            var bytes = ws * hs * wb.Format.BitsPerPixel / 8 ;

            var rBitmapData = bitmap.LockBits(new Rectangle(0, 0, width, height),
                ImageLockMode.ReadOnly, bitmap.PixelFormat);

            wb.Lock();
            unsafe
            {
                Buffer.MemoryCopy(rBitmapData.Scan0.ToPointer(), wb.BackBuffer.ToPointer(), bytes, bytes);
            }
            wb.AddDirtyRect(new Int32Rect(0, 0, width, height));
            wb.Unlock();

            bitmap.UnlockBits(rBitmapData);
        }

我把代码给小伙伴看,他说可以直接从数组转 WriteableBitmap ,我使用他的想法,修改了程序,请看代码

              unsafe
                {
                    fixed (int* ptr = _dest)
                    {
                        _source.Lock();

                        var bytes = LogicalWidth * LogicalHeight * _source.Format.BitsPerPixel / 8;

                        Buffer.MemoryCopy(ptr,_source.BackBuffer.ToPointer(), bytes, bytes);

                        _source.AddDirtyRect(new Int32Rect(0, 0, LogicalWidth, LogicalHeight));
                        _source.Unlock();
                    }
                }

实际上微软已经提供了不安全代码的转换,请看下面代码

bitmapImage.WritePixels(new Int32Rect(0, 0, 宽度, 高度), 图片数据, stride, 0)

stride 一般就是 4*宽度 因为一个像素使用4个byte


本文会经常更新,请阅读原文: https://lindexi.gitee.io/lindexi/post/WPF-%E4%BD%BF%E7%94%A8%E4%B8%8D%E5%AE%89%E5%85%A8%E4%BB%A3%E7%A0%81%E5%BF%AB%E9%80%9F%E4%BB%8E%E6%95%B0%E7%BB%84%E8%BD%AC-WriteableBitmap.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://lindexi.gitee.io ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系

posted on 2019-01-04 10:39 NET未来之路 阅读( ...) 评论( ...) 编辑 收藏

转载于:https://www.cnblogs.com/lonelyxmas/p/10218514.html

<think>我们正在讨论WPF环境下的C#异步编程。根据引用[1]和[2],异步编程在C#中非常重要,特别是在WPF中,因为它可以保持UI线程的响应性。引用[5]展示了Task的使用,但我们需要更详细地探讨在WPF中如何使用async/await模式。 重点: 1. WPF的UI线程是单线程模型,所有UI操作必须在UI线程上执行。 2. 长时间运行的操作(如I/O、网络请求、复杂计算)应该放在后台线程,以避免阻塞UI线程。 3. 使用async/await可以简化异步编程,避免回调地狱。 步骤: 1. 定义异步方法:使用async关键字标记方法,并返回Task或Task<T>。 2. 在异步方法中,对于I/O操作,使用await关键字等待返回(例如:HttpClient.GetAsync, FileStream.ReadAsync等)。 3. 在WPF事件处理中(如按钮点击事件),也可以使用async void,但要小心异常处理(因为async void无法捕获异常)。 4. 在后台任务完成后需要更新UI时,必须回到UI线程。在WPF中,我们可以使用Dispatcher或者利用WPF的数据绑定(通过INotifyPropertyChanged自动更新)来安全更新UI。 注意:引用[2]提到了Dispatcher,它是WPF中进行线程间通信的关键。 示例场景:在WPF中点击按钮,从网络下载一张图片并显示在Image控件中,同时阻塞UI。 步骤详解: 1. 在按钮点击事件中,我们调用一个异步方法。 2. 在异步方法中,使用HttpClient异步下载图片。 3. 将下载的图片数据换为位图,并设置到Image控件的Source属性(必须在UI线程上操作)。 代码示例: ```csharp private async void DownloadButton_Click(object sender, RoutedEventArgs e) { // 显示加载状态 StatusText.Text = "下载中..."; try { // 使用HttpClient异步下载图片 string imageUrl = "https://example.com/image.jpg"; byte[] imageData = await DownloadImageAsync(imageUrl); // 将字节数组换为位图 BitmapImage bitmap = await ConvertToBitmapAsync(imageData); // 更新UI:设置Image控件的Source(注意:这里已经在UI线程) MyImage.Source = bitmap; StatusText.Text = "下载完成"; } catch (Exception ex) { StatusText.Text = $"下载失败: {ex.Message}"; } } private async Task<byte[]> DownloadImageAsync(string url) { using (HttpClient client = new HttpClient()) { // 异步获取响应 HttpResponseMessage response = await client.GetAsync(url); response.EnsureSuccessStatusCode(); // 异步读取内容为字节数组 return await response.Content.ReadAsByteArrayAsync(); } } private async Task<BitmapImage> ConvertToBitmapAsync(byte[] imageData) { // 创建一个位图对象 BitmapImage bitmap = new BitmapImage(); using (MemoryStream stream = new MemoryStream(imageData)) { // 使用异步初始化位图(注意:BitmapImage的BeginInit/EndInit是异步的,所以我们将加载操作放入Task.Run) // 但是,由于我们已经将数据读入内存,所以可以直接在内存流上操作,需要异步。 // 过,如果图片很大,解码可能会耗时,所以我们使用Task.Run在后台线程解码。 await Task.Run(() => { // 在后台线程解码 bitmap.BeginInit(); bitmap.CacheOption = BitmapCacheOption.OnLoad; bitmap.StreamSource = stream; bitmap.EndInit(); bitmap.Freeze(); // 如果需要跨线程使用,可以冻结对象 }); } return bitmap; } ``` 说明: 1. 在按钮点击事件中,我们使用async void,因为事件处理程序要求返回void。但是,我们在内部使用await,所以会阻塞UI线程。 2. 在DownloadImageAsync方法中,我们使用HttpClient的异步方法进行网络请求。 3. 在ConvertToBitmapAsync方法中,我们将耗时的位图解码操作放在Task.Run中,避免阻塞UI线程。注意:BitmapImage的创建和初始化必须在UI线程?实际上,我们是在后台线程创建和初始化的,但是WPF的UI元素(包括BitmapImage)通常只能在创建它们的线程上访问。然而,在这个例子中,我们是在后台线程创建的BitmapImage,然后将其设置到Image控件(UI线程)上,这会导致跨线程访问异常吗? 实际上,我们通过await Task.Run回到UI线程(因为await后面的代码会回到原始上下文,即UI线程)?对,这里有一个常见的误解: 在WPF中,默认情况下,await后面的代码会在原始同步上下文(即UI线程)上执行。但是,在ConvertToBitmapAsync方法中,我们使用await Task.Run,那么Task.Run中的代码是在线程池线程上执行的,而Task.Run之后的代码(即设置位图到Image控件)是在await完成后,在UI线程上执行的。但是,注意:在ConvertToBitmapAsync方法中,我们创建BitmapImage的操作是在Task.Run中(后台线程)进行的,而位图对象是在后台线程创建的。然后,我们将这个后台线程创建的位图对象返回,并在按钮事件中(UI线程)设置给Image控件。这是允许的,因为BitmapImage是派生自DispatcherObject,它只能被创建它的线程访问。 所以,我们需要调整:能在后台线程创建BitmapImage,而应该在后台线程解码数据,然后在UI线程创建BitmapImage。 修正ConvertToBitmapAsync方法: ```csharp private async Task<BitmapImage> ConvertToBitmapAsync(byte[] imageData) { // 我们将在后台线程解码数据,但位图必须在UI线程创建 // 所以,我们先在后台线程将字节数组解码为WriteableBitmap(或任何方式),然后回到UI线程创建BitmapImage?或者直接在UI线程创建? // 另一种做法:使用MemoryStream,然后在UI线程初始化BitmapImage,但解码过程可能阻塞,所以我们可以将整个初始化过程放在后台线程,然后冻结对象,这样冻结后的对象可以在任何线程使用。 return await Task.Run(() => { using (MemoryStream stream = new MemoryStream(imageData)) { BitmapImage bitmap = new BitmapImage(); bitmap.BeginInit(); bitmap.CacheOption = BitmapCacheOption.OnLoad; // 加载后关闭流 bitmap.StreamSource = stream; bitmap.EndInit(); bitmap.Freeze(); // 冻结对象,使其跨线程可用 return bitmap; } }); } ``` 这样,我们在后台线程创建了BitmapImage并冻结它,然后返回给UI线程。冻结的对象可以在任何线程访问,所以设置到Image控件是安全的。 但是,注意:冻结对象后就能再修改。所以,如果后续需要修改图片,则能冻结。 另一种方法:在后台线程创建BitmapImage,而是将解码后的数据通过某种方式传递给UI线程,然后在UI线程创建BitmapImage。但这样需要两次解码(或者传递原始字节数据,然后在UI线程解码),效率高。 因此,使用冻结对象是WPF中跨线程传递图片的常用方法。 此外,引用[4]提到了Image控件的IsAsync属性,它允许在后台线程加载图像源,但这里我们手动控制。 另一个注意点:异常处理。我们在按钮事件中使用了try-catch来捕获异步操作中的异常。 此外,引用[5]中提到了Task的连续任务(ContinueWith),但在async/await模式下,我们通常使用await来顺序执行,代码更清晰。 总结WPF异步编程要点: 1. 使用async/await模式编写异步方法。 2. 在事件处理中可以使用async void,但要确保异常被处理(通过try-catch)。 3. 需要更新UI时,确保在UI线程上操作。可以通过以下方式: a. 使用Dispatcher.Invoke(当在UI线程时) b. 利用await的上下文自动切换(默认情况下,在UI线程await后,后续代码在UI线程执行) 4. 对于耗时操作(特别是计算密集型),使用Task.Run在后台线程执行。 5. 对于非UI线程创建的可视化对象(如BitmapImage),使用Freeze()使其跨线程可用。 引用[2]还提到了BackgroundWorker和TPL(Task Parallel Library)。在.NET 4.5之后,async/await是首选。 相关问题: 1. 在WPF中如何取消异步操作? 2. 如何处理多个异步任务的并发执行? 3. 如何避免异步操作中的内存泄漏? 4. WPF数据绑定如何与异步操作结合? 5. 在WPF中,如何实现异步加载大量数据? 以上内容参考了引用[1][2][4][5]。</think>### WPF中C#异步方法使用详解 在WPF中正确使用异步方法可显著提升UI响应性和程序性能,同时避免界面冻结。以下是核心实践指南: --- #### 1. **基础异步模式(async/await)** ```csharp private async void LoadDataButton_Click(object sender, RoutedEventArgs e) { try { // 显示加载状态(UI线程) StatusText.Text = "加载中..."; // 异步操作(阻塞UI线程) var data = await GetRemoteDataAsync(); // 自动返回UI线程更新界面 DataGrid.ItemsSource = data; StatusText.Text = "加载完成"; } catch (Exception ex) { StatusText.Text = $"错误: {ex.Message}"; } } private async Task<List<DataItem>> GetRemoteDataAsync() { await Task.Delay(2000); // 模拟网络延迟 return new List<DataItem> { ... }; } ``` **关键点**: - `async void`仅用于事件处理器 - `await`自动切换回UI线程执行后续代码 - 异常需在UI线程捕获[^1][^2] --- #### 2. **后台任务与UI更新** 使用`Task.Run`处理CPU密集型操作: ```csharp private async void ProcessImage_Click(object sender, RoutedEventArgs e) { var sourceImage = OriginalImage.Source as BitmapImage; // 在后台线程处理图像 var processedImage = await Task.Run(() => { return ImageProcessor.Transform(sourceImage); // 耗时操作 }); // 自动返回UI线程更新控件 ProcessedImage.Source = processedImage; } ``` **注意**: - 避免在`Task.Run`中直接操作UI控件 - 复杂对象需调用`Freeze()`跨线程使用[^4] --- #### 3. **取消异步操作** ```csharp private CancellationTokenSource _cts; private async void StartTask_Click(object sender, RoutedEventArgs e) { _cts = new CancellationTokenSource(); try { await LongRunningOperationAsync(_cts.Token); } catch (OperationCanceledException) { StatusText.Text = "操作已取消"; } } private void CancelTask_Click(object sender, RoutedEventArgs e) { _cts?.Cancel(); // 触发取消 } ``` --- #### 4. **连续任务(Task Continuations)** ```csharp private async void MultiStepProcess_Click(object sender, RoutedEventArgs e) { var step1 = FetchDataAsync(); var step2 = step1.ContinueWith(t => ProcessData(t.Result), TaskScheduler.FromCurrentSynchronizationContext()); // 指定UI线程 await step2; ShowResult(step2.Result); } ``` --- #### 5. **异步数据绑定** 通过`INotifyPropertyChanged`实现: ```csharp public class ViewModel : INotifyPropertyChanged { private List<Item> _items; public List<Item> Items { get => _items; set { _items = value; OnPropertyChanged(); } } public async Task LoadItemsAsync() { Items = await DataService.GetItemsAsync(); } } // XAML中 <ListBox ItemsSource="{Binding Items}"/> ``` --- #### 6. **Dispatcher的替代方案** 优先使用`await`自动同步上下文,特殊场景用Dispatcher: ```csharp await Task.Run(() => { var result = Compute(); // 需要手动调度到UI线程 Application.Current.Dispatcher.Invoke(() => { ResultLabel.Content = result; }); }); ``` --- #### 常见问题解决 | 问题类型 | 解决方案 | |---------|---------| | **UI冻结** | 检查`await`是否遗漏,确保耗时操作在`Task.Run`中 | | **跨线程异常** | 使用`Dispatcher.Invoke`或`Freeze()`冻结对象[^4] | | **内存泄漏** | 取消注册事件,及时释放`CancellationTokenSource` | | **异步初始化** | 在`Window.Loaded`事件中启动异步加载 | > **最佳实践**: > - 异步方法命名以`Async`结尾(如`LoadDataAsync()`) > - 避免`async void`(除事件处理器外) > - 使用`ConfigureAwait(false)`在非UI组件中提升性能[^1] > - 耗时超过50ms的操作应异步化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值