Ultimate Image Caching for Xamarin.Forms
WARNING: obsolete package
The FFImageLoading.ImageSourceHandler
nuget package has been discontinued.
Use the Xamarin.Forms.Nuke
package instead which has better performance.
https://github.com/roubachof/Xamarin.Forms.Nuke | |
See this blog post: https://www.sharpnado.com/xamarin-forms-nuke/
Abstract and Nuget
The FFImageLoading.ImageSourceHandler
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).
You can find the source code of the FFImageLoading.ImageSourceHandler
here:
https://github.com/roubachof/Xamarin.Forms.ImageSourceHandlers
A nuget package is also at your disposal FFImageLoading.ImageSourceHandler
:
A compelling itch
All started with a conversation on the xamarin slack with my good friend Simon Hoade (well we just exchanged three sentences but I like to have imaginary friends: I was a lonely child).
So as my best friend Simon said, the work made by my future friend Jonathan Peppers (his name is so cool, it sounds like a glorious adventure game on Atari ST) is truely great !
I am always using Glide
or Picasso
when I am working on Xamarin.Android
and the performance is amazing. It makes image handling on Android
so easy (and we all know that image handling is so painful on Android
).
So now we can use this amazing native library on Android
, but what about iOS
?
iOS image caching native libraries
I started to make my research on iOS
, the two most quoted libs are:
Unfortunately I couldn't find a Xamarin
binding for them...
And since binding Swift libraries is sooo painful, we just have to be patient:
So what iOS
library is bound for quite some time now ?
The good old SDWebImage of course!
SDWebImage
I shamelessly took the Jonathan Peppers (being such a nice future friend I'm sure he'll forgive me) 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);
}
}
}
The SDWebImage
lib 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!
Protocol
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.
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
.
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 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
Results
Page | Data Type | Xamarin.Forms | SDWebImage |
---|---|---|---|
GridOnlyRemotePage | Remote only | 247 828 480 | 14 229 504 (-94%) |
GridPage | Remote and local mix | 186 748 928 | 92 033 024 (-50%) |
ViewCellPage | Remote and local mix | 36 646 912 | 18 288 640 (-50%) |
ImageCellPage | Remote and local mix | 81 604 608 | 25 874 432 (-68%) |
HugeImagePage | Local only | 124 104 704 | 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:
SDWebImage
doesn't handle local resources- It doesn't take into account the scale (x2 per instance for retina) of the screen. So the
UIImage
instead 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):
FFImageLoading
Of course we all know the famous FFImageLoading
library, and I can't thank enough Luberda and Molinet (well especially Luberda: 1140 commit vs 329 :) for creating it. Xamarin.Forms
wouldn't really be production ready without it.
I didn't consider it first cause I was thinking that a well known iOS
native library will surely achieve the best performance.
Implementation
Here 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 SDWebImage
really. But FFImageLoading
supports the 3 Xamarin.Form
s 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#
- It's really small
Benchmark Results
Page | Data Type | Xamarin.Forms | FFImageLoading |
---|---|---|---|
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%) |
SDWebImage
wins by a decent percentage against FFImageLoading
(-24%) on remote images (and this is the main usage of this type of lib).
Unfortunately the listed drawbacks are too much to justify its usage.
Maybe when the Swift-o-matic will be released I will be able to have the perfect solution.
But for now FFImageLoading
as a ImageSourceHandler
for Xamarin.Forms
seems to me the best solution.
But, but, but, WHY ?
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.
4. Why can't you have real friends ?
Cause I make terrible jokes.