Skip to content

Commit d2a7d60

Browse files
committed
Run bitmap loading on thread pool instead of the UI thread. This significantly reduces lag when there is a lot of images.
1 parent 443432c commit d2a7d60

File tree

2 files changed

+93
-39
lines changed

2 files changed

+93
-39
lines changed

AsyncImageLoader.Avalonia/AdvancedImage.axaml.cs

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Threading;
3+
using System.Threading.Tasks;
34
using Avalonia;
45
using Avalonia.Controls;
56
using Avalonia.Markup.Xaml;
@@ -176,35 +177,51 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
176177

177178
private async void UpdateImage(string? source, IAsyncImageLoader? loader)
178179
{
179-
_updateCancellationToken?.Cancel();
180-
_updateCancellationToken?.Dispose();
181-
var cancellationTokenSource = _updateCancellationToken = new CancellationTokenSource();
182-
IsLoading = true;
183-
CurrentImage = null;
184-
185-
Bitmap? bitmap = null;
186-
if (source != null)
187-
{
188-
// Hack to support relative URI
189-
// TODO: Refactor IAsyncImageLoader to support BaseUri
190-
try
191-
{
192-
var uri = new Uri(source, UriKind.RelativeOrAbsolute);
193-
if (AssetLoader.Exists(uri, _baseUri)) bitmap = new Bitmap(AssetLoader.Open(uri, _baseUri));
194-
}
195-
catch (Exception)
196-
{
197-
// ignored
198-
}
199-
200-
loader ??= ImageLoader.AsyncImageLoader;
201-
bitmap ??= await loader.ProvideImageAsync(source);
202-
}
203-
204-
if (cancellationTokenSource.IsCancellationRequested) return;
205-
CurrentImage = bitmap;
206-
IsLoading = false;
207-
}
180+
_updateCancellationToken?.Cancel();
181+
_updateCancellationToken?.Dispose();
182+
var cancellationTokenSource = _updateCancellationToken = new CancellationTokenSource();
183+
IsLoading = true;
184+
CurrentImage = null;
185+
186+
187+
Bitmap? bitmap = await Task.Run(async () =>
188+
{
189+
try
190+
{
191+
if (source == null)
192+
return null;
193+
194+
// A small delay allows to cancel early if the image goes out of screen too fast (eg. scrolling)
195+
// The Bitmap constructor is expensive and cannot be cancelled
196+
await Task.Delay(10, cancellationTokenSource.Token);
197+
198+
// Hack to support relative URI
199+
// TODO: Refactor IAsyncImageLoader to support BaseUri
200+
try
201+
{
202+
var uri = new Uri(source, UriKind.RelativeOrAbsolute);
203+
if (AssetLoader.Exists(uri, _baseUri))
204+
return new Bitmap(AssetLoader.Open(uri, _baseUri));
205+
}
206+
catch (Exception)
207+
{
208+
// ignored
209+
}
210+
211+
loader ??= ImageLoader.AsyncImageLoader;
212+
return await loader.ProvideImageAsync(source);
213+
}
214+
catch (TaskCanceledException)
215+
{
216+
return null;
217+
}
218+
});
219+
220+
if (cancellationTokenSource.IsCancellationRequested)
221+
return;
222+
CurrentImage = bitmap;
223+
IsLoading = false;
224+
}
208225

209226
private void UpdateCornerRadius(CornerRadius radius)
210227
{

AsyncImageLoader.Avalonia/ImageLoader.cs

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using System.Threading;
25
using AsyncImageLoader.Loaders;
36
using Avalonia;
47
using Avalonia.Controls;
8+
using Avalonia.Media.Imaging;
9+
using System.Collections.Concurrent;
510

611
namespace AsyncImageLoader;
712

@@ -22,18 +27,50 @@ static ImageLoader()
2227

2328
public static IAsyncImageLoader AsyncImageLoader { get; set; } = new RamCachedWebImageLoader();
2429

25-
private static async void OnSourceChanged(Image sender, AvaloniaPropertyChangedEventArgs args) {
26-
var url = args.GetNewValue<string?>();
27-
SetIsLoading(sender, true);
30+
private static ConcurrentDictionary<Image, CancellationTokenSource> _pendingOperations = new ConcurrentDictionary<Image, CancellationTokenSource>();
31+
private static async void OnSourceChanged(Image sender, AvaloniaPropertyChangedEventArgs args) {
32+
var url = args.GetNewValue<string?>();
2833

29-
var bitmap = url == null
30-
? null
31-
: await AsyncImageLoader.ProvideImageAsync(url);
32-
if (GetSource(sender) != url) return;
33-
sender.Source = bitmap!;
34+
// Cancel/Add new pending operation
35+
CancellationTokenSource? cts = _pendingOperations.AddOrUpdate(sender, new CancellationTokenSource(),
36+
(x, y) =>
37+
{
38+
y.Cancel();
39+
return new CancellationTokenSource();
40+
});
3441

35-
SetIsLoading(sender, false);
36-
}
42+
if (url == null)
43+
{
44+
((ICollection<KeyValuePair<Image, CancellationTokenSource>>)_pendingOperations).Remove(new KeyValuePair<Image, CancellationTokenSource>(sender, cts));
45+
sender.Source = null;
46+
return;
47+
}
48+
49+
SetIsLoading(sender, true);
50+
51+
Bitmap? bitmap = await Task.Run(async () =>
52+
{
53+
try
54+
{
55+
// A small delay allows to cancel early if the image goes out of screen too fast (eg. scrolling)
56+
// The Bitmap constructor is expensive and cannot be cancelled
57+
await Task.Delay(10, cts.Token);
58+
59+
return await AsyncImageLoader.ProvideImageAsync(url);
60+
}
61+
catch (TaskCanceledException)
62+
{
63+
return null;
64+
}
65+
});
66+
67+
if (bitmap != null && !cts.Token.IsCancellationRequested)
68+
sender.Source = bitmap!;
69+
70+
// "It is not guaranteed to be thread safe by ICollection, but ConcurrentDictionary's implementation is. Additionally, we recently exposed this API for .NET 5 as a public ConcurrentDictionary.TryRemove"
71+
((ICollection<KeyValuePair<Image, CancellationTokenSource>>)_pendingOperations).Remove(new KeyValuePair<Image, CancellationTokenSource>(sender, cts));
72+
SetIsLoading(sender, false);
73+
}
3774

3875
public static string? GetSource(Image element)
3976
{

0 commit comments

Comments
 (0)