为什么<Image Source='...'>很慢,我该怎么办?

7

考虑以下示例XAML文件,它显示了Facebook的前1000个人,从第4个人markz开始。请注意,这只是一个示例。任何具有1000个元素的窗口,无论如何构造,都是一个很好的演示。

<Window x:Class="SO.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:clr="clr-namespace:System;assembly=mscorlib"
        Title="MainWindow" Height="350" Width="525">
    <ListBox ItemsSource="{Binding}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Image Source="{Binding}" />
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Window>

而代码背后:

public partial class MainWindow : Window
{
    public MainWindow() {
        InitializeComponent();
        string[] urls = new string[1000];
        for (int i = 0; i < 1000; ++i) {
            urls[i] = "http://graph.facebook.com/" + i + "/picture";
        }
        this.DataContext = urls;
    }
}

在一台配置合理且网络速度很快的电脑上,该程序运行极慢。尝试使用滚动条滚动到中间位置需要花费30秒钟的时间。按下“Home”和“End”键也需要相当长的时间。
这不是首次获取图片缓存的问题。来回切换查看已经呈现的图片会稍微快一些,但通常仍然很慢。似乎没有任何东西被存储在缓存中,关闭应用程序并重新启动后,一切又变得很慢。
等效的HTML代码执行非常快。第一次会有些慢,但之后一切都非常快。
发生了什么?列表元素是否使用任何缓存?列表是否预先获取当前未显示的图像?有没有办法告诉它去做?我的唯一解决方案就是自己管理位图对象,以及缓存和预取逻辑吗?如果是的话,有哪些之前的工作我可以整合?
编辑(总结):
1.关闭虚拟化将会给出最佳结果,整个列表框在窗口加载时渲染,不会重新计算任何图像。
2.Phil的代码非常好用,特别是来回切换时可以提高性能。
3.没有任何额外的代码,WPF不会在调用之间缓存图像。WinINET缓存未被使用。尽管请求在HTTP头中带有缓存指令,但WPF不会对其进行任何操作。
2个回答

6
默认情况下,ListBoxes使用虚拟项,因此如果您向下滚动,将会实时创建这些项。首先需要下载图像,然后解码。如果您已经滚动过所有的图像,它们可能已经被缓存,但是ListBox仍然会重新创建Image控件,因此每次都需要再次解码这些图像。
要关闭虚拟化,请在ListBox上设置VirtualizingStackPanel.IsVirtualizing附加属性false,那么所有内容将立即加载,或者您可以将VirtualizationMode更改为Recycling,然后Images(和包含的ListBoxItems)将不会在创建后被丢弃。

3
另一种方法是添加自己的图像缓存,这样图像只需下载一次即可。
使用我的示例,您将把它放在您的构造函数中。
this.DataContext = new ViewModel();

以下类将存储URL,当首次访问Image属性时下载图像。
public class CachingImage
{
    private readonly Uri _uri;
    public CachingImage(string uriString)
    {
        _uri = new Uri(uriString, UriKind.RelativeOrAbsolute);
    }

    private BitmapImage _image;

    public ImageSource Image
    {
        get
        {
            if (_image == null)
            {
                _image = new BitmapImage(_uri);
                _image.DownloadCompleted += (sender, args) => ((BitmapImage)sender).Freeze();
            }

            return _image;
        }
    }
}

这是视图模型

public class ViewModel
{
    public ViewModel()
    {
        Images = Enumerable.Range(1, 1000).Select(i => new CachingImage("http://graph.facebook.com/" + i + "/picture"));
    }

    public IEnumerable<CachingImage> Images { get; private set; }
    ...

当然,您需要稍微更改您的XAML。
<ListBox ItemsSource="{Binding}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Image Source="{Binding Image}" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接