Friday, 23 August 2019

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 logics are:

  • in the onCreate method I set the ConstraintLayout made of 30 ImageViews (the front side of rhe 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
{
    int qtyElements = 16;
    Map<Integer, MyElement> mapElements = new LinkedHashMap<>();
    Map<Integer, Integer> mapPosMyElements = new LinkedHashMap<>();

    MediaPlayer mediaPlayer;
    MediaPlayer.OnCompletionListener onCompletionListenerReleaseMediaPlayer;

    private AudioManager audioManager;
    private AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener;

    SeekBar seekBar_volume;

    Resources res;

    ImageView imageView_volume;

    int metrics_widthPixels = 0;
    int metrics_heightPixels = 0;

    TextView textViewWin;
    Set<Integer> setIdElementsToFind = new HashSet<>();
    Map<Integer, String> mapViewNameElements = new HashMap<>();
    Map<Integer, ImageView> mapImageViewElements = new HashMap<>();
    Map<Integer, ImageView> mapImageViewElementsBack = new HashMap<>();
    int idElement1;
    int posElement1;
    int idElement2;
    int posElement2;

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

        qtyElements = getIntent().getExtras().getInt("qtyElements", 16);

        int idContentView;
        if(qtyElements == 30) {
            idContentView = R.layout.activity_play_memory_30;
        }
        else if(qtyElements == 16) {
            idContentView = R.layout.activity_play_memory_16;
        }

        setContentView(idContentView);

        res = getResources();

        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;
                    }
                }
            }
        };

        setVolumeControlStream(AudioManager.STREAM_MUSIC);
        initVolumeControls();

        textViewWin = findViewById(R.id.textViewWin);
        textViewWin.setOnClickListener(v -> {
            finish();
            startActivity(getIntent());
        });
        hideView(textViewWin);

        imageView_volume = findViewById(R.id.imageView_volume);

        metrics_widthPixels = MyUtils.getDisplayMetrics_widthPixels(res);
        metrics_heightPixels = MyUtils.getDisplayMetrics_heightPixels(res);

        loadContents();

        if(preferenza_fullScreen)
        {
            hideSystemUI();
        }
    }

    public void onClickElement(int pos)
    {
        hideView(seekBar_volume);

        stopAudio();

        if(idElement1 > 0 && idElement2 > 0) {
            showTileBack(posElement1);
            showTileBack(posElement2);
            idElement1 = 0;
            idElement2 = 0;
            posElement1 = 0;
            posElement2 = 0;
        }

        showTile(pos);

        int idElementChoosen = mapPosMyElements.get(pos);

        playAudioNameThenSound(idElementChoosen);

        if(idElement1 > 0) {
            idElement2 = idElementChoosen;
            posElement2 = pos;
        }
        else {
            idElement1 = idElementChoosen;
            posElement1 = pos;
        }

        if(idElement2 > 0)
        {
            if(idElement1 == idElement2)
            {
                if (setIdElementsToFind.contains(idElementChoosen))
                {
                    setIdElementsToFind.remove(idElementChoosen);

                    idElement1 = 0;
                    idElement2 = 0;
                    posElement1 = 0;
                    posElement2 = 0;

                    if (setIdElementsToFind.isEmpty())
                    {
                        win();
                    }
                }
            }
        }
    }

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

    private void showTile(int pos)
    {
        mapImageViewElements.get(pos).setAlpha(1f);
        mapImageViewElementsBack.get(pos).setAlpha(0f);
    }

    private void showTileBack(int pos)
    {
        mapImageViewElementsBack.get(pos).setAlpha(1f);
        mapImageViewElements.get(pos).setAlpha(0f);
    }

    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 playAudioNameThenSound(int idElement)
    {
        String name = idElement + "_name";
        String sound = idElement + "_sound";

        play2Audio(name, sound);
    }

    public void playSoundThenName(int idElement)
    {
        String name = idElement + "_name";
        String sound = idElement + "_sound";

        play2Audio(sound, name);
    }

    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 play2Audio(final String audioName1, final String audioName2)
    {
        stopAudio();

        final int resID1 = !audioName1.isEmpty() ? res.getIdentifier(audioName1, "raw", getPackageName()) : 0;
        final int resID2 = !audioName2.isEmpty() ? res.getIdentifier(audioName2, "raw", getPackageName()) : 0;

        if(resID1 > 0)
        {
            int result = audioManager.requestAudioFocus(onAudioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
            if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
            {
                releaseMediaPlayer();
                startMediaPlayerWithRes(this, resID1, audioName1);

                if(resID2 == 0)
                {
                    mediaPlayer.setOnCompletionListener(onCompletionListenerReleaseMediaPlayer);
                }
                else
                {
                    mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener()
                    {
                        @Override
                        public void onCompletion(MediaPlayer mp)
                        {
                            int result = audioManager.requestAudioFocus(onAudioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
                            if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
                            {
                                if (resID2 > 0)
                                {
                                    releaseMediaPlayer();
                                    startMediaPlayerWithRes(getApplicationContext(), resID2, audioName2);

                                    mediaPlayer.setOnCompletionListener(onCompletionListenerReleaseMediaPlayer);
                                }
                            }
                        }
                    });
                }
            }
        }
        else if(resID2 > 0)
        {
            playAudio(audioName2, onCompletionListenerReleaseMediaPlayer);
        }
    }

    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()
    {
        int numTotalElements = qtyElements;

        for(int n = 1; n <= numTotalElements; n++)
        {
            mapViewNameElements.put(n, "imageViewElement" + n);
            ImageView imageView = findViewById(res.getIdentifier("imageViewElement" + n, "id", getPackageName()));
            ImageView imageViewRetro = findViewById(res.getIdentifier("imageViewElementBack" + n, "id", getPackageName()));
            if(imageView != null) {
                mapImageViewElements.put(n, imageView);
            }
            if(imageViewRetro != null) {
                mapImageViewElementsBack.put(n, imageViewRetro);
            }
        }

        int qtaPerSide = (int) Math.sqrt(numTotalElements);

        List<EnumElements> listElementsEnum = new ArrayList<>(Arrays.asList(EnumElements.values()));
        for(int posizion = 1; posizion <= numTotalElements; posizion++)
        {
            if(mapPosMyElements.containsKey(posizion)) {
                continue;
            }

            Collections.shuffle(listElementsEnum);
            EnumElements e = listElementsEnum.get(new Random().nextInt(listElementsEnum.size()-1));

            mapElements.put(e.idElement, new MyElement(e.idElement, e.name, e.idElementAudioName, e.idElementAudioSound));

            mapPosMyElements.put(posizion, e.idElement);
            setIdElementsToFind.add(e.idElement);

            int posizion2 = MyUtils.randomIntRangeWithExcludedNumbers(posizion+1, numTotalElements, new ArrayList<>(mapPosMyElements.keySet()));

            mapPosMyElements.put(posizion2, e.idElement);

            String nameRes = "_" + e.idElement;
            int imageId = res.getIdentifier(nameRes, "drawable", getPackageName());
            if(imageId > 0) {

                int reqWidth = metrics_widthPixels / qtaElementiPerLato;
                int reqHeight = metrics_heightPixels / qtaElementiPerLato;
                Bitmap bitmapResized = generateBitmapResized(res, imageId, reqWidth, reqHeight);

                final int posForLambda = posizion;
                mapImageViewElements.get(posizion).setImageBitmap(bitmapResized);
                mapImageViewElements.get(posizion).setOnClickListener(v -> onClickElement(posForLambda));
                mapImageViewElementsBack.get(posizion).setOnClickListener(v -> onClickElement(posForLambda));

                final int pos2ForLambda = posizion2;
                mapImageViewElements.get(posizion2).setImageBitmap(bitmapResized);
                mapImageViewElements.get(posizion2).setOnClickListener(v -> onClickElement(pos2ForLambda));
                mapImageViewElementsBack.get(posizion2).setOnClickListener(v -> onClickElement(pos2ForLambda));

                showTileBack(posizion);
                showTileBack(posizion2);

                if(bitmapResized != null)
                {
                    bitmapResized = null;
                }
            }

            listElementsEnum.remove(e);
        }

        listElementsEnum = null;
    }

    public void onClickImgVolume(View view)
    {
        toggleView(R.id.seekBar_volume);

        setIconVolume();
    }

    public void setIconVolume()
    {
        if(viewVisibile(R.id.seekBar_volume)) {
            imageView_volume.setColorFilter(Color.BLUE);
            imageView_volume.setAlpha(1f);
        }
        else {
            imageView_volume.setColorFilter(Color.GRAY);
            imageView_volume.setAlpha(0.25f);
        }
    }

    public void toggleView(int idView)
    {
        if(viewVisibile(idView)) {
            hideView(idView);
        }
        else {
            showView(idView);
        }
    }

    public void showView(int idLayout)
    {
        View view = findViewById(idLayout);
        view.setVisibility(View.VISIBLE);
    }

    public void showView(View view)
    {
        int idView = view != null ? view.getId() : 0;

        if(idView > 0) showView(idView);
    }

    public void hideView(int idLayout)
    {
        View view = findViewById(idLayout);
        view.setVisibility(View.GONE);
    }

    public void hideView(View view)
    {
        int idView = view != null ? view.getId() : 0;

        if(idView > 0) hideView(idView);
    }

    public boolean viewVisibile(int idView)
    {
        return findViewById(idView).getVisibility() == View.VISIBLE;
    }

    public void initVolumeControls()
    {
        try {
            seekBar_volume = findViewById(R.id.seekBar_volume);
            seekBar_volume.setMax(audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC));
            seekBar_volume.setProgress(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC));

            seekBar_volume.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, progress, 0);
                }

                @Override
                public void onStartTrackingTouch(SeekBar seekBar) {
                }

                @Override
                public void onStopTrackingTouch(SeekBar seekBar) {
                }
            });
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus)
    {
        super.onWindowFocusChanged(hasFocus);

        if (hasFocus)
        {
            hideSystemUI();
        }
    }
    private void hideSystemUI()
    {
        if (Build.VERSION.SDK_INT >= 19)
        {
            View decorView = getWindow().getDecorView();
            decorView.setSystemUiVisibility
            (
                View.SYSTEM_UI_FLAG_IMMERSIVE
                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                    | View.SYSTEM_UI_FLAG_FULLSCREEN
            );
        }
    }

    @Override
    protected void onPause()
    {
        super.onPause();

        releaseMediaPlayer();
    }

    public void freeRes()
    {
        textViewWin.setOnClickListener(null);
        textViewWin = null;

        seekBar_volume = null;
        imageView_volume = null;

        mapPosMyElements.clear();
        mapPosMyElements = null;

        setIdElementsToFind.clear();
        setIdElementsToFind = null;

        mapViewNameElements.clear();
        mapViewNameElements = null;

        for(Map.Entry<Integer, ImageView> entry : mapImageViewElements.entrySet()) {
            ImageView imageView = entry.getValue();
            Drawable drawable = imageView.getDrawable();
            if (drawable instanceof BitmapDrawable) {
                BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
                Bitmap bitmap = bitmapDrawable.getBitmap();
                bitmap.recycle();
                bitmapDrawable = null;
                bitmap = null;
            }
            imageView.setOnClickListener(null);
            imageView.setImageDrawable(null);
            imageView.setImageBitmap(null);
            imageView = null;
            drawable = null;
        }
        mapImageViewElements.clear();
        mapImageViewElements = null;

        for(Map.Entry<Integer, ImageView> entry : mapImageViewElementsBack.entrySet()) {
            ImageView imageView = entry.getValue();
            Drawable drawable = imageView.getDrawable();
            if (drawable instanceof BitmapDrawable) {
                BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
                Bitmap bitmap = bitmapDrawable.getBitmap();
                bitmap.recycle();
                bitmapDrawable = null;
                bitmap = null;
            }
            imageView.setOnClickListener(null);
            imageView.setImageDrawable(null);
            imageView.setImageBitmap(null);
            imageView = null;
            drawable = null;
        }
        mapImageViewElementsBack.clear();
        mapImageViewElementsBack = null;

        mapElements.clear();
        mapElements = null;

        mediaPlayer = null;
        onCompletionListenerReleaseMediaPlayer = null;

        audioManager = null;
        onAudioFocusChangeListener = null;
        res = null;

        releaseMediaPlayer();
    }

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

        freeRes();
    }
}

