Ultimate Image Caching for Xamarin.Forms

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: Nuget

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).

imaginary_friend

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:

  1. https://github.com/onevcat/Kingfisher
  2. https://github.com/kean/Nuke

Unfortunately I couldn't find a Xamarin binding for them...
And since binding Swift libraries is sooo painful, we just have to be patient:

swift-o-matic

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:

  1. Launch simulator
  2. Wait 4-5 seconds on MainPage
  3. Launch a Page
  4. Scroll till the end of page
  5. 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:

  1. SDWebImage doesn't handle local resources
  2. 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}
  3. The lib is HUGE (well of course it would have been linked, but still):

sdwebimage wtf

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.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

  1. It handles local files
  2. UIImage are correctly created (it respects scale factor)
  3. It's 100% C#
  4. It's really small

ffimageloading small size

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.