Warning: obsolete repository
March 10, 2020 ยท View on GitHub
This repository has been deprecated in favor of https://github.com/roubachof/Xamarin.Forms.Nuke
FFImageLoading.ImageSourceHandler
This repository was inspired by Jonathan Peppers GlideX implementation of the new IImageViewHandler interface for Xamarin.Forms (https://github.com/jonathanpeppers/glidex).
Its goal is to provide the same kind of implementation for iOS, achieving a complete image caching solution for Xamarin.Forms: you don't have to change any line of your existing project, the Xamarin.Forms image source handlers will just be overridden with cache-enabled ones.
On iOS the ImageSourceHandler is implemented with FFImageLoading (https://github.com/luberda-molinet/FFImageLoading).
Benchmark
I changed a bit the glidex benchmark samples to have a more fair comparison. I switched from a random distribution of the images to a deterministic one to be sure we are comparing the same data set.
I used System.Diagnostics.Process.GetCurrentProcess().WorkingSet64 to have the memory workload of the process. The value given in the results are the consumed bytes between the MainPage and the complete loading of the target page.
For each test:
- Launch simulator
- Wait 4-5 seconds on
MainPage - Launch a Page
- Scroll till the end of page
- Get consumed bytes in the output window
| Page | Data Type | Xamarin.Forms | FFIL.ImageSourceHandler |
|---|---|---|---|
| GridOnlyRemotePage | Remote only | 247 828 480 | 18 698 240 (-92%) |
| GridPage | Remote and local mix | 186 748 928 | 18 644 992 (-90%) |
| ViewCellPage | Remote and local mix | 36 646 912 | 17 829 888 (-51%) |
| ImageCellPage | Remote and local mix | 81 604 608 | 12 218 368 (-85%) |
| HugeImagePage | Local only | 124 104 704 | 11 902 976 (-90%) |
Installation
FFImageLoading.ImageSourceHandler
- Install https://www.nuget.org/packages/ffimageloading.imagesourcehandler/ in your xamarin forms iOS project
- Add this Init method after
Forms.Initcall:
Xamarin.Forms.Forms.Init();
FFImageLoading.FormsHandler.Init(debug: false);
LoadApplication(new SDWebImageForms.Sample.Views.App());
GlideX.Forms
- Install https://www.nuget.org/packages/glidex.forms/ in your xamarin forms Android project
- Add this one liner after your app's
Forms.Initcall:
Xamarin.Forms.Forms.Init (this, bundle);
//This forces the custom renderers to be used
Android.Glide.Forms.Init ();
LoadApplication (new App ());
BOOM
Jou just achieved 90%+ memory reduction when manipulating Image views on both platforms.
Implementation details
Read the complete post on Sharpnado: https://www.sharpnado.com/ultimate-image-caching
I had an issue with the naming. Following Jonathan's convention was leading me to FFImageLoading.Forms, and well it's already taken.
So I leaned towards a more describing name:
FFImageLoading.ImageSourceHandler
Otherwise the project structure is the same that GlideX.Forms really. FFImageLoading supports the 3 Xamarin.Forms image sources.
[assembly: ExportImageSourceHandler(
typeof(FileImageSource), typeof(FFImageLoading.ImageSourceHandler))]
[assembly: ExportImageSourceHandler(
typeof(StreamImageSource), typeof(FFImageLoading.ImageSourceHandler))]
[assembly: ExportImageSourceHandler(
typeof(UriImageSource), typeof(FFImageLoading.ImageSourceHandler))]
namespace FFImageLoadingX
{
[Preserve (AllMembers = true)]
public class ImageSourceHandler : IImageSourceHandler
{
public Task<UIImage> LoadImageAsync(
ImageSource imageSource,
CancellationToken cancellationToken = new CancellationToken(),
float scale = 1)
{
return FFImageLoadingHelper.LoadViaFFImageLoading(
imageSource, cancellationToken);
}
}
}
And then FFImageLoadingHelper is calling the following methods based on the type of the ImageSource:
private static Task<UIImage> LoadImageAsync(string urlString)
{
return ImageService.Instance
.LoadUrl(urlString)
.AsUIImageAsync();
}
private static Task<UIImage> LoadFileAsync(string filePath)
{
return ImageService.Instance
.LoadFile(filePath)
.AsUIImageAsync();
}
private static Task<UIImage> LoadStreamAsync(StreamImageSource streamSource)
{
return ImageService.Instance
.LoadStream(token => ((IStreamImageSource)streamSource).GetStreamAsync(token))
.AsUIImageAsync();
}
Advantages
- It handles local files
- UIImage are correctly created (it respects scale factor)
- It's 100% C#
- The library is really small
FAQ
1. Why not just use FFImageLoading and its CachedImage ?
Well FFImageLoading is still in my opinion a really decent choice.
Nonetheless, glidex outperforms FFImageLoading on Android.
I ran several tests.
And GlideX is loading faster that FFImageLoading everytime.
This is from a cold start:
| GlideX | FFImageLoading |
|---|---|
![]() |
![]() |
On first run FFImageLoading has a smaller memory footprint. But after some back and forth it uses more memory compared to Glide. Glide clearly balances memory and loading speed, we can suspect some advanced memory management / speeding technics.
Also you can use regular Xamarin.Forms Image view instead of a custom view.
It really shines on existing projects: if you want to give a boost to your Xamarin.Forms app, you just have to install 2 nuget packages and BOOM -95% of memory used :)
2. Why not fork FFImageLoading and embed GlideX ?
It's not the philosophy of the lib. The lib is clearly a 100% C# lib, binding existing native library defeats the purpose.
3. Should I migrate from FFImageLoading to GlideX + FFIL.ImageSourceHandler
It really depends. If you have some issues with performance on Android you can give it a try. But if all is already working smoothly, I don't really see why you would do it.
SDWebImage: a discarded implementation
SDWebImage
I shamelessly took the Jonathan Peppers repo and just renamed everything with SDWebImage to have a real symetrical implementation.
|  |  |
| Any resemblance to real projects, living or dead, is purely coincidental | |
ImageSourceHandler vs IImageViewHandler
Thanks to Jonathan and since Xamarin.Forms 3.3.0, we have now a IImageViewHandler interface on the Android platform. It allows to directly deal with ImageView to implement the ImageSource on the platform.
[assembly: ExportImageSourceHandler(
typeof (FileImageSource), typeof (Android.Glide.ImageViewHandler))]
[assembly: ExportImageSourceHandler(
typeof (StreamImageSource), typeof (Android.Glide.ImageViewHandler))]
[assembly: ExportImageSourceHandler(
typeof (UriImageSource), typeof (Android.Glide.ImageViewHandler))]
namespace Android.Glide
{
[Preserve (AllMembers = true)]
public class ImageViewHandler : IImageViewHandler
{
public ImageViewHandler ()
{
Forms.Debug (
"IImageViewHandler of type `{0}`, instance created.",
GetType ());
}
public async Task LoadImageAsync(
ImageSource source,
ImageView imageView,
CancellationToken token = default(CancellationToken))
{
Forms.Debug(
"IImageViewHandler of type `{0}`, `{1}` called.",
GetType(),
nameof(LoadImageAsync));
await imageView.LoadViaGlide(source, token);
}
}
}
On the iOS side, we can deal directly with the IImageSourceHandler returning an UIImage.
[assembly: ExportImageSourceHandler(
typeof(UriImageSource), typeof(SDWebImage.Forms.ImageSourceHandler))]
namespace SDWebImage.Forms
{
[Preserve (AllMembers = true)]
public class ImageSourceHandler : IImageSourceHandler
{
public Task<UIImage> LoadImageAsync(
ImageSource imageSource,
CancellationToken cancellationToken = new CancellationToken(),
float scale = 1)
{
return SDWebImageViewHelper.LoadViaSDWebImage(
imageSource, cancellationToken);
}
}
}
SDWebImage has a simple api to download and cache UIImage.
private static Task<UIImage> LoadImageAsync(string urlString)
{
var tcs = new TaskCompletionSource<UIImage>();
SDWebImageManager.SharedManager.LoadImage(
new NSUrl(urlString),
SDWebImageOptions.ScaleDownLargeImages,
null,
(image, data, error, cacheType, finished, url) =>
{
if (image == null)
{
Forms.Debug("Fail to load image: {0}", url.AbsoluteUrl);
}
tcs.SetResult(image);
});
return tcs.Task;
}
However it doesn't support local files, only remote ones. That means that local resources are still processed by Xamarin.Forms.
public sealed class FileImageSourceHandler : IImageSourceHandler
{
public Task<UIImage> LoadImageAsync(
ImageSource imagesource,
CancellationToken cancelationToken = default(CancellationToken),
float scale = 1f)
{
UIImage image = null;
var filesource = imagesource as FileImageSource;
var file = filesource?.File;
if (!string.IsNullOrEmpty(file))
image = File.Exists(file)
? new UIImage(file)
: UIImage.FromBundle(file);
if (image == null)
{
Log.Warning(
nameof(FileImageSourceHandler),
"Could not find image: {0}",
imagesource);
}
return Task.FromResult(image);
}
}
Remark: I don't really know if it would be interesting to have also a IUIImageViewHandler interface on iOS. I don't know if it would improve the performance vs a UIImage cache.
Benchmarking!
Since all test pages use a mix of local resources and remote ones, and SDImageWeb only process remote files, I created a special page named GridOnlyRemotePage.
| Page | Data Type | Xamarin.Forms | SDWebImage |
|---|---|---|---|
| GridOnlyRemotePage | Remote only | 247828480 | 14229504 (-94%) |
| GridPage | Remote and local mix | 186748928 | 92033024 (-50%) |
| ViewCellPage | Remote and local mix | 36646912 | 18288640 (-50%) |
| ImageCellPage | Remote and local mix | 81604608 | 25874432 (-68%) |
| HugeImagePage | Local only | 124104704 | Same as XF (0%) |
Downsides
Of course the impact is huge for remote files (memory footprint of SDWebImage is 5% of Xamarin.Forms!) but at this point I was pretty disappointed:
SDWebImagedoesn't handle local resources- It doesn't take into account the scale (x2 per instance for retina) of the screen. So the
UIImageinstead of being scale 2 and size{Width=150, Height=150}was scale 1 and size{Width=300, Height=300} - The lib is HUGE (well of course it would have been linked, but still):


