18

I'm working on an app that uses large images (1390 × 870 : 150kb - 50kb). I'm adding images as I tap a trigger/ImageView.

At a certain point I'm getting an out of memory error:

java.lang.OutOfMemoryError
E/AndroidRuntime(23369): at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
E/AndroidRuntime(23369): at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:613)
E/AndroidRuntime(23369): at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:378)

To resize the image I'm doing this:

Bitmap productIndex = null;
final String imageLoc = IMAGE_LOCATION;
InputStream imageStream;
try {
     imageStream = new FileInputStream(imageLoc);
     productIndex = decodeSampledBitmapFromResource(getResources(), imageLoc, 400, 400);

     productIV.setImageBitmap(productIndex);
     } catch (FileNotFoundException e1) {
          // TODO Auto-generated catch block
          e1.printStackTrace();
     }
}


public static Bitmap decodeSampledBitmapFromResource(Resources res, String resId, int reqWidth, int reqHeight) {

// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(resId, options);

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

// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(resId, options);
}

public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;

if (height > reqHeight || width > reqWidth) {

    final int halfHeight = height / 3;
    final int halfWidth = width / 3;

    // Calculate the largest inSampleSize value that is a power of 2 and keeps both
    // height and width larger than the requested height and width.
    while ((halfHeight / inSampleSize) > reqHeight
            && (halfWidth / inSampleSize) > reqWidth) {
        inSampleSize *= 2;
    }
}

return inSampleSize;
}

I got this way of resizing to save space from the Android Docs: Loading Large Bitmaps Efficiently

According to the log this like is the culprit in the decodeSampledBitmapFromResource method :

return BitmapFactory.decodeFile(resId, options);

----- edit ----- Here is how I'm adding each item to the FrameLayout.

