Загрузчик изображений с автоматической очисткой памяти

У меня есть список (простой ListBox) элементов с изображениями на основе master-detail (если пользователь нажимает на элемент списка, открывается страница сведений). Я столкнулся с довольно известной проблемой утечек памяти изображений, описанной здесь, здесь, здесь и здесь.

Один из возможных способов - запустить все изображения при переходе с NavigatingFrom и очистить их.

В одном из потоков я нашел более интересное решение: оно очищает изображения автоматически, но это не работает для виртуализации (изображения теряются или смешиваются, если добавить личное поле для хранения ImageSource). Предложенное исправление состояло в том, чтобы добавить свойство зависимости.

Но я все еще сталкиваюсь с той же проблемой: изображения перемешиваются после прокрутки вниз и возврата вверх. Похоже, что свойства зависимостей меняются случайным образом, но я не могу уловить момент, когда они меняются.

public class SafePicture : ContentControl
{
    public static readonly DependencyProperty SafePathProperty =
        DependencyProperty.RegisterAttached(
            "SafePath",
            typeof(string),
            typeof(SafePicture),
            new PropertyMetadata(OnSourceWithCustomRefererChanged));

    public string SafePath
    {
        get { return (string)GetValue(SafePathProperty); }
        set { SetValue(SafePathProperty, value); }
    }

    private static void OnSourceWithCustomRefererChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue == null) // New value here
            return;
    }


    public SafePicture()
    {
        Content = new Image();
        Loaded += OnLoaded;
        Unloaded += OnUnloaded;
    }

    private void OnLoaded(object _sender, RoutedEventArgs _routedEventArgs)
    {
        var image = Content as Image;

        if (image == null)
            return;

        var path = (string)GetValue(SafePathProperty); // Also, tried SafePath (debugger cant catch setter and getter calls), but same result.

        image.Source = null;
        {
            var request = WebRequest.Create(path) as HttpWebRequest;
            request.AllowReadStreamBuffering = true;
            request.BeginGetResponse(result =>
            {
                try
                {
                    Stream imageStream = request.EndGetResponse(result).GetResponseStream();
                    DispatcherHelper.CheckBeginInvokeOnUI(() =>
                    {
                        if (imageStream == null)
                        {
                            image.Source = new BitmapImage { UriSource = new Uri(path, UriKind.Relative) };
                            return;
                        }

                        var bitmapImage = new BitmapImage();
                        bitmapImage.CreateOptions = BitmapCreateOptions.BackgroundCreation;
                        bitmapImage.SetSource(imageStream);
                        image.Source = bitmapImage;
                    });
                }
                catch (WebException)
                {
                }
            }, null);
        }
    }


    private void OnUnloaded(object sender, RoutedEventArgs e)
    {
        var image = Content as Image;

        if (image == null)
            return;

        var bitmapImage = image.Source as BitmapImage;
        if (bitmapImage != null)
            bitmapImage.UriSource = null;
        image.Source = null;
    }
}

Использование:

<wpExtensions:SafePicture SafePath="{Binding ImageUrl}"/>

Итак, на первый взгляд все работает нормально, но если прокрутить вниз и вернуться вверх, изображения меняются случайным образом.

РЕДАКТИРОВАТЬ: в этом случае, на данный момент, я использую только чистый ListBox, без виртуализации (но ожидаю этого в других случаях).

EDIT2: пример проекта для воспроизведения этой проблемы. Я полагаю, что это будет содержать решение в ближайшее время: https://simca.codeplex.com/

1 ответ

Решение

Проблема заключается в том, что при использовании виртуализации элемент пользовательского интерфейса, используемый для каждого элемента, перерабатывается и повторно используется для другого объекта (например, объекта изображения), и, поскольку вы загружаете изображение асинхронно, когда выполняете прокрутку достаточно быстро, вы будете устанавливать растровое изображение на изображение, которое уже использовалось для другого элемента.
Быстрое решение проблемы - просто проверить, что значение пути остается тем же, а если нет, просто вернуть, так как изображение уже использовалось для другого объекта:

