java - 查找 Activity 代码中的内存泄漏,以释放内存使用并避免 OutOfMemory 异常

标签 java android memory-leaks out-of-memory leakcanary

我有一个带有 ConstraingLayoutActivity,其中有很多 ImageView(每张卡一个)。

获胜后,通过单击将出现的 ImageView,Activity 将“重新加载”,显示一组要玩的新牌。

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

这会在某些内存较低的设备(例如 Nexus 7)上导致 OutOfMemory 异常。 :(

逻辑是:

  • onCreate 方法中,我设置了由 30 个 ImageView(卡的正面)和其他 30 个 组成的 ConstraintLayout >ImageView(卡片的背面)
  • 对于每个 ImageView(正面和背面),我通过缩放可绘制资源来设置 OnClickListener 和图像
  • 每次用户单击 ImageView 时,我都会设置卡片两侧的 Alpha 以仅显示正确的一侧
  • 如果用户找到所有匹配项,则会出现 win View :如果用户单击它,将调用 win 方法,该方法“重新加载 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();
    }
}

第一次运行 GiocaMemory 时,AndroidStudioprofiler 是:

enter image description here

获胜后(因此在第二次onCreate之后)使用的内存为:

enter image description here

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

屏幕截图是指有 16 个框的布局。 采用 30 个框布局,使用的内存会增加很多。 (例如从 49 MB 到 83 MB)

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

  • 为什么每次获胜后Java使用的MB都会增加?
  • 您能帮我找出代码中留下的一些内存泄漏吗?
  • 我用来“重新加载”GiocaMemory Activity 的方式是正确的还是有其他方式可以让我释放更多资源?

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

编辑:

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

enter image description here

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

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)

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.

但在我的泄漏跟踪中,除了最后一个之外,只有未知泄漏。

如果我的代码中可能存在泄漏,我怎样才能找到YES泄漏?

非常感谢您的帮助!

最佳答案

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

AudioManager#abandonAudioFocus(OnAudioFocusChangeListener监听器)

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

关于java - 查找 Activity 代码中的内存泄漏,以释放内存使用并避免 OutOfMemory 异常,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57462677/

相关文章:

java - 如何使用@SpringBootApplication和@Configuration、数据源

java - ElasticSearch - 使用 FilterBuilder

java - 我希望我的 Android 应用程序能够打开所有文件扩展名

android - 为什么在存在 29.0.3 时需要构建工具 29.0.2?

java - Hibernate 和 Apache Tomcat 的问题

java - 将数据类型从 C 映射到 Java

android - 如何修复 Android Studio 在执行 Gradle 任务时卡住?

Android Webview GIF 堆随着时间的推移急剧增长

c++ - 返回导致内存泄漏的 wchar_t 数组

linux - Upstart init 正在泄漏内存,你如何调试它?