0

first the important lines of code:

Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), getDrawableForLvl(drawable));
int []pixels = new int[bitmap.getWidth()*bitmap.getHeight()];
bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
//..
pixels = null; // please gc remove this huge data
System.gc();

So I'm working on an Android app (game) with multiple levels. Every level is an image that I load as bitmap on level start. Then I have to analyse every pixel (line 3). For that I create an array (line 2). The images are 1280x800, so the array has a size over one million ints. No problems here and since it's a local variable it should be destroyed on method return. But it's java, so it's not -.- Depending on the device the garbage collector is running fast enough in time or not. So when a user starts and closes levels very fast it produces a java.lang.OutOfMemoryError in line 2. I guess because the old array(s) wasn't/weren't removed yet and now I have multiple ones filling the memory.

I could make the array a static member. So it's created once and is always available. Then it's not possible to have multiple instances of it. But I think that's a bad coding style, because it's also available (4 MB) when not needed.

I don't need all pixels at the same time. I'm splitting the images in more than a hundred rectangles, so I could use a way smaller array and fill it one after another with pixels. But I have problems to understand the methods parameters can someone help me here? There is also a method to get just one pixel at position x,y, but I guess a million function calls is pretty slow.

Has someone a better idea? There is no way to force an object out of memory in java, is there?


Update1: As vzoha suggested to get only the first quarter:

int []pixels = new int[bitmap.getWidth()/2*bitmap.getHeight()/2];
bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth()/2, bitmap.getHeight()/2);

gives an ArrayIndexOutOfBound. I think the function call is just getting the pixels of the first quarter, but still expects the full size array and the other fields will be left untouched.

Update2: I guess I can do it row by row (half row by half row for the first quarter):

int []pixels = new int[bitmap.getWidth()/2*bitmap.getHeight()/2];
for(int row = 0; row < bitmap.getHeight()/2; ++row)
    bitmap.getPixels(pixels, bitmap.getWidth()/2, bitmap.getWidth(), 0, row, bitmap.getWidth()/2, 1);

But when I do that for 20x10 parts it's not much better than getting each pixel by itself. Well it is much better but still the method should be capable to do that with one call, shouldn't it? I just don't get this "stride" parameter: "The number of entries in pixels[] to skip between rows (must be >= bitmap's width). Can be negative." How can it be negativ when >= width?

5
  • I had problems with bitmaps and outOfMemoryError too. Try to watch Heap in DDMS if is increasing, or if decrease after bitmap is not used. For me helped calling bitmap.recycle(); bitmap = null;, but I didn't use getPixels. Try it and let me note. Commented Oct 26, 2014 at 18:07
  • Well the error is on the declaration of the huge array. But I guess the Bitmap should be even bigger in memory size, so it's a good advice. I will try to recyle the bitmap when a level is closed, so it should be a bigger free memory buffer for the next level. bitmap.recycle is what I need for the array, because I can't rely on the garbage collector to run when it feels like it. Commented Oct 26, 2014 at 18:49
  • I just noticed: recycle will not free the pixel data, it simply allows it to be garbage collected. So it doesn't really make a difference since I only use recycle when I'm abandon the object that holds the bitmap, so it's available for the GC no matter whether I call recycle or not. Commented Oct 26, 2014 at 19:00
  • Ok, for me helped on some devices also imageView.setImageDrawable(null);, but this is not your problem. As you told, you could use bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); for part of image. I think, that for first quarter it should be: bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth()/2, bitmap.getHeight()/2); and for second quarter: bitmap.getPixels(pixels, 0, bitmap.getWidth(), bitmap.getWidth()/2, 0, bitmap.getWidth(), bitmap.getHeight()/2); and so one. Commented Oct 26, 2014 at 19:10
  • That's what I already tried, because it makes total sense, but it doesn't work. (I added it to the description of my question) Commented Oct 26, 2014 at 19:38

2 Answers 2

1

The size in pixels doesn't directly translate to how much memory the image will take up in memory. Bitmaps in Android (before using ART) are notoriously difficult to use heavily while avoiding OOM exceptions, enough so, that there's a page dedicated to how to use them efficiently. The problem is normally that there is actually enough memory available, but it has become fragmented and there isn't a single contiguous block the size you need available.

My first suggestion (for a quick win) would be to decode the bitmap with a specific config, you should be able to occupy only 1/4 of the amount of memory you were previously using by switching to use ALPHA_8 for your bitmap config. This will use 1 byte per pixel instead of 4 (the default is ARGB_8888)

BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ALPHA_8
bitmap = BitmapFactory.decodeResource(getResources(), getDrawableForLvl(drawable), options);

My next suggestion would be to scale you bitmap to start with and place the appropriate one in your hdpi,xhdpi,xxhdpi folders.

Sign up to request clarification or add additional context in comments.

5 Comments

Your last sentence is a pretty good suggestion, because OOM errors will most likely happen to low end devices that don't have the display resolution anyways. But the APK size ..
TBH, I can't much of a difference between ARGB_8888 and ALPHA_8, I would say that's a good first option. That said, I don't have much of a design-eye ;)
Most of my PNGs are already saved in 8 Bit with Alpha transparency to reduce the APK size. But the get getPixels method gives me 32 Bit with one Byte per channel of ARGB. And I need those channels to determine how high the transparency and how light or dark the color is. I don't know if that will work with 8 Bit Bitmaps anymore. But thanks for the suggestions for reducing the bitmap size, but 5 minutes after your answer I wrote an answer myself because I finally figured out how to get just a part of the pixels, so the array is now pretty small and I think that's enough, but thanks again.
Ok, glad you found a solution. I'll leave this here in case it helps someone else.
Just a short heads-up. The getPixel stuff works now like a charm but on really laggy devices I still got OOM errors on the BitmapFactory.decode. So I remembered your advice and switched all bitmaps to decode in alpha8. The getPixel method still gives me argb32, so my algorithm is working without modification. I guess the getPixel method now needs more time to convert alpha8 into argb32, but previously this needed to be done in the decode method, because my PNGs are saved in alpha8. So shorter decode but longer getPixels == no changes. Except I use the quarter of the memory. So thanks again.
0

In fact it is pretty simple to get just the pixels of a specific area. But the documentation is wrong about the stride parameter. It says:

public void getPixels (int[] pixels, int offset, int stride, int x, int y, int width, int height)

stride: The number of entries in pixels[] to skip between rows (must be >= bitmap's width). Can be negative.

But the truth is it can (and must in most cases) be less than the bitmap's width, it just has to be bigger or equal to the passed width (the second to last parameter of getPixels), meaning the width of the area from which I want the pixels.

So to get the pixels of the first quarter of a bitmap:

int []pixels = new int[bitmap.getWidth()>>1 * bitmap.getHeight()>>1]; // quarter of the bitmap size
bitmap.getPixels(pixels, 0, bitmap.getWidth()>>1, 0, 0, bitmap.getWidth()>>1, bitmap.getHeight()>>1);

Or to get a specific rect with x,y (upper left corner) and width, height:

int []pixels = new int[width*height];
bitmap.getPixels(pixels, 0, width, x, y, width, height);

So pretty simple. It was just the wrong documentation that put a twist in my brain.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.