.net框架wpf中加载页面_.NET框架WPF中加载高质量大图慢的性能优化

时间:2020-03-15  来源:WPF  阅读:

最近的项目中,遇到一个关于WPF中同时加载多张图片时,内存占用非常高的问题。

问题背景:

在一个ListView中同时加载多张图片,注意:我们需要加载的图片分辨率非常高。

代码:

XAML:


    
        
        
    
    
    
    
    
        
            
                
            
        
        
            
                
                    
                        
                    
                
            
        
        
            
                ling" VirtualizingPanel.IsVirtualizing="True"/>
            
        
    


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 models = new List();
        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年,只是我一直用错了方法,才觉得很慢。

.net框架wpf中加载页面_.NET框架WPF中加载高质量大图慢的性能优化

http://m.bbyears.com/asp/87337.html

推荐访问:
相关阅读 猜你喜欢
本类排行 本类最新