for(int ps=0;ps<productSplit.size();ps++){
    //split each product by the equals sign
    List<String> productItem = Arrays.asList(productSplit.get(ps).split("="));

    String tempCarID = productItem.get(0);
    tempCarID = tempCarID.replace(" ", "");
    if(String.valueOf(carID).equals(tempCarID)){

        ImageView productIV = new ImageView(Configurator.this);
        LayoutParams productParams = new LayoutParams(
                LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        productIV.setId(Integer.parseInt(partIdsList.get(x)));
        productIV.setLayoutParams(productParams);

        final String imageLoc = productItem.get(2);

        InputStream imageStream;
        try {
            imageStream = new FileInputStream(imageLoc);
            productIndex = decodeSampledBitmapFromResource(getResources(), imageLoc, 400, 400);
            productIV.setImageBitmap(productIndex);
        } catch (FileNotFoundException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }

        productLayers.addView(productIV);

    }
}
dcp3450
  • 10,959
  • 23
  • 58
  • 110
  • Once you've loaded a sampled bitmap, do you ever remove it from your View/Adapter when it is no longer visible? – Morrison Chang Jan 27 '14 at 22:27
  • it's always visible. I'm running a loop to add items to a FramedLayout. I'll updated my question to show this. – dcp3450 Jan 27 '14 at 22:29
  • I've updated the question. If I recycle the productIndex Bitmap because it destroys the image and not in my FrameLayout anymore. – dcp3450 Jan 27 '14 at 22:36
  • But visually what does this look like, are you overlapping bitmaps (which could be wasteful)? How many bitmaps are you loading up? – Morrison Chang Jan 27 '14 at 22:40
  • Yes, I'm having to overlay the bitmaps. This layers images to create a solid image that the user will them send to themselves or others. The user will be able to add as many as needed... – dcp3450 Jan 27 '14 at 22:47
  • Is this worth looking into: [Caching Bitmaps](http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html) – dcp3450 Jan 27 '14 at 22:59
  • I appreciate you sharing the code from your efforts, it was very helpful as I faced the same issue but did not have all the code there that I needed.. calculateInSampleSize() in particular. Thanks! – Gene Bo May 22 '14 at 17:31

6 Answers6

30

You can use another bitmap-config to heavily decrease the size of the images. The default is RGB-config ARGB8888 which means four 8-bit channels are used (red, green, blue, alhpa). Alpha is transparency of the bitmap. This occupy a lot of memory - imagesize X 4. So if the imagesize is 4 megapixel 16 megabytes will immidiately be allocated on the heap - quickly exhausting the memory.

Instead - use RGB_565 which to some extent deteriorate the quality - but to compensate this you can dither the images.

So - to your method decodeSampledBitmapFromResource - add the following snippets:

 options.inPreferredConfig = Config.RGB_565;
 options.inDither = true;

In your code:

 public static Bitmap decodeSampledBitmapFromResource(Resources res, String resId, int    reqWidth, int reqHeight) {

 // First decode with inJustDecodeBounds=true to check dimensions
 final BitmapFactory.Options options = new BitmapFactory.Options();
 options.inJustDecodeBounds = true;
 BitmapFactory.decodeFile(resId, options);

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

 // Decode bitmap with inSampleSize set
 options.inJustDecodeBounds = false;
 options.inPreferredConfig = Config.RGB_565;
 options.inDither = true;
 return BitmapFactory.decodeFile(resId, options);
 }

References:

http://developer.android.com/reference/android/graphics/Bitmap.Config.html#ARGB_8888

Björn Hallström
  • 3,775
  • 9
  • 39
  • 50
25

High resolution devices such as S4 usually run out of memory if you do not have your image in the proper folder which is drawable-xxhdpi. You can also put your image into drawable-nodpi. The reason it would run out of memorey if your image just in drawable that the android would scale the image thinking that the image was designed for low resolution.

serge
  • 1,590
  • 2
  • 26
  • 49
  • the images being used are downloaded via an API and stored in the device sd storage – dcp3450 Jan 27 '14 at 22:50
  • It is still possible that they are getting adjusted for high res, maybe you can dig in that direction, I'll research as well and let you know what I find out – serge Jan 27 '14 at 22:53
  • as far as the image themselves: they are 72 dpi and compressed via photoshop using "export for web and devices". The iOS and Android app both use the same API and Images. The reason images are so big is to account for Retina. – dcp3450 Jan 27 '14 at 22:56
  • Is this worth looking into: [Caching Bitmaps](http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html) – dcp3450 Jan 27 '14 at 23:00
  • You guys don't have to look if you don't think this is the problem, but I'm going to research. In my experience every time I had oom related to images was the problem I described above – serge Jan 27 '14 at 23:03
  • I'l look into the link you posted after I research res issue. – serge Jan 27 '14 at 23:06
  • Saved me too. Best solution! should be the accepted answer. – Ajit Aug 04 '16 at 13:57
  • @serge - I was stunned to discover that you are correct that `drawable` is treated as if it were `drawable-mdpi`, and images there are scaled by phone density. Puzzling decision by Android designers, but essential to know! – ToolmakerSteve Sep 19 '18 at 17:51
6

You can use this beautiful library https://github.com/davemorrissey/subsampling-scale-image-view

Rohit Arya
  • 6,751
  • 1
  • 26
  • 40
3

Here is how I'm adding each item to the FrameLayout that's the problem, the code keep adding and adding more images, and doesn't matter how well you resize or how much memory the device have, at certain point it WILL run out of memory. That's because every image you add it's keeping in memory.

For this type of situation what the apps do is to use a ViewGroup that can recycle views. I don't know your layout, but usually is a ListView, GridView or a ViewPager, by recycling views you re-use the layout and can dispose re-load images as necessary.

For the specific purpose of loading and resizing images I strongly advise use Picasso library as it is VERY well written, simple to use and stable.

Budius
  • 39,391
  • 16
  • 102
  • 144
  • Yeah, that's what I thought was happening. Basically, I have a frame layout with sets of images (all the same size) that need to lay on top of each other. For each image I'm using an ImageView. I thought about just keeping an ArrayList of all my image locations and everytime a new layer is added flatten all the layers into one bitmap image instead of add new layers for each image. – dcp3450 Jan 27 '14 at 23:16
  • that probably would work. But if you just keep adding them, it will always run out of memory. – Budius Jan 27 '14 at 23:28
0

You are still going to need to manage the bitmap memory as I wouldn't try to allocate a total space more than 3x the size of the screen (if you think about it makes sense for scrolling behavior). If you are overlaying one image on top of another, at some point, you're hitting an Out Of Memory error. You may need to look at capturing the prior screen image as a single background image to make sure you still fit within the available memory. Or when a new image overlaps an existing image only load and render the visible portion. If performance becomes an issue, then you may need to consider OpenGL Textures but the memory allocation problem is still the same.

Do go through all of the Displaying Bitmaps Training as it should give you some additional ideas of how to handle display.

Morrison Chang
  • 11,691
  • 3
  • 41
  • 77
  • I thought about taking a "current state" of the FrameLayout -> flattening the Bitmaps into one image -> replace the current view. The flattened image would get bigger though. but I wouldn't just be stacking layers like I am now. Does that sound more doable or is roughly the same thing? – dcp3450 Jan 27 '14 at 23:09
  • It sounds like what I was suggesting, I would still keep in mind that you don't have an arbitrary large bitmap memory budget. If the background image is scrollable, then you'll have to even play more games to keep it within budget or change the limits the of the app. – Morrison Chang Jan 27 '14 at 23:13
  • Nothing is scrollable. I eventually have to push this to an API so I'll have to flatten it anyhow. – dcp3450 Jan 27 '14 at 23:18
  • Then at most your background image would be one screen size big and you'll be well under 3x provided everything else is unloaded. – Morrison Chang Jan 27 '14 at 23:19
  • So, based on these comments does this sound better: load base layers into ArrayList -> Flatten layers -> place into layout as one bitmap... user adds layer: add new layer to ArrayList -> remove current bitmap -> Flatten layers again -> place new bitmap into layout. Just replace the bitmap area with a progress bar as it flattens the image. – dcp3450 Jan 27 '14 at 23:25
  • Only you can determine if it works for you, i.e. is an undo function a requirement, does flattening and caching the new background image take too long, and other requirements not mentioned here. As a first step though I would try it. – Morrison Chang Jan 27 '14 at 23:32
0

Use Fresco library to load large images will avoid this error. in xml layout

<com.facebook.drawee.view.SimpleDraweeView
    android:id="@+id/my_image_view"
    android:layout_width="1300dp"
    android:layout_height="1300dp"
    fresco:placeholderImage="@drawable/my_drawable"
  />

and in javacode

Uri uri = Uri.parse("https://image.png");
SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view);
draweeView.setImageURI(uri);
lee
  • 161
  • 2
  • 5