...
var request = WebRequest.Create(path) as HttpWebRequest;
        request.AllowReadStreamBuffering = true;
        request.BeginGetResponse(result =>
        {

            try
            {

                Stream imageStream = request.EndGetResponse(result).GetResponseStream();
                DispatcherHelper.CheckBeginInvokeOnUI(() =>
                {
                if (path!=SafePath){
                  //Item has been recycled
                  return;
                }
                 ....

Редактировать: в коде было несколько проблем: -Switch RegisterAttached to Register, RegisterAttached - для присоединенного свойства, а не для свойства обычной зависимости. -Call OnLoaded в OnSourceWithCustomRefererChanged, поскольку измененное свойство SafePath может фактически преуспеть после загрузки элемента. -Add clear uri and source в начале onLoaded, чтобы очистить изображение, когда путь пуст

Вот полный рабочий код:

public class SafeImage : ContentControl
{
    private SynchronizationContext uiThread;

    public static readonly DependencyProperty SafePathProperty =
        DependencyProperty.Register("SafePath", typeof (string), typeof (SafeImage),
        new PropertyMetadata(default(string), OnSourceWithCustomRefererChanged));

    public string SafePath
    {
        get { return (string) GetValue(SafePathProperty); }
        set { SetValue(SafePathProperty, value); }
    }


    private static void OnSourceWithCustomRefererChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        SafeImage safeImage = o as SafeImage;
        safeImage.OnLoaded(null, null);
        //OnLoaded(null, null);
        if (e.NewValue == null)
            return;
    }





    public SafeImage()
    {
        Content = new Image();
        uiThread = SynchronizationContext.Current;

        Loaded += OnLoaded;
        Unloaded += OnUnloaded;
    }

    private void OnLoaded(object _sender, RoutedEventArgs _routedEventArgs)
    {
        var image = Content as Image;

        if (image == null)
            return;

        var path = SafePath; //(string)GetValue(SafePathProperty);
        //image.Source = new BitmapImage(new Uri(SafePath));
        Debug.WriteLine(path);

        var bitmapImage = image.Source as BitmapImage;
        if (bitmapImage != null)
            bitmapImage.UriSource = null;
        image.Source = null;

        if (String.IsNullOrEmpty(path))
        {
            //image.Source = new BitmapImage { UriSource = new Uri(Constants.RESOURCE_IMAGE_EMPTY_PRODUCT, UriKind.Relative) };
            return;
        }

        // If local image, just load it (non-local images paths starts with "http")
        if (path.StartsWith("/"))
        {
            image.Source = new BitmapImage { UriSource = new Uri(path, UriKind.Relative) };
            return;
        }



        {
            var request = WebRequest.Create(path) as HttpWebRequest;
            request.AllowReadStreamBuffering = true;
            request.BeginGetResponse(result =>
            {
                try
                {
                    Stream imageStream = request.EndGetResponse(result).GetResponseStream();
                    uiThread.Post(_ =>
                    {

                        if (path != this.SafePath)
                        {
                            return;
                        }
                        if (imageStream == null)
                        {
                            image.Source = new BitmapImage { UriSource = new Uri(path, UriKind.Relative) };
                            return;
                        }

                        bitmapImage = new BitmapImage();
                        bitmapImage.CreateOptions = BitmapCreateOptions.BackgroundCreation;
                        bitmapImage.SetSource(imageStream);
                        image.Source = bitmapImage;
                        //imageCache.Add(path, bitmapImage);
                    }, null);
                }
                catch (WebException)
                {
                    //uiThread.Post(_ =>
                    //{
                    //    image.Source = new BitmapImage { UriSource = new Uri(Constants.RESOURCE_IMAGE_EMPTY_PRODUCT, UriKind.Relative) };
                    //}, null);
                }
            }, null);
        }
    }


    private void OnUnloaded(object sender, RoutedEventArgs e)
    {
        var image = Content as Image;

        if (image == null)
            return;

        var bitmapImage = image.Source as BitmapImage;
        if (bitmapImage != null)
            bitmapImage.UriSource = null;
        image.Source = null;
    }
}

В качестве последнего замечания, Windows Phone ListBox по умолчанию использует виртуализацию и утилизацию (используется ItemPanel VirtualisedStackPanel).

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