Search code examples
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:

  • in the onCreate method I set the ConstraintLayout made of 30 ImageViews (the front side of the cards) and others 30 ImageViews (the back side of the cards)
  • for each ImageView (front and back sides) I set the OnClickListener and the image by scaling the drawable resource
  • every time the user clicks on an ImageView, I set the alpha for the two sides of the card to show only the proper side
  • if the user finds all matches, the win View will appear: if the user clicks it, will be invoked the win method, which "reload the activity"

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.

  • Why after every win the MB used by Java will increase?
  • How can I find any memory leaks I left in the code?
  • The way I'm using to "reload" the GiocaMemory Activity is correct or there is an other way which let me free more resources?

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.