Sunday 13 May 2012

Solving the "bitmap size exceeds VM budget error"

java.lang.OutOfMemoryError: bitmap size exceeds VM budget.
This is one phrase you'll come across frequently in android while you try to load several large bitmaps in your program.And Why?Because the heap size available to an application is limited and large bitmaps are the easiest way of exhausting it.
So how do you work around it?
Understand that mostly,it is pointless to load a bitmap in its full capacity onto a device's screen that fits on your palm.So,all you have to do is to load a scaled down version of the bitmap and everything will be well and good.But how do you scale down a bitmap for loading? Configure and use the BitmapFactory.Options class while decoding the bitmap .
Here,for simplicity,I'll be loading a bitmap on to a single ImageView.

In simple words,here's what they mean:
1. BitmapFactory.Options.inJustDecodeBounds: When set to true,we will be able to measure the bitmap dimensions,without actually loading the bitmap into memory.
2.BitmapFactory.Options.inSampleSize: The value of this int allows us to load a scaled down version of the bitmap into memory. As an example,if the value set to this variable is 2, the resulting bitmap will have a width half the original width and height set to half the original height.
That is,
        New width= original width/2
        New Height= original height/2.

Here,we'll be dedicating a class for loading the bitmap and in the class here's what we are going to do:
1. Measure the actual height and width of the bitmap by setting inJustDecodeBounds=true;
2. Obtain an appropriate scale(which we will set to inSampleSize),using the actual height and width.
3. Load the scaled down bitmap using the inSampleSize.


Note that this article is an implementation of the guidelines given here.


As I have said,my main.xml contains only a single ImageView,for the purpose of demonstration.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <ImageView 
        android:id="@+id/bitmapView"
        android:layout_width="100dp"
        android:layout_height="100dp"/>

</LinearLayout>



Now for the code,read the comments and you will find it self explanatory.


package com.blog.simplebitmaploader;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Environment;
import android.widget.ImageView;

public class BitmapLoaderActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        //path of the file to be loaded
        String filePath=Environment.getExternalStorageDirectory()+
          "/Sunset_Beach.jpg";
  
        setContentView(R.layout.main);
        ImageView bitmapView=(ImageView) findViewById(R.id.bitmapView);
        
        //load the scaled down version
        bitmapView.setImageBitmap(BitmapLoader.loadBitmap(filePath, 100, 100));
    }
}

class BitmapLoader
{
  public static int getScale(int originalWidth,int originalHeight,
       final int requiredWidth,final int requiredHeight)
  {
 //a scale of 1 means the original dimensions 
 //of the image are maintained
 int scale=1;
       
 //calculate scale only if the height or width of 
 //the image exceeds the required value.
 if((originalWidth>requiredWidth) || (originalHeight>requiredHeight)) 
   {
       //calculate scale with respect to
    //the smaller dimension
    if(originalWidth<originalHeight)
   scale=Math.round((float)originalWidth/requiredWidth);
    else
   scale=Math.round((float)originalHeight/requiredHeight);

    }
      
  return scale;
  }
 
  public static BitmapFactory.Options getOptions(String filePath,
    int requiredWidth,int requiredHeight)
   {
  
 BitmapFactory.Options options=new BitmapFactory.Options();
 //setting inJustDecodeBounds to true
 //ensures that we are able to measure
 //the dimensions of the image,without
 //actually allocating it memory
 options.inJustDecodeBounds=true;
  
 //decode the file for measurement
 BitmapFactory.decodeFile(filePath,options);
  
 //obtain the inSampleSize for loading a 
 //scaled down version of the image.
 //options.outWidth and options.outHeight 
 //are the measured dimensions of the 
 //original image
 options.inSampleSize=getScale(options.outWidth,
   options.outHeight, requiredWidth, requiredHeight);
  
 //set inJustDecodeBounds to false again
 //so that we can now actually allocate the
 //bitmap some memory
 options.inJustDecodeBounds=false;
  
 return options;
  
  }
 
 
 
 public static Bitmap loadBitmap(String filePath,
   int requiredWidth,int requiredHeight){
 

  BitmapFactory.Options options= getOptions(filePath,
     requiredWidth, requiredHeight);
  
  return BitmapFactory.decodeFile(filePath,options);
 }
}



Simple,right?? The only disadvantage of this code is that everything related to the bitmap is being done in the UI thread,which could slow down your application.In my next post,we'll see how this code can be executed in a different thread.For now,adios..:):):).



7 comments:

  1. The case you describe here allows you to work with smaller versions of the image(s) you need. But what about the case you need to load an image from camera folder (8 MP camera on galaxy), to process the image (let's say adding a border and a piece of text on it) and sending to a webservice. This is a scenario I have in one of my applications and I'm a bit stuck on this error.
    I got Out of memory exception in the case above, and I have one more scenario: I need to rotate the image 15 degree and to build another image - bigger than the image I need to rotate).
    How do you work in those cases?

    ReplyDelete
    Replies
    1. hey zeltera..i'm sorry but I have'nt worked on any kind of image processing on android yet..
      In my case I encountered this error while loading images from various sources onto a ListView..I'l be glad if you have worked around your problem..
      n yeah..sorry for the late reply..been out of touch with the world for months..;-)

      Delete
  2. Thanks, I was looking for a way to do this without loading the Bitmap into memory!

    One suggestion though. In the getScale function you should calculate both the width and height scales and use Math.max or Math.min, for cropping or filling a view. Your example will show weird behavior if you try to display a landscape image in portrait view and vice versa.

    ReplyDelete
    Replies
    1. thanx for the suggestion dude..
      my idea was to compute a scale using the the smaller dimension..i guess your suggestion fits too..:)

      Delete
  3. Thank you very much. Helped us to fix a long standing issue.
    Prasanth Kumar.S

    ReplyDelete
  4. If OOM was caused by loading an 8 mb image, this solution will work as I will essentially load scaled down bitmap, say, of 2 mb. But what if I load several images of 2 mb in size? OOM will still occur. How to resolve that??

    ReplyDelete
    Replies
    1. I suppose you'll have to find a balance between your requirement and the memory available by tuning the bitmap options.

      Delete