在 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
。 :(
逻辑是:
- 在
onCreate
方法中,我设置 ConstraintLayout
由 30 ImageView
s(rhe 卡片的正面)和其他 30 ImageView
s(背面)组成牌面)
- 对于每个
ImageView
(正面和背面),我通过缩放可绘制资源 来设置 OnClickListener
和图像
- 每次用户点击
ImageView
,我将卡片两侧的 alpha 设置为只显示正确的一面
- 如果用户找到所有匹配项,将出现win视图:如果用户单击它,将调用win方法,"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();
}
}
GiocaMemory
的第一个 运行 AndroidStudio
的 profiler
是:
一场胜利后(即第二次胜利后onCreate
)使用的内存为:
现在 Java
内存使用量为 28,1 MB
,而不是返回 25,2 MB
的初始值。
截图参考了16个盒子的布局。 使用 30 盒布局,使用的内存增加了很多。 (例如,从 49 MB 到 83 MB)
我可能会说图像的大小调整得足够多,以便使用尽可能少的内存,所以也许它们不是问题所在。如果我错了请告诉我。
- 为什么每次获胜后
Java
使用的MB都会增加?
- 你能帮我找到我留在代码中的一些内存泄漏吗?
- 我用来 "reload"
GiocaMemory
Activity 的方法是正确的还是有其他方法可以让我释放更多资源?
我发现很难找到它们,因为我对 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 可能是上述非回收列表的结果,但这可能是内存泄漏的原因。
我有一个 Activity
和一个 ConstraingLayout
有很多 ImageView
(每张卡一张)。
获胜后,单击将出现的 ImageView,Activity
将 "reloaded" 显示一组新牌。
问题是每次获胜后 Activity
使用的内存都会增加,而不是返回初始使用量。
这会导致某些内存不足的设备(例如 Nexus 7)出现 OutOfMemory Exception
。 :(
逻辑是:
- 在
onCreate
方法中,我设置ConstraintLayout
由 30ImageView
s(rhe 卡片的正面)和其他 30ImageView
s(背面)组成牌面) - 对于每个
ImageView
(正面和背面),我通过缩放可绘制资源 来设置 - 每次用户点击
ImageView
,我将卡片两侧的 alpha 设置为只显示正确的一面 - 如果用户找到所有匹配项,将出现win视图:如果用户单击它,将调用win方法,"reload the activity"
OnClickListener
和图像
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
的第一个 运行 AndroidStudio
的 profiler
是:
一场胜利后(即第二次胜利后onCreate
)使用的内存为:
现在 Java
内存使用量为 28,1 MB
,而不是返回 25,2 MB
的初始值。
截图参考了16个盒子的布局。 使用 30 盒布局,使用的内存增加了很多。 (例如,从 49 MB 到 83 MB)
我可能会说图像的大小调整得足够多,以便使用尽可能少的内存,所以也许它们不是问题所在。如果我错了请告诉我。
- 为什么每次获胜后
Java
使用的MB都会增加? - 你能帮我找到我留在代码中的一些内存泄漏吗?
- 我用来 "reload"
GiocaMemory
Activity 的方法是正确的还是有其他方法可以让我释放更多资源?
我发现很难找到它们,因为我对 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 firstLeaking: YES
.
但是在我的 leakTrace 中只有 UNKNOWN
个泄漏,除了最后一个 YES
。
我如何才能在我的代码中找到 YES
泄漏,如果它可能是泄漏?
非常感谢您的帮助!
您是否正确地放弃了 AudioManager 中的焦点侦听器?
AudioManager#abandonAudioFocus(OnAudioFocusChangeListener listener)
实际的 OOM 可能是上述非回收列表的结果,但这可能是内存泄漏的原因。