Почему я получаю 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/

Другие вопросы по тегам