最近的项目中,遇到一个关于WPF中同时加载多张图片时,内存占用非常高的问题。
问题背景:
在一个ListView中同时加载多张图片,注意:我们需要加载的图片分辨率非常高。
代码:
XAML:
<Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Button Content="Load" Width="100" Height="35" Margin="0,10" Click="Button_Click"/> <ListView Grid.Row="1" x:Name="lvImages"> <ListView.ItemTemplate> <DataTemplate> <Image Source="{Binding ImageSource}" MaxWidth="800"/> </DataTemplate> </ListView.ItemTemplate> <ListView.Template> <ControlTemplate> <Grid> <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden"> <ItemsPresenter /> </ScrollViewer> </Grid> </ControlTemplate> </ListView.Template> <ListView.ItemsPanel> <ItemsPanelTemplate> <StackPanel IsItemsHost="True" VirtualizingPanel.VirtualizationMode="Recycling" VirtualizingPanel.IsVirtualizing="True"/> </ItemsPanelTemplate> </ListView.ItemsPanel> </ListView> </Grid>
C#:
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { lvImages.Items.Clear(); // Image folder location: D:\Pics string[] files = System.IO.Directory.GetFiles(@"D:\Pics"); List<ImageSourceModel> models = new List<ImageSourceModel>(); foreach(var path in files) { BitmapImage image = new BitmapImage(); image.BeginInit(); image.UriSource = new System.Uri(path); image.EndInit(); image.Freeze(); models.Add(new ImageSourceModel() { ImageSource = image }); } lvImages.ItemsSource = models; } } public class ImageSourceModel { public ImageSource ImageSource { get; set; } }
内存占用情况(此时只加载了20张图片,内存占用>1G):
优化方案:
1. 初始加载时,只加载部分图片并显示。当ScrollViewer滚动到底部时,再加载一部分。关于这个方案,可以参考 WPF MVVM模式下实现ListView下拉显示更多内容;
但是这并不能解决最终内存占用过高的情况。
2. 给图片设置DecodePixelWidth属性,
BitmapImage image = new BitmapImage();
image.BeginInit();
image.UriSource = new System.Uri(path);
image.DecodePixelWidth = 800;
image.EndInit();
image.Freeze();
models.Add(new ImageSourceModel() { ImageSource = image });
此时的内存占用如图
内存降低的非常显著,此时同样多的图片内存占用只有40M左右。
最终我们可以把优化方案1和优化方案2结合起来。这样在加载多张图片时不会出现卡顿的现象。另外从用户体验的角度我们可以在图片显示出来前,先用一个Loading的动画效果过渡下。
wpf大图片处理速度优化:指针操作,并行操作,几十倍优化
我一直用GDI+做Winform 的基于指针的图片处理,这次下决心全部移到wpf上(主要是显示布局很方便)
采用的图片是
2512*3307 的大图 830万像素
类库基于WritableBitmapEx 的wpf版本
函数是我自己写的扩展方法,只是利用了 writableBitmapEx提供的环境 ,我懒得从头到尾自己写了
1.标准int32数组遍历计算 release
0.28s
unsafe public static void TestGray1(this WriteableBitmap bmp) { using (var context = bmp.GetBitmapContext()) { int height = context.Height; int width = context.Width; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int pos=y * context.Width + x; var c = context.Pixels[pos]; var r = (byte)(c >> 16); var g = (byte)(c >> 8); var b = (byte)(c); var gray = ((r * 38 + g * 75 + b * 15) >> 7); var color=(255 << 24) | (gray << 16) | (gray << 8) | gray; context.Pixels[pos]=color; } } } }
2.标准int32指针遍历计算 release
0.04s
unsafe public static void TestGray2(this WriteableBitmap bmp) { using (var context = bmp.GetBitmapContext()) { var ptr = context.Pixels; int height = context.Height; int width = context.Width; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { var c = *ptr; var r = (byte)(c >> 16) ; var g = (byte)(c >> 8) ; var b = (byte)(c) ; var gray = ((r * 38 + g * 75 + b * 15) >> 7); var color = (255 << 24) | (gray << 16) | (gray << 8) | gray; *ptr = color; ptr++; } } } }
3.colorstruct指针 遍历计算
0.02 s
应该是已经到极限速度了[除了后面的并行方式],我已经想不出还有什么方法可以提高处理速度
而且这种方式是最直观的,最容易理解的处理方式,也便于以后维护
[StructLayout(LayoutKind.Sequential)] public struct PixelColor { public byte Blue; public byte Green; public byte Red; public byte Alpha; } unsafe public static void TestGray3(this WriteableBitmap bmp) { using (var context = bmp.GetBitmapContext()) { var ptr = (PixelColor*)context.Pixels; int height = context.Height; int width = context.Width; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { var c = *ptr; var gray = ((c.Red * 38 + c.Green * 75 + c.Blue * 15) >> 7); (*ptr).Green=(*ptr).Red=(*ptr).Blue = (byte)gray; ptr++; } } } }
4.作为对比,我又测试了一下 GDI+的 指针处理图片的速度
0.06s
public static unsafe Bitmap ToGray(Bitmap img) { var rect = new System.Drawing.Rectangle(0, 0, img.Width, img.Height); var data = img.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); var ptr = (ColorType*)data.Scan0.ToPointer(); var bytes = new Int32[img.Width * img.Height]; var height = img.Height; var width = img.Width; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { var color = *ptr; var gray = ((color.R * 38 + color.G * 75 + color.B * 15) >> 7); (*ptr).R = (*ptr).G = (*ptr).B = (byte)gray; ptr++; } } img.UnlockBits(data); return img; }
5.重头戏来了。我一直对Parallel.For 很迷惑,为什么他的消耗时间是普通for的好几倍。今天仔细研究了一下,发现原来是用错了
0.01秒 release
笔记本i5cpu,如果台式机的I7会更加强悍,速度会成半成半降低。
主要是利用了微软的任务并行库的循环并行化的方法。
注意:默认的并行循环对于函数体很小的情况是很慢的,这种情况必须用Partitioner 创建循环体,这在MSDN有介绍,是关键之中的关键
unsafe public static void TestGray5(this WriteableBitmap bmp) { using (var context = bmp.GetBitmapContext()) { int height = context.Height; int width = context.Width; Parallel.ForEach(Partitioner.Create(0, height), (h) => { var ptr = (PixelColor*)context.Pixels; ptr += h.Item1 * width; for (int y = h.Item1; y < h.Item2; y++) { for (int x = 0; x < width; x++) { var c = *ptr; var gray = ((c.Red * 38 + c.Green * 75 + c.Blue * 15) >> 7); (*ptr).Green = (*ptr).Red = (*ptr).Blue = (byte)gray; ptr++; } } }); } }
感想
1.绝对不要在循环体内使用属性或函数,很有可能会降低数倍计算速度。
因为属性本质上是个函数,而在循环体内最好不要再调用函数,如果确实需要用内联代码的方式,c#没有inline,那么copy代码吧,反正为了速度。
2. 用指针移位操作 似乎比 直接数组访问要快10倍啊
我感觉要么是cache命中的原因,要么是 数组本身存取被属性封装了。相当于又调用了函数。
3.TPL 任务并行库果真好用,看来微软早已考虑过大量数据并行的循环优化问题09年,只是我一直用错了方法,才觉得很慢。
以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索指针
, 内存
, 优化
, new
, 性能优化
用户体验
高质量大图、ios点击查看大图框架、wpf ui 框架、wpf 开源ui框架、wpf mvvm框架,以便于您获取更多的相关知识。