web-dev-qa-db-fra.com

Pourquoi ai-je une exception OutOfMemoryException lorsque j'ai des images dans mon ListBox?

Je souhaite afficher toutes les images stockées dans le dossier photo de Windows Phone 8 dans ma galerie personnalisée, qui utilise une variable ListBox pour afficher les images.

Le code ListBox est le suivant:

    <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>

Avec le convertisseur suivant:

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();
    }
}

Les images sont stockées dans une classe personnalisée:

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;
        }
    }
}

Le code suivant permet de définir la source de données 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;
};

Enfin, voici le code de "nettoyage":

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;
    }
}

Tout cela fonctionne bien, mais le code se bloque avec une OutOfMemoryException après quelques images (en particulier lors du défilement rapide). La méthode VirtualizingStackPanel_CleanUpVirtualizedItemEvent_1 est appelée régulière (par exemple, toutes les 2 ou 3 entrées de zone de liste) lorsque la ListBox défile.

Quel est le problème avec cet exemple de code?

Pourquoi la mémoire n'est pas libérée (assez vite)?

27
Hyndrix

Oh, j'ai récemment tué toute la journée pour que cela fonctionne!

La solution est donc:

Rendez vos ressources libres de contrôle d’image. Alors mettez le 

BitmapImage bitmapImage = image.Source as BitmapImage;
bitmapImage.UriSource = null;
image.Source = null;

comme il a été mentionné auparavant. 

Assurez-vous de virtualiser _bitmap sur chaque élément de la liste. Vous devez le charger à la demande (méthode LongListSelector.Realized) et vous devez le détruire! Il ne sera pas collecté automatiquement et GC.Collect ne fonctionnera pas non plus. La référence nulle ne fonctionne pas aussi :( Mais voici la méthode: Make 1x1 pixel Copiez-le dans Assembly et créez un flux de ressources pour supprimer vos images avec un vide de 1x1 pixels. Liez la méthode de disposition personnalisée à l'événement LongListSelector.UnRealized (e.Container gère votre élément de liste). 

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 { }
}

Travailler pour moi dans LongListSelector avec 1000 images de 400 largeurs chacune.

Si vous manquez la procédure en 2 étapes avec la collecte de données, vous pouvez voir les bons résultats, mais la mémoire déborde après 100 défilement d'éléments. 

23
gleb.kudr

Vous venez d'avoir Windows Phone avec afficher toutes les images dans le dossier "images" de la bibliothèque multimédia d'un utilisateur à l'écran. Cela nécessite incroyablement beaucoup de mémoire et compte tenu de la limite de 150 Mo d'applications WP8, il n'est pas étonnant que vous obteniez des exceptions de MOO. 

Quelques choses que vous devriez envisager d'ajouter:

1) Définissez les propriétés Source et SourceUri sur null lorsque vous faites défiler les éléments listbox. Voir "Caching Images" dans l'article de Stefan ici @ 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) Si vous êtes sur WP8, assurez-vous de définir DecodePixelWidth et/ou DecodePixelHeight. Ainsi, une image sera chargée en mémoire, redimensionnée de façon permanente et seule la copie redimensionnée sera stockée en mémoire. Les images chargées en mémoire peuvent être beaucoup plus grandes que la taille de l'écran du téléphone lui-même. Il est donc essentiel de les rogner à la bonne taille et de ne stocker que les images redimensionnées. Définissez BitmapImage.DecodePixelWidth = 480 (au maximum) pour vous aider. 

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;

(exemple de code de ici )

3) Pourquoi utilisez-vous Picture.GetImage () au lieu de Picture.GetThumbnail ()? Avez-vous vraiment besoin de l'image pour occuper tout l'écran? 

4) Envisagez de passer de ListBox à LongListSelector s'il s'agit d'une application exclusive WP8. La LLS offre une bien meilleure virtualisation que ListBox. En examinant votre exemple de code, il vous suffira peut-être de remplacer l'élément XAML ListBox par l'élément LongListSelector. 

13
JustinAngel
0