javaandroidmemory-leaksout-of-memoryleakcanary

Find memory leaks in the Activity code to free memory usage and avoid OutOfMemory Exception


I have an Activity with a ConstraingLayout with lots of ImageViews (one per card).

After a win, by clicking on the ImageView that will appear, the Activity will be "reloaded" showing a new set of cards to play.

The problem is that after each win the memory used by the Activity raises instead of returning at the initial amount used.

This cause an OutOfMemory Exception on some devices with low memory (e.g. on Nexus 7). :(

The logic items are:

GiocaMemory.java:

package ...
import ...

public class GiocaMemory extends AppCompatActivity
{
    ...

    MediaPlayer mediaPlayer;
    MediaPlayer.OnCompletionListener onCompletionListenerReleaseMediaPlayer;

    private AudioManager audioManager;
    private AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener;

    ...
    
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        
        ...

        onCompletionListenerReleaseMediaPlayer = new MediaPlayer.OnCompletionListener()
        {
            @Override
            public void onCompletion(MediaPlayer mp)
            {
                releaseMediaPlayer();
            }
        };

        audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);

        onAudioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener()
        {
            @Override
            public void onAudioFocusChange(int focusChange)
            {
                if(mediaPlayer != null)
                {
                    switch (focusChange)
                    {
                        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                            mediaPlayer.pause();
                            mediaPlayer.seekTo(0);
                            break;
                        case AudioManager.AUDIOFOCUS_GAIN:
                        case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
                        case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
                            mediaPlayer.start();
                            break;
                        case AudioManager.AUDIOFOCUS_LOSS:
                            releaseMediaPlayer();
                            break;
                    }
                }
            }
        };

        ...
    }

    private void win()
    {
        showView(textViewWin);
    }

    public void reloadActivity()
    {
        finish();
        startActivity(getIntent());
    }

    public void playSound(int idElement)
    {
        String name = idElement + "_name";

        playAudio(name, onCompletionListenerReleaseMediaPlayer);
    }

    public void playAudioName(int idElement)
    {
        String name = idElement + "_sound";

        playAudio(name, onCompletionListenerReleaseMediaPlayer);
    }
    
    public void onClickHome(View view)
    {
        finish();
    }

    public void stopAudio()
    {
        releaseMediaPlayer();
    }

    private void playAudio(String audioName, MediaPlayer.OnCompletionListener onCompletionListener)
    {
        stopAudio();
        
        if(!audioName.isEmpty())
        {
            int result = audioManager.requestAudioFocus(onAudioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
            if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
            {
                int resID = res.getIdentifier(audioName, "raw", getPackageName());

                if (resID == 0)
                {
                    return;
                }
                releaseMediaPlayer();

                startMediaPlayerWithRes(this, resID, audioName);
                mediaPlayer.setOnCompletionListener(onCompletionListener);

            }
        }
    }

    private void startMediaPlayerWithRes(Context context, int resID, String audioName)
    {
        mediaPlayer = MediaPlayer.create(context, resID);

        if(mediaPlayer != null) mediaPlayer.start();
        else mediaPlayer = new MediaPlayer();
    }

    private void releaseMediaPlayer()
    {
        if(mediaPlayer != null) mediaPlayer.release();
        mediaPlayer = null;
    }

    private void loadContents()
    {
        ...
    }
    
    @Override
    protected void onPause()
    {
        super.onPause();

        releaseMediaPlayer();
    }

    public void freeRes()
    {
        ...

        mediaPlayer = null;
        onCompletionListenerReleaseMediaPlayer = null;

        audioManager = null;
        onAudioFocusChangeListener = null;
        res = null;
        
        releaseMediaPlayer();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        
        freeRes();
    }
}

At the first run of GiocaMemory the AndroidStudio's profiler is:

enter image description here

After a win (so after the second onCreate) the used memory is:

enter image description here

Now the Java memory usage is 28,1 MB, instead of returning at the initial value of 25,2 MB.

The screenshots refer to the layout with 16 boxes. With the 30 box layout the memory used increases a lot more. (E.g. from 49 MB to 83 MB)

I may say that the images are enough resized in order to use the less memory possible, so maybe they cannot be the problem. Of course, I would be keen to know if I'm wrong.

I'm find very very hard to find them because I'm relatively new to Android programming, especially since I almost never had to face problems related to excessive memory usage.

Edit

These are some info using LeakCanary:

enter image description here

By clicking on one of the 3 "GiocaMemory Leaked 21 Agosto 13:35" (all 3 are the same, changing only the key = at the end of the trace)

ApplicationLeak(className=app.myapp.GiocaMemory, leakTrace=
┬
├─ android.media.AudioManager$1
│    Leaking: UNKNOWN
│    Anonymous subclass of android.media.IAudioFocusDispatcher$Stub
│    GC Root: Global variable in native code
│    ↓ AudioManager$1.this$0
│                     ~~~~~~
├─ android.media.AudioManager
│    Leaking: UNKNOWN
│    ↓ AudioManager.mAudioFocusIdListenerMap
│                   ~~~~~~~~~~~~~~~~~~~~~~~~
├─ java.util.HashMap
│    Leaking: UNKNOWN
│    ↓ HashMap.table
│              ~~~~~
├─ java.util.HashMap$HashMapEntry[]
│    Leaking: UNKNOWN
│    ↓ array HashMap$HashMapEntry[].[0]
│                                   ~~~
├─ java.util.HashMap$HashMapEntry
│    Leaking: UNKNOWN
│    ↓ HashMap$HashMapEntry.value
│                           ~~~~~
├─ app.myapp.GiocaMemory$2
│    Leaking: UNKNOWN
│    Anonymous class implementing android.media.AudioManager$OnAudioFocusChangeListener
│    ↓ GiocaMemory$2.this$0
│                    ~~~~~~
╰→ app.myapp.GiocaMemory
​     Leaking: YES (Activity#mDestroyed is true and ObjectWatcher was watching this)
​     key = dfa0d5fe-0c50-4c64-a399-b5540eb686df
​     watchDurationMillis = 380430
​     retainedDurationMillis = 375425
, retainedHeapByteSize=470627)

The official LeakCanary's documentation says:

If a node is not leaking, then any prior reference that points to it is not the source of the leak, and also not leaking. Similarly, if a node is leaking then any node down the leak trace is also leaking. From that, we can deduce that the leak is caused by a reference that is after the last Leaking: NO and before the first Leaking: YES.

But in my leakTrace there are only UNKNOWN leaks except the last YES.

How can I find that YES leak in my code, if it maybe a leak?


Solution

  • Are you properly abandoning the focus listener from AudioManager?

    AudioManager#abandonAudioFocus(OnAudioFocusChangeListener listener)

    The actual OOM might be the result of non recycling lists as mentioned, but that could be the cause of your memory leak.