在 Activity 代码中查找内存泄漏以释放内存使用并避免 OutOfMemory Exception

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

我有一个 Activity 和一个 ConstraingLayout 有很多 ImageView(每张卡一张)。

获胜后,单击将出现的 ImageView,Activity 将 "reloaded" 显示一组新牌。

问题是每次获胜后 Activity 使用的内存都会增加,而不是返回初始使用量。

这会导致某些内存不足的设备(例如 Nexus 7)出现 OutOfMemory Exception。 :(

逻辑是:

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

GiocaMemory 的第一个 运行 AndroidStudioprofiler 是:

一场胜利后(即第二次胜利后onCreate)使用的内存为:

现在 Java 内存使用量为 28,1 MB,而不是返回 25,2 MB 的初始值。

截图参考了16个盒子的布局。 使用 30 盒布局,使用的内存增加了很多。 (例如,从 49 MB 到 83 MB)

我可能会说图像的大小调整得足够多,以便使用尽可能少的内存,所以也许它们不是问题所在。如果我错了请告诉我。

我发现很难找到它们,因为我对 Android 编程还比较陌生,尤其是因为我几乎从未遇到过与内存使用过多相关的问题。

编辑:

这些是使用 LeakCanary 的一些信息:

通过单击 3 个 "GiocaMemory Leaked 21 Agosto 13:35" 之一(所有 3 个都相同,仅更改跟踪末尾的 key =

ApplicationLeak(className=app.myapp.GiocaMemory, leakTrace=
┬
├─ android.media.AudioManager
│    Leaking: UNKNOWN
│    Anonymous subclass of android.media.IAudioFocusDispatcher$Stub
│    GC Root: Global variable in native code
│    ↓ AudioManager.this[=12=]
│                     ~~~~~~
├─ 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
│    Leaking: UNKNOWN
│    Anonymous class implementing android.media.AudioManager$OnAudioFocusChangeListener
│    ↓ GiocaMemory.this[=12=]
│                    ~~~~~~
╰→ 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)

official LeakCanary's documentation 说:

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.

但是在我的 leakTrace 中只有 UNKNOWN 个泄漏,除了最后一个 YES

我如何才能在我的代码中找到 YES 泄漏,如果它可能是泄漏?

非常感谢您的帮助!

您是否正确地放弃了 AudioManager 中的焦点侦听器?

AudioManager#abandonAudioFocus(OnAudioFocusChangeListener listener)

实际的 OOM 可能是上述非回收列表的结果,但这可能是内存泄漏的原因。