濟南做網站的公司電腦全自動掛機賺錢
案例3——view內存泄漏
前文提到,profile#Leaks視圖無法展示非Activity、非Fragment的內存泄漏,換言之,除了Activity、Fragment的內存泄漏外,其他類的內存問題我們只能自己檢索hprof文件查詢了。
下面有一個極佳的view內存泄漏例子,它的操作步驟為:
- 播放音樂,喚醒音樂懸浮窗
- 播放一段時間后,關閉音樂懸浮窗
- 重復步驟1和2
我們重復三次之后,得到一份hprof文件,下面我們來分析一下內存泄漏問題
①輸入view的名稱
②選擇view
③可以看到分配了3個實例對象
④Instance List視圖顯示,view有3個實例對象及其引用
我們從上至下依次看3分實例的調用鏈
第一個泄漏點
view的第一個實例
先查看Fields區(qū)域,觀察mLayoutmode值,判斷view是否離開了窗口,如果已經離開了窗口,表明view未被回收,存在內存泄漏
可以看到mLayoutMode = -1 ,表明布局已經離開屏幕了,此實例存在內存泄漏的情況
接著我們查看References區(qū)域,逐級點開我們發(fā)現(xiàn)Handler發(fā)送的Message持有了當前view,導致view在離開窗口的時候,無法被垃圾回收器回收。
右鍵點擊查看問題代碼
問題代碼:
playHandler.post(new Runnable() {@Overridepublic void run() {tv_play.setText(playItem.getProgramTitle());tb_play.setSelected(true);initView();}});
看到new Runnbale,這是是匿名內部類,匿名內部類持有當前類的引用,匿名Runnbale未執(zhí)行完畢,Runnbale內存未釋放的時候,view就無法被釋放,而匿名Runnbale的釋放時機不可控,由Handler、Looper、Runnbale執(zhí)行情況影響。
那么我們該怎么優(yōu)化呢?
- 使用非匿名或靜態(tài)的Handler+弱引用,處理此任務
- 在主線程處理此任務
- view退出的時候釋放Message對view的引用
筆者采用了方案3:
tv_play.setText(playItem.getProgramTitle());
tb_play.setSelected(true);
initView();
方案1代碼與下面view的第三個實例寫法一致,不重復寫了;我們解釋一下方案3:
view退出的時候釋放Message對view的引用
根據(jù)上圖所示,我們看到Message-Runnbale-View的引用關系可知,Looper中的Message持續(xù)的引用view,我們最高效釋放內存的做法是view離開窗口的時候,斬斷Message與view的引用關系,那么我們該怎么做呢?答案是:
- 結束子線程任務
- 清空Looper緩存的Message
- 釋放Handler
第一步:結束子線程任務很簡單
thread.interrupt()
本案例給Handler傳入的是Runnbale,Handler未提供結束Runnbale的接口,此項優(yōu)化擱置
第二步:清空Message
已知Looper提供了清空Message的接口
- Looper#quit
- Looper#quitSafely
- 主線程的Looper無法退出
已知Handler提供了釋放Message的接口 - Handler#removeCallbacksAndMessages
那我們優(yōu)化起來就很簡單了,清空Handler持有的Message
@Override protected void onDetachedFromWindow() {
super.onDetachedFromWindow(); ... // 釋放message,斷開message-Runnbale-view的引用鏈 if (playHandler != null) { playHandler.removeCallbacksAndMessages(null); playHandler = null; }}
第二個泄漏點
我們繼續(xù)看view的第二個實例
先查看Fields區(qū)域,觀察mLayoutmode值,判斷view是否離開了窗口,如果已經離開了窗口,表明view未被回收,存在內存泄漏
可以看到mLayoutMode = -1 ,表明布局已經離開屏幕了,此實例存在內存泄漏的情況
接著我們看References區(qū)域,觀察調用鏈
可以看到MediaPlayerIml有一個成員變量mMediaPlayListenerCacheList,緩存了MediaPlayListener,MediaPlayListener又是在view實例里面創(chuàng)建的,并且作為內部類,它持有view的實例。現(xiàn)在我們得到了清晰的調用鏈,MediaPlayerIml->mMediaPlayListenerCacheList->MediaPlayListener->view,MediaPlayerIml引用view導致view實例無法被釋放
查看問題代碼:
筆者發(fā)現(xiàn)view#onDetachedFromWindow已經觸發(fā)了移除list#listener操作
@Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mediaPlayerIml.unregisterListener(playListener);
可以看到內部實現(xiàn)是remove調引用的
/*** 取消注冊listener** @param listener*/public synchronized void unregisterListener(MediaPlayListener listener) {mMediaPlayListenerCacheList.remove(listener);}
那為什么會未回收持續(xù)占用內存呢?
- 抓拍hprof文件期間,代碼未執(zhí)行到unregisterListener,導致view內存未得到釋放
- mMediaPlayListenerCacheList添加的listener與remove的listener不是同一個
- 此處沒有產生內存泄漏,判斷view是否應該被回收的依據(jù)有問題
第三個泄漏點
擱置疑問,接著我們來看view的第三個實例,節(jié)省時間,筆者直接調到代碼索引出,展示問題代碼:
/*** 播放進度條刷新控*/private Handler m_handler = new Handler() {@Overridepublic 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;}}};
可以看到此處還是使用了非靜態(tài)內部類m_handler,m_handler持有當前view 的引用,m_handler如果長期存在,那么view的內存也不會被釋放
解決方法如下:
- 定義外部類Handler
- 定義靜態(tài)內部類
- 定義靜態(tài)內部類+弱引用
筆者采用了方案3:
定義靜態(tài)內部類
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);}@Overridepublic 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,構造參數(shù)傳入組件id;
m_handler = new UpdateHandler(MediaPlayerIml.getInstance(),mSeekBar,mPrograssBar);
m_handler.sendEmptyMessage(MSG_FLUSH_SEEKBAR);
在view離開窗口時候,銷毀handler數(shù)據(jù);
@Overrideprotected void onDetachedFromWindow() {super.onDetachedFromWindow();...if(m_handler!=null){m_handler.removeCallbacksAndMessages(null);m_handler = null;}}
總結
總結我們針對此按理做的優(yōu)化
- 靜態(tài)Handler+弱引用,釋放了對handler對view的引用,讓view及時銷毀,view占據(jù)的內存及時被垃圾回收器釋放
- 釋放了Message對view的引用,在view及時退出界面的時候,立即斬斷message對view
回顧一下優(yōu)化前的實例數(shù)量,多次操作,隱藏展示懸浮窗之后,內存中存在多份懸浮窗實例,之前創(chuàng)建過的懸浮窗內存一直無法被回收:
優(yōu)化后效果,多次操作,當屏幕上存在一個view時,只存在一份view實例: