背景
笔者优化音乐App内存泄漏时候,遇到了3个典型内存泄漏,泄漏的内存为39kb,一次39KB看上去不多,积少成多很有可能导致OOM,值得重视。
PS:文末有优化方案,优化后内存减少至原先的150分之一。
操作步骤为进入歌单列表页面,点击播放按钮,我们按照此步骤抓取一份内存日志,来分析下:
①点+导入hprof文件
②选择app 堆存储
③根据类名排序
④仅展示Activity、Fragment内存泄漏
⑤过滤关键字,如app进程的包名
⑥点击Leaks,下方Class Name区域会展示出内存泄漏的类名
⑦在Class Name区域点击选择要观察的类名
⑧在Instance List区域点击选择该类的实例,这一步我们选择了第三个实例
⑨在Instace Details区域点击References 选项卡
⑩选中 GC root only,一层一层展开引用链
也可以在第9步时候观察Activity的生命周期来判断内存泄漏
PlayUtil导致内存泄漏
首先点击Fields,查看Activity的生命周期,可以看到Instacen Details - Fields -Instance视图中Activity#mDestroyed = true ,表明此页面已经处于销毁状态,但Activity的内存空间仍然未释放
其次点击References,点一层层展开引用链,可以看到Activity使用了PlayUtil,PlayUtil初始化的时候传入了此Activity,而PlayUtil是一个单例类,该单例类全局持有了此Activity的实例,导致Activity一直被PlayUtil持有着,在Activity生命周期结束后,Activity占据的堆内存,无法被垃圾回收器回收,出现了泄漏的情况。
单例类持有Activity,导致Activity占据的内存无法被释放,遇到这种问题,解决起来也很容易:
- 替换context,使用ApplicationContext
- 使用一个更轻量无任何业务的Activity来初始化PlayUtil
- 如无必要不要使用context
PlayUtil问题,笔者采用了方法3:
修改前
private PlayUtil(Context context) {
m_MediaPlayerIml = MediaPlayerIml.getInstance();
this.m_context = context;
}
修改后
private PlayUtil(Context context) {
m_MediaPlayerIml = MediaPlayerIml.getInstance();
// 兼容老代码,保留入参context,但不使用,避免内存泄漏
// this.m_context = context;
}
这样可以达到PlayUtil不持有Activity的目的,在Activity#onDestroyed时候,Activity占据的内存将被垃圾回收器回收。
LoadProgress导致内存泄漏
接着我们继续看该Activity的第二个实例,查看该实例的生命周期可知该Activity已经处于Destroyed状态,但内存未被回收,这是为什么呢?
我们继续查看References,可以看到Activity被LoadProgress持有了,且无法被释放,笔者分析可能是Activity#showLoading期间,Activity转入后台或其他因素,并未来得及调用dissLoading导致LoadProgress工具类持续持有该Activity的引用,产生了内存泄漏。
笔者分析可能是Activity#showLoading期间,Activity转入后台或其他因素,并未来得及调用dissLoading导致LoadProgress工具类持续持有该Activity的引用,产生了内存泄漏。我们来看看问题代码
问题代码
Activity#initLoading,传入了当前Activity
private void initLoading() {
if (this.viewModel != null) {
this.viewModel.showLoading.observe(this, new Observer<Boolean>() {
public void onChanged(Boolean aBoolean) {
if (aBoolean) {
if (!LoadProgress.get().getDialog(BaseActivity.this).isShowing()) {
LoadProgress.get().getDialog(BaseActivity.this).show();
}
} else {
LoadProgress.get().dismissDialog(BaseActivity.this);
}
}
});
}
}
LoadProgress内使用HashMap缓存了传入的Activity
this.dialogs.put(context, lp);
解决方法
遇到这种问题也很好处理——在适当的时机清除HashMap缓存的Activity引用。
按照这种思路,我们看到:LoadProgress工具类里提供了HashMap的清空方法LoadProgress#cleanUpTrash,那么我们在合适的地方,清空`Activity缓存即可
在Activity#onDestroyed调用
@Override protected void onDestroy() {
super.onDestroy(); // 清空缓存
LoadProgress.get().cleanUpTrash();
}
这样,当Activity生命周期处于onDestroyed的时候,HashMap会清空持有的Activity,避免Activity内存泄漏
MediaplayerListener导致内存泄漏
接着我们看Activity的第三个实例,有前两个泄漏点的分析经验,可得知Activity此时已经处于onDestroyed状态,被某个类引用了,导致Activity的内存无法回收,熟能生巧,我们按图索骥可看到是MediaplayerIml通过mMediaPlayListenerCacheList持有了该类,该类是用于存储播放回调的,用于AIDL服务端通知Client的回调,更新播放界面。
问题代码:
Activity#onCreate时候调用了注册回调
mediaPlayerIml.registerListener(playListener);
private List<MediaPlayListener> mMediaPlayListenerCacheList = new ArrayList<>();
public synchronized void registerListener(MediaPlayListener listener) {
if (!mMediaPlayListenerCacheList.contains(listener)) {
mMediaPlayListenerCacheList.add(listener);
}
}
解决办法也很简单,页面销毁时,调用解绑注册
@Override
protected void onDestroy() {
super.onDestroy();
if(mediaPlayerIml!=null){
mediaPlayerIml.unregisterListener(playListener);
}
// 清空缓存
LoadProgress.get().cleanUpTrash();
}
总结
总结上述3个内存泄漏点
- PlayUtil构造函数持有Activity未释放
- LoadProgress成员变量HashMap持有Activity未释放
- AIDL远程服务端持有Listener,Listener持有Activity未释放
优化效果
优化前,多次进入该页面,点击播放按钮,产生4个实例,有3个实例无法被垃圾回收,出现了内存泄漏的情况
优化后,多次进入该页面,点击播放按钮,只产生一个实例
根据Retained Szie来看,优化前后节省内存,效果达到了 49937/332 = 150 倍。由此可见,Activity及时回收,极大的节省了内存占用。