The Challenge
Did you ever encounter one of these errors while processing large images on Android?
OutofMemoryError: bitmap size exceeds VM budget
If you have done a fair amount of image work on Android, I am sure you did. This is one of the most common errors when working with images.
Why does it happen?
The Android Java API requires loading the entire image into memory for processing. Unfortunately sometimes there is not enough memory to load the entire image into memory, so the operation fails.
Whether or not there is sufficient memory depends on a variety of factors including:
- The amount of heap memory allocated for the process. This differs from device to device and can be as little as 16MB on the original TMobile G1 and as much as 64 MB or more on more recent models. The amount of heap memory is set in the device’s firmware.
- The amount of memory usage within the same process. If your app already loaded a lot of bitmaps, then there may not be sufficient memory to load additional bitmaps, and some old bitmaps may need to be removed from memory to do so.
- The amount of fragmentation in the heap. The total number of bytes required to load your image may exist on the heap, but the heap memory may be fragmented, meaning that the free memory is split into several chunks. If no sufficiently large contiguous area can fit your image, then allocation will fail.
How to fix it
This article assumes that you can sacrifice a bit of image resolution in order to complete processing using the Java API, which is memory bound. If this is not acceptable, then a more complex solution such as using a C/C++ library and the NDK is probably appropriate.
This article also assumes that you are managing memory correctly. Improper memory management such as progressively leaking images when orientation changes or failing to unload adapter images that are not displayed on screen can also substantially reduce the available memory and lead to frequent OutOfMemoryError conditions.
The Solution
To get rid of the OutOfMemoryError errors we will reduce the size of the image that is loaded in the memory, sacrificing some image resolution in the process.
A natural question at this point is: what is the proper size that will guarantee that the image will fit into memory? Unfortunately there is no magic size that is guaranteed to work on every device — each device has varying amounts of memory.
Faced with this, it may be tempting to try to determine the amount of remaining free memory. Unfortunately this is not likely to be successful. The Java API makes it difficult to get a reliable number. Even if you could get a reliable number, there is no guarantee that the available free memory is contiguous (see heap fragmentation issue above).
A better approach is to rely on brute force. Repeatedly attempt to load the image into memory and progressively use a smaller image size if the load fails, until the load finally succeeds. A failed load will tend to fail fast, because allocating memory is one of the first steps in processing.
This algorithm is demonstrated below in the context of rotating an image. We increment inSampleSize with each iteration to reduce the image size, and continue as for as long as an OutOfMemoryError occurs. If your program is managing memory properly and you don’t have other large images loaded into memory, this approach requires no more than 3 iterations.
private boolean rotateBitmap(File inFile, File outFile, int angle) throws FileNotFoundException, IOException { // Declare FileInputStream inStream = null; FileOutputStream outStream = null; // Create options BitmapFactory.Options options = new BitmapFactory.Options(); // Create transform matrix Matrix matrix = new Matrix(); matrix.postRotate(angle); // Increment inSampleSize progressively to reduce image resolution and size. If // the program is properly managing memory, and you don't have other large images // loaded in memory, this loop will generally not need to go through more than 3 // iterations. To be safe though, we stop looping after a certain amount of tries // to avoid infinite loops for (options.inSampleSize = 1; options.inSampleSize <= 32; options.inSampleSize++) { try { // Load the bitmap from file inStream = new FileInputStream(inFile); Bitmap originalBitmap = BitmapFactory.decodeStream(inStream, null, options); // Rotate the bitmap Bitmap rotatedBitmap = Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(), originalBitmap.getHeight(), matrix, true); // Save the rotated bitmap outStream = new FileOutputStream(outFile); rotatedBitmap.compress(CompressFormat.JPEG, 100, outStream); outStream.close(); // Recycle the bitmaps to immediately free memory originalBitmap.recycle(); originalBitmap = null; rotatedBitmap.recycle(); rotatedBitmap = null; // Return return true; } catch (OutOfMemoryError e) { // If an OutOfMemoryError occurred, we continue with for loop and next inSampleSize value } finally { // Clean-up if we failed on save if (outStream != null) { try { outStream.close(); } catch (IOException e) { } } } } // Failed return false; }
This approach of trapping the OutOfMemoryError and progressively reducing image resolution can also work for other transforms using the Java API such as hue changes, greyscale conversions and more.
Conclusion
Hopefully this article is helpful in helping you process large images using the Java API. Let us know about any questions or comments in the section below.