从MemoryStream生成ImageSource的最佳实践

从MemoryStream生成ImageSource的最佳实践

好久没有写博客了,今天刚好清明节假期,闲来无事,把最近项目中优化的一个点总结一下。

需求

最近做的项目,需要增加表情功能,需要加载近4000张表情图,供用户使用。项目使用的是WPF框架和MVVM设计模式。
浏览图片功能使用的是ListBox控件,每个ListBoxItem使用Image重写的ControlTemplate。
Image的Source属性是ImageSource类型,通常在图片少的时候通过绑定图片的路径来加载并显示图片。但现在有4000张图片,一张张的通过这种方式来显示,效率是一个很大的问题,在浏览的时候,用户很容感受到卡顿。
那么在实现功能的同时,怎样做到加载和显示流畅不卡顿?

思路

一个通常的方案就是先读取图片到内存中,然后把内存中的数据转换成ImageSource类型,然后把转换后的数据再直接与Image的Source属性进行绑定。通过实践,找到了两个方案,并对这两种方案做了对比和进一步的优化。

实现

我们先通过后台任务在软件启动的时候,把图片加载到内存中,存储为MemoryStream类型,再从MemoryStream生成ImageSource类型的对象。从MemoryStream生成ImageSource类型的对象,有两种方式:

  1. 我们知道BitmapImage是ImageSource类型的子类,可以直接MemoryStream转换为BitmapImage对象,然后把BitmapImage对象再绑定到Image的Source属性。
public static BitmapImage ToBitmapImage(MemoryStream stream)
{
    try
    {
        var bitmapImage = new BitmapImage();
        bitmapImage.BeginInit();
        bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
        bitmapImage.StreamSource = stream;
        bitmapImage.EndInit();

    	return bitmapImage;
    }catch(Exception ex)
    {
        return null;
    }
}

这种方法可行,效率比直接绑定路径的方式要好,一旦全部转换成BitmapImage对象,后面在用户浏览图片的时候,一点都不卡顿,用户体验提高了很多。但是,还有一个问题,4000张图片在从MemoryStream转换到BitmapImage对象,大概用时要2秒多。好像还可以接受。但是生成BitmapImage对象只能在UI线程中进行,这2秒多的转化过程也势必会造成UI界面的卡顿。如果在其他线程中生成BitmapImage对象后再去与Image的Source属性绑定的话,程序就会报异常。

  1. 我们需要再对其进行优化,通过搜索我们还找到了另外一种方法可以从MemoryStream转换到BitmapSource。是基于所提供的非托管位图和调色板信息的指针,返回一个托管的
    BitmapSource。BitmapSource是ImageSource的子类。可以直接绑定到Image的Source属性。
public static ImageSource ToImageSource(MemoryStream stream)
{
    var bitmap = new Bitmap(stream);
    return Imaging.CreateBitmapSourceFromHBitmap(bitmap.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
}

同样,BitmapSource也只能在UI线程中创建,否则也会报异常。由于经过测试,这种方式转换4000张图片的效率大大提升了。从上个方法的2秒多降低到了大概1秒钟,效率提升了100%。这样在软件的启动过程中,用户也只是感受到一点点的卡顿,整体还可以接受。
但是,我们还有没有可以优化的地方?让界面一点都不卡顿。由于ImageSource对象的创建只能在主线程中进行,这块的代码无法挪到其他线程中进行。通过上面的代码,我们发现从MemorySteam转换到ImageSource经过了三个过程:
1)从MemorySteam生成Bitmap对象;
2)通过Bitmap对象调用GetHbitmap()方法获取非托管的指针;
3)从非托管的指针通过调用Imaging.CreateBitmapSourceFromHBitmap()函数转换到所需的ImageSource类型的对象。
那我们是不是可以把上面的三个步骤拆开?把1)和2)放到其他线程中,在内存里保存转换所需要的指针即可。把3)仍然放在UI线程中。
经过这样的改造,我们测试,在UI线程中转换到所需的ImageSource对象使用的时间大约90ms左右。这样几乎不影响在启动过程中UI的流畅性。

小结

从MemoryStream生成ImageSource的最佳实践经过不断的尝试和优化,最终的方案还是比较令人满意的。把优化的过程记录下来,防止忘记,也方便其他小伙伴。如有更好的方案,欢迎在留言区讨论。谢谢!
附最后的成品效果:
从MemoryStream生成ImageSource的最佳实践

参考

  • https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.media.imaging.bitmapimage?view=net-5.0
  • https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.interop.imaging.createbitmapsourcefromhbitmap?view=net-5.0
上一篇:C# 通过MemoryStream,BinaryWriter,BinaryReader读写字节数据


下一篇:策略模式(Strategy Pattern)