在分组的 ICollectionView 中实现延迟项加载

Implement deferred item loading in a grouped ICollectionView

我有一组项目 (~12.000) 我想在 ListView 中展示。这些项目中的每一个都是一个视图模型,它有一个分配的图像,该图像不是应用程序包的一部分(它位于本地磁盘上的 'external' 文件夹中)。而且由于 UWP 的限制,我不能(afaik 和测试)将 Uri 分配给 ImageSource 并且必须改用 SetSourceAsync 方法。因此,应用程序的初始加载时间太长,因为所有 ImageSource 对象都必须在启动时初始化,即使图像不会被用户看到(列表在启动时未过滤)和由此产生的内存消耗约为 4GB。将图像文件复制到应用程序数据目录可以解决问题,但对我来说不是解决方案,因为图像会定期更新并且会浪费磁盘 space.

项目显示在 ListView 中,使用分组的 ICollectionView 作为来源。

现在我想我可以在每个组上实现 IItemsRangeInfoISupportIncrementalLoading 并推迟视图模型的初始化,以便仅加载要显示的图像。我对此进行了测试,但它似乎不起作用,因为在运行时接口的方法都没有在组上调用(如果这不是真的并且可以实现,请在这里更正我)。当前(不工作)版本使用自定义 ICollectionView(用于测试目的),但 DeferredObservableCollection 也可以实现 IGrouping<TKey, TElement> 并用于 CollectionViewSource.

有什么方法可以实现延迟初始化或使用 Uri 作为图像源,还是必须使用 'plain' 集合或自定义 ICollectionView 作为 ItemsSource 在实现所需行为的 ListView 上?

应用程序的当前目标版本:1803(内部版本 17134) 应用程序的当前目标版本:Fall Creators Update(内部版本 16299) 两者(最低版本和目标版本)都可以更改。

创建图像源的代码:

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
}

从减少内存消耗的角度来说,不推荐使用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>

Please modify this code according to your actual situation

这样做有一些好处。通过分布式的方式,一方面提高了图片的加载速度(同时加载)。另一方面,通过虚拟化,有些图片实际上并没有被渲染,可以减少内存占用。

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

希望这两种方法可以帮助您减少内存占用。