引言:
不知道你们有没有遇到过这种情况:App用着用着就卡了,甚至直接闪退。打开Android Studio的Profiler一看,内存曲线一路飙升,最后被系统无情杀死...... 这十有八九就是内存泄漏在作祟! 今天,我们就来一场内存泄漏的'狩猎'行动。我将用一个真实的案例,手把手教你如何使用Android Profiler这套'猎枪',精准定位并解决内存泄漏问题。文章不长,全是干货,保证你看完就能用得上!
什么是内存泄漏?
用一句通俗的话讲:当一个对象已经不再需要使用了,本该被垃圾回收器(GC)回收,但因为某些原因,仍然被其他对象间接或直接地持有引用,导致无法被回收,这就是内存泄漏。
随着泄漏对象的不断堆积,应用可用内存越来越少,最终引发OOM(Out Of Memory)崩溃。
实战开始
我们创建一个非常常见的泄漏场景:持有Activity引用的单例。
java
// 一个管理音视频播放的单例类
public class PlayerManager {
private static PlayerManager instance;
private PlayCallback mCallback; // 这是一个回调接口
public static PlayerManager getInstance() {
if (instance == null) {
instance = new PlayerManager();
}
return instance;
}
// 设置回调,用于将播放状态通知给UI
public void setCallback(PlayCallback callback) {
this.mCallback = callback; // 【危险操作!】
}
public interface PlayCallback {
void onPlayFinished();
}
}
java
// 在MainActivity中使用这个单例
public class MainActivity extends AppCompatActivity implements PlayerManager.PlayCallback {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 将当前Activity设置为播放回调
PlayerManager.getInstance().setCallback(this);
}
@Override
public void onPlayFinished() {
// 更新UI,比如隐藏播放进度条
Log.d("MainActivity", "播放完成");
}
}
问题分析: 当我们旋转屏幕,MainActivity会被销毁并重建。但是,新的MainActivity创建时,单例PlayerManager已经持有了旧Activity的引用(通过mCallback)。导致旧的Activity无法被GC回收,从而发生内存泄漏。你多旋转几次屏幕,就会多泄漏几个Activity!
狩猎行动 - 使用Android Profiler抓"真凶"
- 运行App并打开Profiler 在Android Studio中点击 View > Tool Windows > Profiler,运行你的App。
- 诱发泄漏 在手机上反复旋转屏幕5-6次,然后强制触发一次垃圾回收(点击Profiler内存视图上的垃圾桶图标)。
- 捕获堆转储 点击内存视图上的"Dump Java heap"图标。系统会捕获当前时刻的Java堆内存快照。
- 分析堆转储 · 在堆转储分析器中,选择按"Package"分组。 · 找到你的应用包名,展开后,你会发现有多个
MainActivity
的实例还存活着! · 正常情况下去,在旋转屏幕后,旧的Activity应该已经被销毁,只存在一个实例。 (这里可以你自己操作后,截一张Profiler的图放上去,显示有多个Activity实例,效果会非常好) - 定位引用链 · 右键点击其中一个多余的MainActivity实例,选择 Go to Instance。 · 在右侧的"References"面板中,你就可以看到这个Activity被谁引用了。 · 逐层展开,你会发现一条清晰的引用链: MainActivity instance -> this$0 (匿名内部类持有外部类引用) -> mCallback -> PlayerManager instance。 看!我们成功抓到了"元凶"!
修复漏洞 - 几种解决方案
知道了原因,修复就很简单了。这里提供几种方法:
方案一:及时解引用(推荐) 在Activity销毁时,将Callback置为null。
java
public class MainActivity extends AppCompatActivity implements PlayerManager.PlayCallback {
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
}
@Override
protected void onDestroy() {
super.onDestroy();
// 在Activity销毁时,移除回调,切断引用链
PlayerManager.getInstance().setCallback(null);
}
// ...
}
方案二:使用弱引用(WeakReference) 修改单例,使用弱引用来持有Callback。
java
public class PlayerManager {
// ...
private WeakReference<PlayCallback> mCallbackRef; // 使用弱引用
public void setCallback(PlayCallback callback) {
this.mCallbackRef = new WeakReference<>(callback);
}
// 在使用回调时,需要先判断是否还存在
private void notifyPlayFinished() {
if (mCallbackRef != null && mCallbackRef.get() != null) {
mCallbackRef.get().onPlayFinished();
}
}
}
总结
我们来总结一下关键步骤:
- 怀疑泄漏:App卡顿、内存只增不减。
- 使用Profiler:诱发场景 -> 捕获堆转储。
- 分析堆转储:查找重复类实例 -> 追踪GC Root引用链。
- 修复问题:解引用 or 弱引用。
内存泄漏的场景远不止这一种,比如还有:Handler、非静态内部类、线程、传感器管理器等。但排查的思路和工具的使用都是通用的。
你在开发中还遇到过哪些诡异的内存泄漏案例呢?欢迎在评论区分享,我们一起排雷!
QQ交流群:1063024045