Windows Phone 8.1 - 使用gridview上的图片库图像进行数据虚拟化

时间:2015-01-22 11:32:05

标签: c# xaml windows-phone windows-phone-8.1

我正在构建一个Windows Phone 8.1应用程序,它要求我在GridView中显示图片库中的所有图像。我已经构建了一个名为VirtualList的类,它是一个支持IncrementalLoading的列表,我已经将图片库中的所有图像添加到该列表中。当图像数量减少(少于80张照片)时,一切正常,但当有超过80张照片时,应用程序因OutOfMemoryException而关闭。 我想现在没有显示的项目不会保存在内存中,或者是它们? 为了我的目的,我应该继续使用增量加载,还是应该切换到随机访问数据虚拟化? 如果我应该切换到随机访问数据虚拟化,你能提供一个关于如何实现它的例子吗?

我的代码如下:

VirtualList.cs

class VirtualList : List<Windows.UI.Xaml.Media.ImageSource>, ISupportIncrementalLoading
{

    private IReadOnlyList<StorageFile> photos;

    public VirtualList(IReadOnlyList<StorageFile> files) : base()
    {
        photos = files;
    }

    public bool HasMoreItems
    {
        get 
        {
            return this.Count < photos.Count;
        }
    }

    public Windows.Foundation.IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
    {
        return LoadMoreItemsAwaitable(count).AsAsyncOperation<LoadMoreItemsResult>();
    }

    private async Task<LoadMoreItemsResult> LoadMoreItemsAwaitable(uint count)
    {
        for (int i = Count; i < Count + count; i++)
        {
            using (var fileStream = await photos[i].OpenAsync(FileAccessMode.Read))
            {
                BitmapImage bitmapImage = new BitmapImage();
                await bitmapImage.SetSourceAsync(fileStream);
                this.Add(bitmapImage);
            }
        }

        return new LoadMoreItemsResult { Count = count };
    }
}

XAML代码(MainPage.xaml):

<GridView x:Name="photosGrid" Height="392" Width="400" ItemsSource="{Binding}" Margin="0,0,-0.333,0" SelectionMode="Multiple" Background="Black">
                    <GridView.ItemTemplate>
                        <DataTemplate>   
                            <Image Width="90" Height="90" Margin="5" Source="{Binding}" Stretch="UniformToFill"/>
                        </DataTemplate>
                    </GridView.ItemTemplate>

                </GridView>

MainPage.xaml.cs代码

//This code is inside OnNavigatedTo method
var files = await KnownFolders.CameraRoll.GetFilesAsync();
VirtualList imageList = new VirtualList(files);
photosGrid.DataContext = imageList;

2 个答案:

答案 0 :(得分:3)

这里有很多问题。

第一个是你的集合需要实现INotifyPropertyChanged来正确支持增量加载(不要问我为什么)。幸运的是,它很容易修复:只需继承ObservableCollection<T>而不是List<T>

第二个问题来自您LoadMoreItemsAwaitable的实施。更具体地说,for循环:

for (int i = Count; i < Count + count; i++)
{
    using (var fileStream = await photos[i].OpenAsync(FileAccessMode.Read))
    {
        BitmapImage bitmapImage = new BitmapImage();
        await bitmapImage.SetSourceAsync(fileStream);
        this.Add(bitmapImage);
    }
}

每次向集合中添加项目(this.Add(bitmapImage))时,Count的值都会增加。结果是iCount同时增加,使您的循环无限。为防止这种情况,请将Count的值保存在循环之外:

int offset = this.Count;

for (int i = offset; i < offset + count && i < photos.Count; i++)
{
    using (var fileStream = await photos[i].OpenAsync(FileAccessMode.Read))
    {
        BitmapImage bitmapImage = new BitmapImage();
        await bitmapImage.SetSourceAsync(fileStream);
        this.Add(bitmapImage);
    }
}

请注意,我还会检查i是否低于photos.Count,否则您可能会遇到ArgumentOufOfRangeException。

从这一点开始,你可以尝试,你会发现它会起作用。不过,如果向下滚动列表,内存将永久增加。为什么?因为您正在存储BitmapImage,因此无法实现虚拟化的好处。