MyUtils.java:

public class MyUtils {

    public static int getDisplayMetrics_widthPixels(Resources res) {
        return res.getDisplayMetrics().widthPixels;
    }

    public static int getDisplayMetrics_heightPixels(Resources res) {
        return res.getDisplayMetrics().heightPixels;
    }

    public static int randomIntRange(int min, int max)
    {
        return (new Random().nextInt(max-min+1))+min;
    }

    public static int randomIntRangeWithExcludedNumbers(int min, int max, List<Integer> excl)
    {
        int maxTry = 1000;

        int num = -1;
        while(maxTry > 0)
        {
            maxTry--;

            int numTemp = MyUtils.randomIntRange(min, max);
            if(!excl.contains(numTemp)) {
                num = numTemp;
                break;
            }
        }

        return num;
    }
}

MyImg.java:

public class MyImg {
    public static Bitmap generateBitmapResized(Resources res, int imageId, int reqWidthPixels, int reqHeightPixels)
    {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, imageId, options);

        return decodeSampledBitmapFromResource(res, imageId, reqWidthPixels, reqHeightPixels);
    }

    public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        options.inJustDecodeBounds = false;

        return BitmapFactory.decodeResource(res, resId, options);
    }

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

        if (height > reqHeight || width > reqWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;
            while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}

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. Please tell me if I'm wrong.

  • Why after every win the MB used by Java will increase?
  • Can you please help me find some 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 exept the last YES.

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

Thank you very much for your help.



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

No comments:

Post a Comment