Cross Platform
Android
iOS
Test Cloud

Load Large Bitmaps Efficiently

This recipe shows you how you can load large images into memory without the application throwing an OutOfMemoryException by loading a smaller subsampled version in memory.

Recipe

Images come in all shapes and sizes. In many cases they are larger than required for a typical application user interface (UI). For example, the system Gallery application displays photos taken using your Android device's camera which are typically much higher resolution than the screen density of your device.

Given that you are working with limited memory, ideally you only want to load a lower resolution version in memory. The lower resolution version should match the size of the UI component that displays it. An image with a higher resolution does not provide any visible benefit, but still takes up precious memory and incurs additional performance overhead due to additional on the fly scaling.

Read Bitmap Dimensions and Type

The BitmapFactory class provides several decoding methods ( DecodeByteArray , DecodeFile , DecodeResource , etc.) for creating a Bitmap from various sources. Choose the most appropriate decode method based on your image data source. These methods attempt to allocate memory for the constructed bitmap and therefore can easily result in an OutOfMemoryException . Each type of decode method has additional signatures that let you specify decoding options via the BitmapFactory.Options class. 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.

var options = new BitmapFactory.Options {
    InJustDecodeBounds = true,
};
// BitmapFactory.DecodeResource() will return a non-null value; dispose of it.
using (var dispose = BitmapFactory.DecodeResource(Resources, Resource.Id.myimage, options)) {
}
var imageHeight = options.OutHeight;
var imageWidth  = options.OutWidth;
var imageType   = options.OutMimeType;

To avoid OutOfMemoryException s, check the dimensions of a bitmap before decoding it, unless you absolutely trust the source to provide you with predictably sized image data that comfortably fits within the available memory.

Load a Scaled Down Version into Memory

Now that the image dimensions are known, they can be used to decide if the full image should be loaded into memory or if a subsampled version should be loaded instead. Here are some factors to consider:

  • Estimated memory usage of loading the full image in memory.
  • Amount of memory you are willing to commit to loading this image given any other memory requirements of your application.
  • Dimensions of the target ImageView or UI component that the image is to be loaded into.
  • Screen size and density of the current device.

    For example, it’s not worth loading a 1024x768 pixel image into memory if it will eventually be displayed in a 128x96 pixel thumbnail in an ImageView .

    To tell the decoder to subsample the image, loading a smaller version into memory, set InSampleSize to true in your BitmapFactory.Options object. For example, an image with resolution 2048x1536 that is decoded with an InSampleSize of 4 produces a bitmap of approximately 512x384. Loading this into memory uses 0.75MB rather than 12MB for the full image (assuming a bitmap configuration of Argb8888 ). Here’s a method to calculate a the sample size value based on a target width and height:

 

public static int CalculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight)
{
    // Raw height and width of image
    var height = (float)options.OutHeight;
    var width = (float)options.OutWidth;
    var inSampleSize = 1D;

    if (height > reqHeight || width > reqWidth)
    {
        inSampleSize = width > height
                            ? height/reqHeight
                            : width/reqWidth;
    }

    return (int) inSampleSize;
}

Note: Using powers of 2 for InSampleSize values is faster and more efficient for the decoder. However, if you plan to cache the resized versions in memory or on disk, it’s usually still worth decoding to the most appropriate image dimensions to save space.

To use this method, first decode with InJustDecodeBounds set to true , pass the options through and then decode again using the new inSampleSize value and InJustDecodeBounds set to false:

public static Bitmap DecodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight)
{
    // First decode with inJustDecodeBounds=true to check dimensions
    var options = new BitmapFactory.Options {
        InJustDecodeBounds = true,
    };
    using (var dispose = BitmapFactory.DecodeResource(res, resId, options)) {
    }

    // Calculate inSampleSize
    options.InSampleSize = CalculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.InJustDecodeBounds = false;
    return BitmapFactory.DecodeResource(res, resId, options);
}

This method makes it easy to load a bitmap of arbitrarily large size into an ImageView that displays a 100x100 pixel thumbnail, as shown in the following example code:

imageView.SetImageBitmap(DecodeSampledBitmapFromResource(Resources, Resource.Id.myimage, 100, 100));

You can follow a similar process to decode bitmaps from other sources, by substituting the appropriate BitmapFactory.DecodeXXX method as needed.

As both the Mono and Java runtimes have garbage collectors, the lifetime of bitmaps can sometimes be much longer than actually needed. The is because both the Garbage Collectors have to mark the bitmap for disposal before it will actually collected. 

In order to help the GCs, you can tell the Mono GC that it no longer needs to keep a reference to the bitmap. This can be done by disposing of the Mono object. This does not dispose of the actual image, but rather only the reference or link to the Java object. This leaves the Java GC free to dispose of the bitmap when it is no longer in use.

using (var bmp = DecodeSampledBitmapFromResource(Resources, Resource.Id.myimage, 100, 100))
    imageView.SetImageBitmap(bmp);

If you are finished using an image or bitmap, then you can also inform the Java GC that this particular bitmap is free to be disposed of. This can be done by calling Recycle on the bitmap, which will free the native object and clear the reference to the pixel data.

using (var bmp = DecodeSampledBitmapFromResource(Resources, Resource.Id.myimage, 100, 100))
{
    imageView.SetImageBitmap(bmp);

    // dispose of the actual image bytes
    bmp.Recycle();
}