我正在构建一个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;
答案 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
的值都会增加。结果是i
和Count
同时增加,使您的循环无限。为防止这种情况,请将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
,因此无法实现虚拟化的好处。
让我解释一下:
不幸的是,我认为没有一种完美的方法可以用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();
}
}