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