6

As title says, I have out of memory exception being thrown on loading layouts. One would think this is a memory leak issue, but after battling it for 2 days I am not so sure anymore. The app has 10+ activities, most of which have background images.

Few facts/discoveries I made:

  • so far issue only appears on Galaxy Nexus running Android 4.0.3. I was unable to reproduce it on Nexus S (4.1.1) and Galaxy S II (2.3.3).

  • the screen orientation does not change. In fact most of my activities locked to portrait anyway.

  • just for laughs I added call to finish() when opening new activity, so there wouldn't be more than one activity in memory at the time. Verified that onDestroy is being called.

defined as:

@Override
public void onDestroy()
{
    super.onDestroy();
    cleanupDrawables(contentView);

    // null all the fields referencing views and drawables

    System.gc();
}

where cleanupDrawables() is:

protected static void cleanupDrawables(View view)
{
    cleanupDrawable(view.getBackground());

    if (view instanceof ImageView)
        cleanupDrawable(((ImageView)view).getDrawable());
    else if (view instanceof TextView)
    {
        TextView tv = (TextView)view;
        Drawable[] compounds = tv.getCompoundDrawables();
        for (int i = 0; i < compounds.length; i++)
            cleanupDrawable(compounds[i]);
    }
    else if (view instanceof ViewGroup && !(view instanceof AdapterView))
    {
        ViewGroup vg = (ViewGroup)view;
        for (int i = 0; i < vg.getChildCount(); i++)
            cleanupDrawables(vg.getChildAt(i));
        vg.removeAllViews();
    }
}

protected static void cleanupDrawable(Drawable d)
{
    if (d == null)
        return;

    d.setCallback(null);

    if (d instanceof BitmapDrawable)
        ((BitmapDrawable)d).getBitmap().recycle();
    else if (d instanceof LayerDrawable)
    {
        LayerDrawable layers = (LayerDrawable)d;
        for (int i = 0; i < layers.getNumberOfLayers(); i++)
            cleanupDrawable(layers.getDrawable(i));
    }
}
  • looking at the Eclipse heap inspector, memory appears to be stable, i.e. some activities take more memory than others, but it is released upon closing, and it appears stable over time.

  • according to related answers on SO images are stored in native memory, but this dude claims they should be on the heap since Android 3, so I should see memory increase if this was indeed image memory leak.

The end result of my efforts is that I still get out of memory error, though not as fast as I did before. Before error occurs I begin to see visible bitmap corruption, which wasn't the case before I added the cleanupDrawables() code. I deduced that call to Bitmap.recycle() is causing corruption, even though this code is only called on onDestroy. Corruption appears on both bitmaps that are parts of common styles appearing on many activities as well as bitmaps only shown on one activity.

In short results of my investigation are rather inconclusive. At this point I don't know what else to try.

The error stack trace for reference:

