2
\$\begingroup\$

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;
        }
    }
}
\$\endgroup\$
0
3
\$\begingroup\$

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);
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.