在分组的 ICollectionView 中实现延迟项加载
Implement deferred item loading in a grouped ICollectionView
我有一组项目 (~12.000) 我想在 ListView
中展示。这些项目中的每一个都是一个视图模型,它有一个分配的图像,该图像不是应用程序包的一部分(它位于本地磁盘上的 'external' 文件夹中)。而且由于 UWP 的限制,我不能(afaik 和测试)将 Uri
分配给 ImageSource
并且必须改用 SetSourceAsync
方法。因此,应用程序的初始加载时间太长,因为所有 ImageSource
对象都必须在启动时初始化,即使图像不会被用户看到(列表在启动时未过滤)和由此产生的内存消耗约为 4GB。将图像文件复制到应用程序数据目录可以解决问题,但对我来说不是解决方案,因为图像会定期更新并且会浪费磁盘 space.
项目显示在 ListView
中,使用分组的 ICollectionView
作为来源。
现在我想我可以在每个组上实现 IItemsRangeInfo
或 ISupportIncrementalLoading
并推迟视图模型的初始化,以便仅加载要显示的图像。我对此进行了测试,但它似乎不起作用,因为在运行时接口的方法都没有在组上调用(如果这不是真的并且可以实现,请在这里更正我)。当前(不工作)版本使用自定义 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());
}
希望这两种方法可以帮助您减少内存占用。
我有一组项目 (~12.000) 我想在 ListView
中展示。这些项目中的每一个都是一个视图模型,它有一个分配的图像,该图像不是应用程序包的一部分(它位于本地磁盘上的 'external' 文件夹中)。而且由于 UWP 的限制,我不能(afaik 和测试)将 Uri
分配给 ImageSource
并且必须改用 SetSourceAsync
方法。因此,应用程序的初始加载时间太长,因为所有 ImageSource
对象都必须在启动时初始化,即使图像不会被用户看到(列表在启动时未过滤)和由此产生的内存消耗约为 4GB。将图像文件复制到应用程序数据目录可以解决问题,但对我来说不是解决方案,因为图像会定期更新并且会浪费磁盘 space.
项目显示在 ListView
中,使用分组的 ICollectionView
作为来源。
现在我想我可以在每个组上实现 IItemsRangeInfo
或 ISupportIncrementalLoading
并推迟视图模型的初始化,以便仅加载要显示的图像。我对此进行了测试,但它似乎不起作用,因为在运行时接口的方法都没有在组上调用(如果这不是真的并且可以实现,请在这里更正我)。当前(不工作)版本使用自定义 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());
}
希望这两种方法可以帮助您减少内存占用。