08-22 10:49:51.889: E/AndroidRuntime(31697): java.lang.RuntimeException: Unable to start activity ComponentInfo{klick.beatbleeds/klick.beatbleeds.Bleeds}: android.view.InflateException: Binary XML file line #67: Error inflating class <unknown>
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1955)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1980)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.app.ActivityThread.access$600(ActivityThread.java:122)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1146)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.os.Handler.dispatchMessage(Handler.java:99)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.os.Looper.loop(Looper.java:137)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.app.ActivityThread.main(ActivityThread.java:4340)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at java.lang.reflect.Method.invokeNative(Native Method)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at java.lang.reflect.Method.invoke(Method.java:511)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at dalvik.system.NativeStart.main(Native Method)
08-22 10:49:51.889: E/AndroidRuntime(31697): Caused by: android.view.InflateException: Binary XML file line #67: Error inflating class <unknown>
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.view.LayoutInflater.createView(LayoutInflater.java:606)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at com.android.internal.policy.impl.PhoneLayoutInflater.onCreateView(PhoneLayoutInflater.java:56)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.view.LayoutInflater.onCreateView(LayoutInflater.java:653)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:678)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.view.LayoutInflater.rInflate(LayoutInflater.java:739)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.view.LayoutInflater.inflate(LayoutInflater.java:489)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.view.LayoutInflater.inflate(LayoutInflater.java:396)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.view.LayoutInflater.inflate(LayoutInflater.java:352)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at klick.beatbleeds.ActivityBase.setContentView(ActivityBase.java:82)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at klick.beatbleeds.Bleeds.onCreate(Bleeds.java:40)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.app.Activity.performCreate(Activity.java:4465)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1049)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1919)
08-22 10:49:51.889: E/AndroidRuntime(31697):    ... 11 more
08-22 10:49:51.889: E/AndroidRuntime(31697): Caused by: java.lang.reflect.InvocationTargetException
08-22 10:49:51.889: E/AndroidRuntime(31697):    at java.lang.reflect.Constructor.constructNative(Native Method)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.view.LayoutInflater.createView(LayoutInflater.java:586)
08-22 10:49:51.889: E/AndroidRuntime(31697):    ... 23 more
08-22 10:49:51.889: E/AndroidRuntime(31697): Caused by: java.lang.OutOfMemoryError
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.graphics.Bitmap.nativeCreate(Native Method)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.graphics.Bitmap.createBitmap(Bitmap.java:605)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.graphics.Bitmap.createBitmap(Bitmap.java:551)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.graphics.Bitmap.createScaledBitmap(Bitmap.java:437)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.graphics.BitmapFactory.finishDecode(BitmapFactory.java:524)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:499)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:351)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.graphics.drawable.Drawable.createFromResourceStream(Drawable.java:773)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.content.res.Resources.loadDrawable(Resources.java:1937)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.content.res.TypedArray.getDrawable(TypedArray.java:601)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.view.View.<init>(View.java:2780)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.view.ViewGroup.<init>(ViewGroup.java:385)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.widget.LinearLayout.<init>(LinearLayout.java:174)
08-22 10:49:51.889: E/AndroidRuntime(31697):    at android.widget.LinearLayout.<init>(LinearLayout.java:170)
08-22 10:49:51.889: E/AndroidRuntime(31697):    ... 26 more

Here is a screenshot of one of the views to get the idea of how many images are used here enter image description here