让我解释一下:

  1. 首先,您的网格显示五个元素。因此,您的收藏中会加载五个元素。
  2. 用户向下滚动,网格需要显示下五个元素。它从您的集合中加载了五个新元素,并丢弃了之前的五个元素以节省内存(由于虚拟化)。问题:仍然将这五个项目存储在您的收藏中!因此,永远不会释放内存。
  3. 不幸的是,我认为没有一种完美的方法可以用ISupportIncrementalLoading解决这个问题(没有API告诉网格需要重新显示以前的元素,所以你需要一直保留它们) 。但是你仍然可以通过仅存储文件的路径而不是BitmapImage来避免占用内存。

    问题:有一种方法可以通过仅提供路径(使用Image URI方案)来填充ms-appx控件,但据我所知,它对于存储的图片不起作用在图片库中。所以需要在某个时候返回BitmapImage控件。起初,我考虑过编写转换器(将路径转换为BitmapImage,但它需要异步API,并且转换器是同步的...我能想到的最简单的解决方案是创建自己的{{1}控制,可以加载这种路径。Image控件是密封的,所以你不能直接从它继承(有时,我认为WinRT是专门设计来惹恼开发人员),但你可以包装它一个UserControl。

    让我们创建一个名为Image的UserControl。 XAML只包含LocalImage控件:

    Image

    在代码隐藏中,我们创建了一个依赖项属性,并使用它来加载图片:

    <UserControl
        x:Class="StackOverflowUniversal10.LocalImage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        d:DesignHeight="300"
        d:DesignWidth="400">
    
        <Image x:Name="Image" Width="90" Height="90" Margin="5" Source="{Binding}" Stretch="UniformToFill"/>
    </UserControl>
    

    然后修改您的页面以使用UserControl而不是public sealed partial class LocalImage : UserControl { public static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof (string), typeof (LocalImage), new PropertyMetadata(null, SourceChanged)); public LocalImage() { this.InitializeComponent(); } public string Source { get { return this.GetValue(SourceProperty) as string; } set { this.SetValue(SourceProperty, value); } } private async static void SourceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) { var control = (LocalImage)obj; var path = e.NewValue as string; if (string.IsNullOrEmpty(path)) { control.Image.Source = null; } else { var file = await StorageFile.GetFileFromPathAsync(path); using (var fileStream = await file.OpenAsync(FileAccessMode.Read)) { BitmapImage bitmapImage = new BitmapImage(); await bitmapImage.SetSourceAsync(fileStream); control.Image.Source = bitmapImage; } } } } 控件:

    Image

    最后但并非最不重要的是,更改您的集合以存储路径而不是图片:

    <GridView x:Name="photosGrid" Height="382" Width="400" ItemsSource="{Binding}" Margin="0,0,-0.333,0" SelectionMode="Multiple" Background="Black">
        <GridView.ItemTemplate>
            <DataTemplate>
                <local:LocalImage Width="90" Height="90" Margin="5" Source="{Binding}"/>
            </DataTemplate>
        </GridView.ItemTemplate>
    </GridView>
    

    它应该工作,具有稳定的内存消耗。请注意,您可以(并且应该)通过设置class VirtualList : ObservableCollection<string>, ISupportIncrementalLoading { private IReadOnlyList<StorageFile> photos; public VirtualList(IReadOnlyList<StorageFile> files) : base() { photos = files; } public bool HasMoreItems { get { return this.Count < photos.Count; } } public Windows.Foundation.IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count) { return LoadMoreItemsAwaitable(count).AsAsyncOperation<LoadMoreItemsResult>(); } private async Task<LoadMoreItemsResult> LoadMoreItemsAwaitable(uint count) { int offset = this.Count; for (int i = offset; i < offset + count && i < photos.Count; i++) { this.Add(photos[i].Path); } return new LoadMoreItemsResult { Count = count }; } } 的{​​{1}}和DecodePixelHeight属性来进一步减少内存消耗(以便运行时将内存中的缩略图加载而不是完整-res图片)。

答案 1 :(得分:3)

除了Kevin的反馈,我建议使用GetThumbnailAsync()而不是将完整尺寸的图像拉入内存。

您可以考虑的另一件事是使用转换器,以便在调用绑定时仅加载图像缩略图,然后您可以在ItemTemplate上使用Deferred Rendering(延迟渲染是提高性能的好方法)大/长名单)

这是我的ThumbnailConverters之一:

public class ThumbnailToBitmapImageConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        if (DesignMode.DesignModeEnabled)
            return "images/VideoFileIcon.png";

        var fileName = (string) value;

        if (string.IsNullOrEmpty(fileName))
            return "";

        return GetThumbnailImage(fileName);
    }

    private async Task<BitmapImage> GetThumbnailImage(string fileName)
    {
        try
        {
            var file = await ApplicationData.Current.LocalFolder.GetFileAsync(fileName)
                .AsTask().ConfigureAwait(false);
            var thumbnail = await file.GetScaledImageAsThumbnailAsync(ThumbnailMode.ListView, 90, ThumbnailOptions.UseCurrentScale)
                .AsTask().ConfigureAwait(false);
            var bmi = new BitmapImage();
            bmi.SetSource(thumbnail);
            return bmi;
        }
        catch (Exception ex)
        {
           await new MessageDialog(ex.Message).ShowAsync();
        }
        return null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotImplementedException();
    }
}