案例3——view内存泄漏
前文提到,profile#Leaks视图无法展示非Activity、非Fragment的内存泄漏,换言之,除了Activity、Fragment的内存泄漏外,其他类的内存问题我们只能自己检索hprof文件查询了。
下面有一个极佳的view内存泄漏例子,它的操作步骤为:
- 播放音乐,唤醒音乐悬浮窗
- 播放一段时间后,关闭音乐悬浮窗
- 重复步骤1和2
我们重复三次之后,得到一份hprof文件,下面我们来分析一下内存泄漏问题
①输入view的名称
②选择view
③可以看到分配了3个实例对象
④Instance List视图显示,view有3个实例对象及其引用
我们从上至下依次看3分实例的调用链
第一个泄漏点
view的第一个实例
先查看Fields区域,观察mLayoutmode值,判断view是否离开了窗口,如果已经离开了窗口,表明view未被回收,存在内存泄漏
可以看到mLayoutMode = -1 ,表明布局已经离开屏幕了,此实例存在内存泄漏的情况
接着我们查看References区域,逐级点开我们发现Handler发送的Message持有了当前view,导致view在离开窗口的时候,无法被垃圾回收器回收。
右键点击查看问题代码
问题代码:
playHandler.post(new Runnable() {
@Override
public void run() {
tv_play.setText(playItem.getProgramTitle());
tb_play.setSelected(true);
initView();
}
});
看到new Runnbale,这是是匿名内部类,匿名内部类持有当前类的引用,匿名Runnbale未执行完毕,Runnbale内存未释放的时候,view就无法被释放,而匿名Runnbale的释放时机不可控,由Handler、Looper、Runnbale执行情况影响。
那么我们该怎么优化呢?
- 使用非匿名或静态的Handler+弱引用,处理此任务
- 在主线程处理此任务
- view退出的时候释放Message对view的引用
笔者采用了方案3:
tv_play.setText(playItem.getProgramTitle());
tb_play.setSelected(true);
initView();
方案1代码与下面view的第三个实例写法一致,不重复写了;我们解释一下方案3:
view退出的时候释放Message对view的引用
根据上图所示,我们看到Message-Runnbale-View的引用关系可知,Looper中的Message持续的引用view,我们最高效释放内存的做法是view离开窗口的时候,斩断Message与view的引用关系,那么我们该怎么做呢?答案是:
- 结束子线程任务
- 清空Looper缓存的Message
- 释放Handler
第一步:结束子线程任务很简单
thread.interrupt()
本案例给Handler传入的是Runnbale,Handler未提供结束Runnbale的接口,此项优化搁置
第二步:清空Message
已知Looper提供了清空Message的接口
- Looper#quit
- Looper#quitSafely
- 主线程的Looper无法退出
已知Handler提供了释放Message的接口 - Handler#removeCallbacksAndMessages
那我们优化起来就很简单了,清空Handler持有的Message
@Override protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
... // 释放message,断开message-Runnbale-view的引用链 if
(playHandler != null) {
playHandler.removeCallbacksAndMessages(null); playHandler = null;
}
}
第二个泄漏点
我们继续看view的第二个实例
先查看Fields区域,观察mLayoutmode值,判断view是否离开了窗口,如果已经离开了窗口,表明view未被回收,存在内存泄漏
可以看到mLayoutMode = -1 ,表明布局已经离开屏幕了,此实例存在内存泄漏的情况
接着我们看References区域,观察调用链
可以看到MediaPlayerIml有一个成员变量mMediaPlayListenerCacheList,缓存了MediaPlayListener,MediaPlayListener又是在view实例里面创建的,并且作为内部类,它持有view的实例。现在我们得到了清晰的调用链,MediaPlayerIml->mMediaPlayListenerCacheList->MediaPlayListener->view,MediaPlayerIml引用view导致view实例无法被释放
查看问题代码:
笔者发现view#onDetachedFromWindow已经触发了移除list#listener操作
@Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mediaPlayerIml.unregisterListener(playListener);
可以看到内部实现是remove调引用的
/**
* 取消注册listener
*
* @param listener
*/
public synchronized void unregisterListener(MediaPlayListener listener) {
mMediaPlayListenerCacheList.remove(listener);
}
那为什么会未回收持续占用内存呢?
- 抓拍hprof文件期间,代码未执行到unregisterListener,导致view内存未得到释放
- mMediaPlayListenerCacheList添加的listener与remove的listener不是同一个
- 此处没有产生内存泄漏,判断view是否应该被回收的依据有问题
第三个泄漏点
搁置疑问,接着我们来看view的第三个实例,节省时间,笔者直接调到代码索引出,展示问题代码:
/**
* 播放进度条刷新控
*/
private Handler m_handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_FLUSH_SEEKBAR:
boolean isPlaying = mediaPlayerIml != null && mediaPlayerIml.getPlayStatus() == QingtingConfig.PLAY;
if (isPlaying) {
int currentTime = mediaPlayerIml.getCurrentTime();
int totalTime = mediaPlayerIml.getTotalTime();
mSeekBar.setMax(totalTime);
mSeekBar.setProgress(currentTime);
mPrograssBar.setMaxProgress(totalTime);
mPrograssBar.setCurrentProgress(currentTime);
LoggerUtils.instance().logE("mediaPlayJindu", "mediaPlayJindu" + totalTime + "/" + currentTime);
}
m_handler.sendEmptyMessageDelayed(MSG_FLUSH_SEEKBAR, MSG_FLUSH_TIME);
break;
}
}
};
可以看到此处还是使用了非静态内部类m_handler,m_handler持有当前view 的引用,m_handler如果长期存在,那么view的内存也不会被释放
解决方法如下:
- 定义外部类Handler
- 定义静态内部类
- 定义静态内部类+弱引用
笔者采用了方案3:
定义静态内部类
private static class UpdateHandler extends Handler {
private final WeakReference<MediaPlayerIml> mediaPlayerImlWeakReference;
private final WeakReference<SeekBar> seekBarWeakReference;
private final WeakReference<QQCircleProgressBar> progressBarWeakReference;
public UpdateHandler(MediaPlayerIml mediaPlayerIml, SeekBar seekBar, QQCircleProgressBar progressBar) {
mediaPlayerImlWeakReference = new WeakReference<MediaPlayerIml>(mediaPlayerIml);
seekBarWeakReference = new WeakReference<SeekBar>(seekBar);
progressBarWeakReference = new WeakReference<QQCircleProgressBar>(progressBar);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == MSG_FLUSH_SEEKBAR) {
MediaPlayerIml mediaPlayerIml = mediaPlayerImlWeakReference.get();
SeekBar seekBar = seekBarWeakReference.get();
QQCircleProgressBar qqCircleProgressBar =progressBarWeakReference.get();
boolean isPlaying = mediaPlayerIml != null && mediaPlayerIml.getPlayStatus() == QingtingConfig.PLAY;
if (isPlaying && seekBar!=null && qqCircleProgressBar != null) {
int currentTime = mediaPlayerIml.getCurrentTime();
int totalTime = mediaPlayerIml.getTotalTime();
seekBar.setMax(totalTime);
seekBar.setProgress(currentTime);
qqCircleProgressBar.setMaxProgress(totalTime);
qqCircleProgressBar.setCurrentProgress(currentTime);
LoggerUtils.instance().logE("mediaPlayJindu", "mediaPlayJindu" + totalTime + "/" + currentTime);
}
sendEmptyMessageDelayed(MSG_FLUSH_SEEKBAR, MSG_FLUSH_TIME);
}
}
}
在view使用时,初始化handler,构造参数传入组件id;
m_handler = new UpdateHandler(MediaPlayerIml.getInstance(),mSeekBar,mPrograssBar);
m_handler.sendEmptyMessage(MSG_FLUSH_SEEKBAR);
在view离开窗口时候,销毁handler数据;
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
...
if(m_handler!=null){
m_handler.removeCallbacksAndMessages(null);
m_handler = null;
}
}
总结
总结我们针对此按理做的优化
- 静态Handler+弱引用,释放了对handler对view的引用,让view及时销毁,view占据的内存及时被垃圾回收器释放
- 释放了Message对view的引用,在view及时退出界面的时候,立即斩断message对view
回顾一下优化前的实例数量,多次操作,隐藏展示悬浮窗之后,内存中存在多份悬浮窗实例,之前创建过的悬浮窗内存一直无法被回收:
优化后效果,多次操作,当屏幕上存在一个view时,只存在一份view实例: