Реализация отложенной загрузки элементов в сгруппированном ICollectionView - PullRequest
0 голосов
/ 20 февраля 2020

У меня есть набор предметов (~ 12.000), которые я хочу показать в ListView. Каждый из этих элементов представляет собой модель представления с назначенным изображением, которое не является частью пакета приложения (оно находится во «внешней» папке на локальном диске c). И из-за ограничений UWP я не могу (на самом деле и проверено) назначить Uri для ImageSource и вместо этого должен использовать метод SetSourceAsync. Из-за этого начальное время загрузки приложения слишком велико, потому что все ImageSource объекты должны быть инициализированы при запуске, даже если изображение не будет видно пользователю (список не фильтруется при запуске) и полученная память потребление ~ 4 ГБ. Копирование файлов изображений в каталог данных приложения решило бы проблему, но для меня это не является решением, поскольку изображения регулярно обновляются и это приводит к потере дискового пространства c.

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

Теперь я подумал, что мог бы реализовать IItemsRangeInfo или ISupportIncrementalLoading в каждой группе и отложить инициализацию модели представления, чтобы загружались только изображения, если они должны быть отображены. Я проверяю это, и это, похоже, не работает, потому что ни один метод интерфейса не вызывается для групп во время выполнения (пожалуйста, исправьте меня здесь, если это не так и может быть достигнуто). В текущей (не рабочей) версии используется пользовательский ICollectionView (для целей тестирования), но DeferredObservableCollection может также реализовать IGrouping<TKey, TElement> и использоваться в CollectionViewSource.

Есть ли способ, которым я Можно ли выполнить отложенную инициализацию или использовать Uri для источника изображения или мне нужно использовать «обычную» коллекцию или пользовательский ICollectionView как ItemsSource для ListView, который реализует желаемое поведение?

Текущая целевая версия приложения: 1803 (сборка 17134) Текущая целевая версия приложения: обновление создателей осени (сборка 16299) Можно изменить как минимальную, так и целевую версию.

Код для создания изображения source:

public class ImageService
{
    // ...
    private readonly IDictionary<short, ImageSource> imageSources;

    public async Task<ImageSource> GetImageSourceAsync(Item item)
    {
        if (imageSources.ContainsKey(item.Id))
            return imageSources[item.Id];

        try
        {
            var imageFolder = await storageService.GetImagesFolderAsync();
            var imageFile = await imageFolder.GetFileAsync($"{item.Id}.jpg");

            var source = new BitmapImage();
            await source.SetSourceAsync(await imageFile.OpenReadAsync());

            return imageSources[item.Id] = source;
        }
        catch (FileNotFoundException)
        {
            // No image available.
            return imageSources[item.Id] = unknownImageSource;
        }
    }
}

Код для результирующих групп, возвращаемых свойством ICollectionView.CollectionGroups:

public class CollectionViewGroup : ICollectionViewGroup
{
    public object Group { get; }

    public IObservableVector<object> GroupItems { get; }

    public CollectionViewGroup(object group, IObservableVector<object> items)
    {
        Group = group ?? throw new ArgumentNullException(nameof(group));
        GroupItems = items ?? throw new ArgumentNullException(nameof(items));
    }
}

Код коллекции, содержащей элементы каждой группы:

public sealed class DeferredObservableCollection<T, TSource>
    : ObservableCollection<T>, IObservableVector<T>, IItemsRangeInfo //, ISupportIncrementalLoading
    where T : class
    where TSource : class
{
    private readonly IList<TSource> source;
    private readonly Func<TSource, Task<T>> conversionFunc;

    // private int currentIndex; // Used for ISupportIncrementalLoading.

    // Used to get the total number of items when using ISupportIncrementalLoading.
    public int TotalCount => source.Count;

    /// <summary>
    /// Initializes a new instance of the <see cref="DeferredObservableCollection{T, TSource}"/> class.
    /// </summary>
    /// <param name="source">The source collection.</param>
    /// <param name="conversionFunc">The function used to convert item from <typeparamref name="TSource"/> to <typeparamref name="T"/>.</param>
    /// <exception cref="ArgumentNullException">
    /// <paramref name="source"/> is <see langword="null"/> or
    /// <paramref name="conversionFunc"/> is <see langword="null"/>.
    /// </exception>
    public DeferredObservableCollection(IList<TSource> source, Func<TSource, Task<T>> conversionFunc)
    {
        this.source = source ?? throw new ArgumentNullException(nameof(source));
        this.conversionFunc = conversionFunc ?? throw new ArgumentNullException(nameof(conversionFunc));

        // Ensure the underlying lists capacity.
        // Used for IItemsRangeInfo.
        for (var i = 0; i < source.Count; ++i)
            Items.Add(default);
    }

    private class VectorChangedEventArgs : IVectorChangedEventArgs
    {
        public CollectionChange CollectionChange { get; }

        public uint Index { get; }

        public VectorChangedEventArgs(CollectionChange collectionChange, uint index)
        {
            CollectionChange = collectionChange;
            Index = index;
        }
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnCollectionChanged(e);
        // For testing purposes the peformed action is not differentiated.
        VectorChanged?.Invoke(this, new VectorChangedEventArgs(CollectionChange.ItemInserted, (uint)e.NewStartingIndex));
    }

    //#region ISupportIncrementalLoading Support

    //public bool HasMoreItems => currentIndex < source.Count;

    //public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
    //{
          // Won't get called.
    //    return AsyncInfo.Run(async cancellationToken =>
    //    {
    //        if (currentIndex >= source.Count)
    //            return new LoadMoreItemsResult();

    //        var addedItems = 0u;

    //        while (currentIndex < source.Count && addedItems < count)
    //        {
    //            Add(await conversionFunc(source[currentIndex]));
    //            ++currentIndex;
    //            ++addedItems;
    //        }

    //        return new LoadMoreItemsResult { Count = addedItems };
    //    });
    //}

    //#endregion

    #region IObservableVector<T> Support

    public event VectorChangedEventHandler<T> VectorChanged;

    #endregion

    #region IItemsRangeInfo Support

    public void RangesChanged(ItemIndexRange visibleRange, IReadOnlyList<ItemIndexRange> trackedItems)
    {
        // Won't get called.
        ConvertItemsAsync(visibleRange, trackedItems).FireAndForget(null);
    }

    private async Task ConvertItemsAsync(ItemIndexRange visibleRange, IReadOnlyList<ItemIndexRange> trackedItems)
    {
        for (var i = visibleRange.FirstIndex; i < source.Count && i < visibleRange.LastIndex; ++i)
        {
            if (this[i] is null)
            {
                this[i] = await conversionFunc(source[i]);
            }
        }
    }

    public void Dispose()
    { }

    #endregion
}

1 Ответ

1 голос
/ 21 февраля 2020

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

1. Не инициализируйте 12000 изображений равномерно

Чтение 12000 изображений за раз неизбежно увеличит использование памяти. Но мы можем создать UserControl как один блок изображения и передать работу по загрузке изображений в эти блоки.

-ImageItem.cs

public class ImageItem
{
    public string Name { get; set; }
    public BitmapImage Image { get; set; } = null;
    public ImageItem()
    {

    }
    public async Task Init()
    {
        // do somethings..
        // get image from folder, named imageFile
        Image = new BitmapImage();
        await Image.SetSourceAsync(await imageFile.OpenReadAsync());
    }
}

-ImageItemControl.xaml

<UserControl
    ...>

    <StackPanel>
        <Image Width="200" Height="200" x:Name="MyImage"/>
    </StackPanel>
</UserControl>

-ImageItemControl.xaml.cs

public sealed partial class ImageItemControl : UserControl
{
    public ImageItemControl()
    {
        this.InitializeComponent();
    }


    public ImageItem Data
    {
        get { return (ImageItem)GetValue(DataProperty); }
        set { SetValue(DataProperty, value); }
    }

    public static readonly DependencyProperty DataProperty =
        DependencyProperty.Register("Data", typeof(ImageItem), typeof(ImageItemControl), new PropertyMetadata(null,new PropertyChangedCallback(Data_Changed)));

    private static async void Data_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if(e.NewValue != null)
        {
            var image = e.NewValue as ImageItem;
            var instance = d as ImageItemControl;
            if (image.Image == null)
            {
                await image.Init();
            }
            instance.MyImage.Source = image.Image;
        }
    }
}

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

<Page.Resources>
    <DataTemplate x:DataType="local:ImageItem" x:Key="ImageTemplate">
        <controls:ImageItemControl Data="{Binding}"/>
    </DataTemplate>
</Page.Resources>
<Grid>
    <GridView ItemTemplate="{StaticResource ImageTemplate}"
              .../>
</Grid>

Пожалуйста, измените этот код в соответствии с вашей реальной ситуацией

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

2. Ограничение разрешения BitmapImage

Это очень важно, это может значительно снизить потребление памяти при загрузке большого количества изображений.

Например, у вас есть изображение с Разрешение 1920x1080, но в приложении отображается только разрешение 200x200. Тогда загрузка исходного изображения приведет к потере системных ресурсов.

Мы можем изменить метод ImageItem.Init:

public async Task Init()
{
    // do somethings..
    // get image from folder, named imageFile
    Image = new BitmapImage() { DecodePixelWidth = 200 };
    await Image.SetSourceAsync(await imageFile.OpenReadAsync());
}

Надеюсь, что эти два метода помогут вам уменьшить использование памяти.

...