Ilia G
  • 10,043
  • 2
  • 40
  • 59
  • So, how big are these bitmaps (resolution)? If you replace them with smaller images (just for kicks), do the problems go away? – dmon Aug 22 '12 at 15:34
  • Cleanup bitmap resource from the View isn't necessary and will only slow down your transition to a new Activity. What kind of layout are you inflating and with how many images? – DeeV Aug 22 '12 at 15:34
  • I also agree with @Deev, you shouldn't need to manually "clean up" your bitmaps on destroy. – dmon Aug 22 '12 at 15:35
  • @dmon I have background images that are 640x960, which is only somewhat bigger than Galaxy Nexus' resolution (480x800). I also agree that doing manual cleanup of bitmaps is ridiculous, but that is what people suggested in other similar threads. – Ilia G Aug 22 '12 at 15:42
  • @DeeV my top level layouts are LinearLayout. The look and feel of the app is highly customized (some fool in UX decided it would be cool to make it look like iPhone). I updated the question with a screenshot to give you a general idea on images used. – Ilia G Aug 22 '12 at 15:53
  • @llia G How many background images are you loading? Are you loading them for each view? Would it be possible to keep it in the background as a top-level container view and just inflate your views on top of it? The latter would mean you only inflate the bitmap once. – DeeV Aug 22 '12 at 15:54
  • @DeeV that wouldn't work. Each activity has different background. Home activity has 4 rotating backgrounds. – Ilia G Aug 22 '12 at 15:55
  • 4
    This is the internal punishment for doing iPhone design on Android! – WarrenFaith Aug 22 '12 at 15:58
  • Ah, four rotating backgrounds, that seems like a potential out of memory generator. Can you show us how you do that bit? – dmon Aug 22 '12 at 15:58
  • 640x960 * 4 = 2.45MB per image. Max memory allocation ~ 16MB. – IAmGroot Aug 22 '12 at 16:00
  • @dmon the image is set on once `onCreate` like this `findViewById(R.id.innerLayout).setBackgroundResource(backgrounds[new Random().nextInt(backgrounds.length)]);` where backgrounds is defined as `private static final int[] backgrounds = new int[] { R.drawable.background1, R.drawable.background2, R.drawable.background3 };` – Ilia G Aug 22 '12 at 16:02
  • http://developer.android.com/training/displaying-bitmaps/index.html Has a lot of helpful tips. – IAmGroot Aug 22 '12 at 16:02
  • I think this thread [here](http://stackoverflow.com/questions/1949066/java-lang-outofmemoryerror-bitmap-size-exceeds-vm-budget-android) may answer your question – David Hirst Aug 22 '12 at 16:04
  • @DavidHirst thanks, but I've already seen that and resources referenced form there too. – Ilia G Aug 22 '12 at 16:06
  • The selected answer there doesn't solve bitmaps problem anyways – User Aug 22 '12 at 17:21
  • Hey, I updated my answer with something which might be the reason of your problem - last sentence. – User Aug 22 '12 at 17:37

1 Answers1

2

Use these static methods to find out the exact problem(s):

public static void showBitmapSize(Bitmap bitmap) {
    Log.d("test", "bitmap dimensions: w:" + bitmap.getWidth() + ", h:" + bitmap.getHeight() + " memory: " + (bitmap.getRowBytes() * bitmap.getHeight() / 1048576d));
}

And the most important:

static double lastavail;
static double initavail;
static boolean first = true;

public static void showMemoryStats() {
    showMemoryStats("");
}

public static void showMemoryStats(String message) {
    Log.i("memory", message + "----------------------------------------------------------------------------------------");
    double nativeUsage = Debug.getNativeHeapAllocatedSize(); 
    Log.i("memory", "nativeUsage: " + (nativeUsage / 1048576d));
    //current heap size 
    double heapSize =  Runtime.getRuntime().totalMemory();
//      Log.i("memory", "heapSize: " + (heapSize / 1048576d));
    //amount available in heap 
    double heapRemaining = Runtime.getRuntime().freeMemory();
//      Log.i("memory", "heapRemaining: " + (heapRemaining / 1048576d)); 
    double memoryAvailable = Runtime.getRuntime().maxMemory() - (heapSize - heapRemaining) - nativeUsage;
    Log.i("memory", "memoryAvailable: " + (memoryAvailable / 1048576d));

    if (first) {
        initavail = memoryAvailable;
        first = false;
    }
    if (lastavail > 0) {
        Log.i("memory", "consumed since last: " + ((lastavail - memoryAvailable) / 1048576d));
    }
    Log.i("memory", "consumed total: " + ((initavail - memoryAvailable) / 1048576d));

    lastavail = memoryAvailable;

    Log.i("memory", "-----------------------------------------------------------------------------------------------");
}

Dividing by 1048576 is only to get values in MB (at least for me it's easier to think in MB).

Put a call to showMemoryStats with some meaningful message before setContentView() call, and another one after it. And when you start a new activity, etc. At the end you will the exact reasons of your problem.

Manual recycling can be necessary. I had to implement it in some places in my app. Also using a bitmap-intensive one (many backgrounds and pics) and had this kind of problems in all devices. With these methods I was able to find all the problems and handle appropiatedly.

Ah. And there's a possible fast solution for your problem, you say it only appears in Galaxy Nexus. It's the only xhdpi device from the ones you mention. You probably have all the bitmaps only in folder drawable or drawable-hdpi. The xhdpi device will take the bitmaps from drawable or drawable-hdpi and scale up (although they might be already in the correct size), and this will consume a lot of memory. Solution: Create drawable-xhdpi folder, if not existent, and put a copy of the bitmaps there.

User
  • 31,811
  • 40
  • 131
  • 232