Почему я получаю исключение OutOfMemoryException, когда у меня есть изображения в моем ListBox?
Я хочу отобразить все изображения, хранящиеся в папке с папкой Windows Phone 8 в моей собственной галерее, которая использует ListBox
для отображения изображений.
Код ListBox
выглядит следующим образом:
<phone:PhoneApplicationPage.Resources>
<MyApp:PreviewPictureConverter x:Key="PreviewPictureConverter" />
</phone:PhoneApplicationPage.Resources>
<ListBox Name="previewImageListbox" VirtualizingStackPanel.VirtualizationMode="Recycling">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel CleanUpVirtualizedItemEvent="VirtualizingStackPanel_CleanUpVirtualizedItemEvent_1">
</VirtualizingStackPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Image Source="{Binding Converter={StaticResource PreviewPictureConverter}}" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Со следующим конвертером:
public class PreviewPictureConverter : System.Windows.Data.IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
PreviewImageItem c = value as PreviewImageItem;
if (c == null)
return null;
return c.ImageData;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Изображения сохраняются в пользовательском классе:
class PreviewImageItem
{
public Picture _picture = null;
public BitmapImage _bitmap = null;
public PreviewImageItem(Picture pic)
{
_picture = pic;
}
public BitmapImage ImageData
{
get
{
System.Diagnostics.Debug.WriteLine("Get picture " + _picture.ToString());
_bitmap = new BitmapImage();
Stream data = _picture.GetImage();
try
{
_bitmap.SetSource(data); // Out-of memory exception (see text)
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("Exception : " + ex.ToString());
}
finally
{
data.Close();
data.Dispose();
data = null;
}
return _bitmap;
}
}
}
Следующий код используется для установки источника данных ListBox
:
private List<PreviewImageItem> _galleryImages = new List<PreviewImageItem>();
using (MediaLibrary library = new MediaLibrary())
{
PictureCollection galleryPics = library.Pictures;
foreach (Picture pic in galleryPics)
{
_galleryImages.Add(new PreviewImageItem(pic));
}
previewImageListbox.ItemsSource = _galleryImages;
};
Наконец, вот код очистки:
private void VirtualizingStackPanel_CleanUpVirtualizedItemEvent_1(object sender, CleanUpVirtualizedItemEventArgs e)
{
PreviewImageItem item = e.Value as PreviewImageItem;
if (item != null)
{
System.Diagnostics.Debug.WriteLine("Cleanup");
item._bitmap = null;
}
}
Все это прекрасно работает, но после нескольких изображений (особенно при быстрой прокрутке) код падает с OutOfMemoryException
. Метод VirtualizingStackPanel_CleanUpVirtualizedItemEvent_1
называется регулярным (например, каждые 2 или 3 записи списка), когда прокручивается ListBox
.
Что не так с этим примером кода?
Почему память не освобождается (достаточно быстро)?
Ответы
Ответ 1
О, я недавно убил весь день, чтобы сделать это!
Итак, решение:
Сделайте свои свободные ресурсы для управления изображением. Поэтому установите
BitmapImage bitmapImage = image.Source as BitmapImage;
bitmapImage.UriSource = null;
image.Source = null;
как было упомянуто ранее.
Убедитесь, что вы виртуализировали _bitmap для каждого элемента списка. Вы должны загрузить его по требованию (метод LongListSelector.Realized), и вам нужно его уничтожить! Он не собирается собираться автоматически, и GC.Collect тоже не работает.
Нулевая ссылка тоже не работает:(
Но вот метод:
Сделайте 1x1 пиксельный файл. Скопируйте его в сборку и сделайте поток ресурсов из него, чтобы избавиться от изображений с размером 1x1 пиксель. Привязать настраиваемый метод размещения к событию LongListSelector.UnRealized(e.Container обрабатывает элемент списка).
public static void DisposeImage(BitmapImage image)
{
Uri uri= new Uri("oneXone.png", UriKind.Relative);
StreamResourceInfo sr=Application.GetResourceStream(uri);
try
{
using (Stream stream=sr.Stream)
{
image.DecodePixelWidth=1; //This is essential!
image.SetSource(stream);
}
}
catch { }
}
Работает для меня в LongListSelector с 1000 изображениями шириной 400.
Если вы пропустите 2-й шаг с сбором данных, вы можете увидеть хорошие результаты, но память переполнена после прокрутки списка из 100-200 элементов.
Ответ 2
У вас только что был Windows Phone, на котором было показано все изображение в папке "Фотографии" в пользовательской медиа-библиотеке на экране. Это невероятно интенсивное использование памяти и учитывая ограничение на 150 МБ для приложений WP8, неудивительно, что вы получаете исключения OOM.
Несколько вещей, которые вы должны рассмотреть, добавьте:
1) Установите для источника Source и SourceUri значение null при прокрутке списка из списка. См. "Кэширование изображений" в статье Стефана здесь. http://blogs.msdn.com/b/swick/archive/2011/04/07/image-tips-for-windows-phone-7.aspx
BitmapImage bitmapImage = image.Source as BitmapImage;
bitmapImage.UriSource = null;
image.Source = null;
2) Если вы на WP8, обязательно установите DecodePixelWidth и/или DecodePixelHeight. Таким образом, изображение будет загружено в память, будет изменяться постоянно, и только копия с измененным размером будет сохранена в памяти. Изображения, загруженные в память, могут быть намного больше размера экрана самого телефона. Так важно обрезать их до нужного размера и сохранять только измененные изображения. Установите BitmapImage.DecodePixelWidth = 480 (максимально), чтобы помочь с этим.
var bmp = new BitmapImage();
// no matter the actual size,
// this bitmap is decoded to 480 pixels width (aspect ratio preserved)
// and only takes up the memory needed for this size
bmp.DecodePixelWidth = 480;
bmp.UriSource = new Uri(@"Assets\Demo.png", UriKind.Relative);
ImageControl.Source = bmp;
(пример кода из здесь)
3) Почему вы используете Picture.GetImage() вместо Picture.GetThumbnail()? Вам действительно нужно изображение, чтобы охватить весь экран?
4) Рассмотрите возможность перехода из ListBox в LongListSelector, если это эксклюзивное приложение WP8. LLS имеет гораздо лучшую визуализацию, чем ListBox. Если посмотреть на образец кода, вам может быть достаточно просто изменить элемент XAML ListBox на элемент LongListSelector.
Ответ 3
Попробуйте этот подход: Загрузка изображения с автоматической очисткой памяти. Пример проекта здесь: https://simca.codeplex.com/