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..:):):).