This code addresses performance issues specific to Android running Xamarin / C# when downloading images (BMP/PNG) into a listview. The caller simply specifies the URL to fetch the image from, and the preferred dimensions.
Optimisations I could do, but haven't:
- Not sure if I should use the Android or Java namespace
- Stage the downloaded
byte[]
in an intermediate location - not sure if memory exhaustion is possible - Image caching
- Convert to an Android
Drawable
- Abstract the source to be something other than a URL, e.g. the local resources
- Make a static "helper" class (not sure how to do this...)
using System;
using System.Threading.Tasks;
using Android.Graphics;
using Android.Content.Res;
using System.Net;
namespace validAndroid
{
public class ImageUtils
{
async Task<BitmapFactory.Options> GetBitmapOptionsOfImageAsync(byte[] imageBytes)
{
BitmapFactory.Options options = new BitmapFactory.Options {
/*Setting the InJustDecodeBounds property to true while decoding avoids memory allocation,
* returning null for the bitmap object but setting OutWidth, OutHeight and OutMimeType .
* This technique allows you to read the dimensions and type of the image data prior to
* construction (and memory allocation) of the bitmap.*/
InJustDecodeBounds = true
};
// The result will be null because InJustDecodeBounds == true.
Bitmap result = await BitmapFactory.DecodeByteArrayAsync (imageBytes, 0, imageBytes.Length -1, options);
int imageHeight = options.OutHeight;
int imageWidth = options.OutWidth;
System.Diagnostics.Debug.WriteLine (string.Format ("Original Size= {0}x{1}", imageWidth, imageHeight));
return options;
}
static int CalculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight)
{
// Raw height and width of image
float height = options.OutHeight;
float width = options.OutWidth;
double inSampleSize = 1D;
if (height > reqHeight || width > reqWidth)
{
int halfHeight = (int)(height / 2);
int halfWidth = (int)(width / 2);
// Calculate a inSampleSize that is a power of 2 - the decoder will use a value that is a power of two anyway.
while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth)
{
inSampleSize *= 2;
}
}
return (int)inSampleSize;
}
async Task<Android.Graphics.Bitmap> LoadScaledDownBitmapForDisplayAsync(byte[] imageBytes, BitmapFactory.Options options, int reqWidth, int reqHeight)
{
// Calculate inSampleSize
options.InSampleSize = CalculateInSampleSize (options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.InJustDecodeBounds = false;
return await Android.Graphics.BitmapFactory.DecodeByteArrayAsync (imageBytes,0,imageBytes.Length-1, options);
}
public async Task<Bitmap> GetImageForDisplay(string imageURL,int reqWidth, int reqHeight )
{
byte[] imageBytes = null;
using (var webClient = new WebClient())
{
imageBytes= webClient.DownloadData(imageURL);
}
BitmapFactory.Options options = await GetBitmapOptionsOfImageAsync(imageBytes);
var bitmapToDisplay = await LoadScaledDownBitmapForDisplayAsync ( imageBytes,options, reqWidth, reqHeight);
imageBytes = null;
return bitmapToDisplay;
}
}
}
1 Answer 1
WebClient.DownloadData
is a blocking method; you should use DownloadDataTaskAsync
. Network I/O is almost certainly the slowest part of the whole operation, so it's important that we're not blocking here. You might also want to consider using the newer HttpClient
instead of WebClient
.
The third parameter of DecodeByteArrayAsync
is the length of the array, but you're passing it imagesBytes.Length - 1
. I think you want to pass it imageBytes.Length
instead.
There's no need to set imageBytes
to null
.
I don't really like that LoadScaledDownBitmapForDisplayAsync
modifies the options
parameter. Since the method is private it's not that important, but it is, to me, surprising behaviour. To avoid this, I would consider merging GetBitmapOptionsOfImageAsync
and LoadScaledDownBitmapForDisplayAsync
into one method, e.g:
public static async Task<Bitmap> DecodeByteArrayAsync(byte[] imageBytes, int requiredWidth, int requiredHeight)
{
var options = new Options { InJustDecodeBounds = true };
await BitmapFactory.DecodeByteArrayAsync(imageBytes, 0, imageBytes.Length, options);
options.InSampleSize = CalculateInSampleSize(options, requiredWidth, requiredHeight);
options.InJustDecodeBounds = false;
return await BitmapFactory.DecodeByteArrayAsync(imageBytes, 0, imageBytes.Length, options);
}