Почему я получаю 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
прокручивается
Что не так с этим примером кода?
Почему память не освобождается (достаточно быстро)?
3 ответа
О, я недавно убил целый день, чтобы заставить это работать!
Итак, решение таково:
Сделайте свой Image control бесплатными ресурсами. Так что установите
BitmapImage bitmapImage = image.Source as BitmapImage;
bitmapImage.UriSource = null;
image.Source = null;
как было упомянуто ранее.
Убедитесь, что вы виртуализируете _bitmap для каждого элемента списка. Вы должны загрузить его по требованию (метод LongListSelector.Realized), и вы должны уничтожить его! Он не будет собираться автоматически, и GC.Collect тоже не работает. Нулевая ссылка тоже не работает:(Но вот метод: Создайте файл размером 1x1. Скопируйте его в сборку и создайте из него поток ресурсов, чтобы избавиться от изображений с пустым размером 1x1. Свяжите пользовательский метод dispose с событием LongListSelector.UnRealized (e. Контейнер обрабатывает ваш элемент списка).
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 элементов.
У вас только что был 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.
Попробуйте этот подход: загрузчик изображений с автоматической очисткой памяти. Пример проекта здесь: https://simca.codeplex.com/