前言
在 Android 開發(fā)中,內(nèi)存泄漏這個(gè)名詞我想大家都不陌生,但是真正注意到這個(gè)問題并去解決的估計(jì)很少,因?yàn)閮?nèi)存泄漏表面上并不會表現(xiàn)出對app的任何影響,加之現(xiàn)在的手機(jī)配置與內(nèi)存都挺高的,所以對于中小型app來說,可能不怎么去處理也幾乎看不出來,但是作為一名android 開發(fā)者,你肯定和我一樣不能忍受這種瑕疵吧,那 就擼起袖子干它就完事了
內(nèi)存抖動 & 內(nèi)存泄漏 & 內(nèi)存溢出(OOM)
內(nèi)存抖動
含義:短時(shí)間內(nèi)有大量對象創(chuàng)建銷毀,它伴隨著頻繁的GC。
查看:可以使用android studio自帶的profile工具檢測。
現(xiàn)象:在profile中的內(nèi)存圖像就像是心電圖一樣,忽上忽下,如下圖所示:
常見場景:循環(huán)使用字符串拼接,比如我們項(xiàng)目的日志打印等
預(yù)防內(nèi)存抖動方法:
- 避免在循環(huán)中創(chuàng)建對象,能復(fù)用的盡量復(fù)用。
- 避免在頻繁調(diào)用的方法中創(chuàng)建對象,如自定義view中的onDraw()等方法中創(chuàng)建畫筆。
- 獲取對象盡量從對象池中獲取,如Handler獲取Message對象應(yīng)使用obtain()方法獲取了。
內(nèi)存泄漏
程序中己動態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費(fèi)。
長生命周期對象持有短生命周期對象強(qiáng)引用,從而導(dǎo)致短生命周期對象無法被回收!
查看:使用profile工具檢測內(nèi)存情況,重復(fù)執(zhí)行進(jìn)入然后退出一個(gè)activity,看activity實(shí)例是否還存在。如果activity實(shí)例還存在,很可能就出現(xiàn)了內(nèi)存泄漏。
現(xiàn)象:反復(fù)進(jìn)入A,然后退出A ,執(zhí)行三次,可以看到A 的實(shí)例存在兩個(gè)。如下圖,VideoPlayerActivity:
這就說明我們的activity并沒有被銷毀,至少目前是這樣的。至于究竟會不會內(nèi)存泄漏,就需要接下來使用另一款工具配合使用了。
- 如何判斷內(nèi)存泄漏:
- 使用可達(dá)性分析法
通過一系列稱為“GC Roots”的對象作為起始點(diǎn),從這些節(jié)點(diǎn)向下搜索,搜索所有的引用鏈,當(dāng)一個(gè)對象到GC Roots沒有任何引用鏈(即GC Roots到對象不可達(dá))時(shí),則證明此對象是不可用的。也就會被回收。
何為GC Roots 對象,一般靜態(tài)變量就是gc root對象,可以理解成生命周期很長的對象。
如何預(yù)防內(nèi)存泄漏:
使用 軟引用、弱引用間接的持有對象的引用。
軟引用:
定義一些還有用但并非必須的對象。對于軟引用關(guān)聯(lián)的對象,GC不會直接回收,而是在系統(tǒng)將要內(nèi)存溢出之前才會觸發(fā)GC將這些對象進(jìn)行回收。
- 弱引用 :
同樣定義非必須對象。被弱引用關(guān)聯(lián)的對象在GC執(zhí)行時(shí)會被直接回收。
- 造成內(nèi)存泄漏的常見場景:
- 使用集合時(shí),例如add一個(gè)監(jiān)聽器,我們必須要手動remove掉。
- 使用靜態(tài)成員變量/單利對象時(shí),如果持有短生命周期對象的引用(Activity)將導(dǎo)致短生命周期對象無法被釋放。
- 進(jìn)行文件io操作時(shí),沒有close()。最好寫在finally{ }里面;
- android 系統(tǒng)bug、第三方類庫造成的內(nèi)存泄漏。
內(nèi)存溢出
內(nèi)存溢出(Out Of Memory,簡稱OOM)是指應(yīng)用系統(tǒng)中存在無法回收的內(nèi)存或使用的內(nèi)存過多,最終使得程序運(yùn)行要用到的內(nèi)存大于能提供的最大內(nèi)存。此時(shí)程序就運(yùn)行不了,系統(tǒng)會提示內(nèi)存溢出,有時(shí)候會自動關(guān)閉軟件,重啟電腦或者軟件后釋放掉一部分內(nèi)存又可以正常運(yùn)行該軟件
- 頻繁的出現(xiàn)內(nèi)存抖動或者大量內(nèi)存泄漏很有可能就會導(dǎo)致內(nèi)存溢出(OOM)。
Android 中的垃圾回收器 CMS
android 中使用的垃圾回收器 叫做CMS ,下面簡單介紹下他的垃圾回收算法。
- 新生代對象
新生代對象采用的是復(fù)制算法,當(dāng)大對象也可能直接進(jìn)入老年代。
- 老年代對象
老年代對象采用的是標(biāo)記-清除算法,所以頻繁的內(nèi)存抖動,會造成內(nèi)存碎片化,最后可能我們需要加載一個(gè)大對象的時(shí)候,就OOM 了。
簡單介紹下CMS垃圾回收算法,如果不熟悉的建議請先百度jvm 垃圾回收機(jī)制相關(guān)知識。
實(shí)戰(zhàn) 內(nèi)存泄漏
配置環(huán)境
- android studio
- eclipse memory analyzer (mat)
下載mat
選擇相應(yīng)版本進(jìn)行下載安裝。
- 配置mat 環(huán)境,因?yàn)閺?android profile直接獲取到的hprof文件格式與mat的格式不兼容,所以需要使用工具轉(zhuǎn)換一下
win 環(huán)境配置,請自行百度,由于本人用的Mac 所以這里只寫Mac的配置
- 打開終端輸入:echo $HOME
- 繼續(xù)輸入:touch .bash_profile
- 繼續(xù)輸入: open -e .bash_profile
- 在打開的bash文件中輸入: export PATH=${PATH}:/Users/用戶名/你的sdk路徑/platform-tools
- 最后輸入: source .bash_profile
- 沒有6了,已經(jīng)成功配置了。
使用profile獲取內(nèi)存分析文件
名字隨便了,怎么方便怎么來
打開終端,進(jìn)行文件轉(zhuǎn)換
轉(zhuǎn)換格式 : hprof-conv before.hprof after.hprof
我們這里輸入 : hprof-conv memory-99.hprof 66.hprof
能看懂吧,吧我們的源文件 -99 轉(zhuǎn)換成 -66文件,
注意了 : 需要進(jìn)入-99 所在的文件目錄,要不然會報(bào)錯找不到文件
打開mat工具,導(dǎo)入我們的-66 文件
打開后可以看到這樣的界面
點(diǎn)擊紅框的選項(xiàng),這個(gè)是進(jìn)行內(nèi)存泄漏分析的
下面就是這段時(shí)間所產(chǎn)生的對象,點(diǎn)擊紅框 可以直接搜索你要分析的對象
這里找到了我們的VideoPlayerActivity
鼠標(biāo)右鍵選擇
可以看到,意思就是我們排除掉軟、弱、虛引用,因?yàn)檫@幾種是不會造成內(nèi)存泄漏的,可以不用管它,我們只需要看排除后還有沒引用存在,有的話 那就是強(qiáng)引用了,也就發(fā)生了內(nèi)存泄漏了。
繼續(xù)看我們的結(jié)果:
點(diǎn)擊后,發(fā)現(xiàn)里面存在數(shù)據(jù),那就說明我們有內(nèi)存泄漏發(fā)生了,也就是為什么上面我們已經(jīng)退出了,profile里面還有三個(gè)activity的存在,剛剛上面那張圖右側(cè)也有顯示有3個(gè)activity對象存在。
我們一一展開,看看到底哪里內(nèi)存泄漏了
可以看到,我們的Avtivity 作為mContext 變量被我們的自定義CoverVideoPlayerView 持有了,那也就是說,因?yàn)槲覀兊淖远xView不能被gc回收,所以activity也無法被回收。
那就繼續(xù)看,為什么自定義view無法被回收,可以看到,this$0,這表示在自定義view的內(nèi)部有一個(gè)非靜態(tài)內(nèi)部類,而非靜態(tài)內(nèi)部類是默認(rèn)持有外部類的引用的,也就是我們的,mNetChangeListener對象,這個(gè)就熟悉了吧,肯定是new 一個(gè)匿名內(nèi)部類啊,
繼續(xù)看,這個(gè)內(nèi)部類又被NetInfoModeule引用了,我丟,然后繼續(xù)往上看,我就不看了,你自己看吧
我去喝杯水,你慢慢看吧
啥? 我喝完回來了,你還沒看完呢? 那還是我們一起看吧,
繼續(xù)往上的話,可以看到 NetInfoModeule又被什么xxxxxBroadcastReceiver 引用了,那不就是廣播嗎? 猜想,肯定又是一個(gè)非靜態(tài)內(nèi)部類了,在往上看
咳咳,別看了,上面和我們沒啥關(guān)系了,全是系統(tǒng)在搞事情了,
通過上面一波分析,我們應(yīng)該清楚了,泄漏的原因了,
然后梳理一下:
VideoPlayerActivity -> mContext -> CoverVideoPlayerView -> mNetChangeListener -> NetInfoModeule -> xxxxxBroadcastReceiver;
引用鏈找到了,所以,我們把鏈條給整段了,就不會內(nèi)存泄漏了啊
怎么段呢?
最簡單的方式: 假如啊 ,這些個(gè)不靠譜的代碼都是你自己寫的,那好辦
在activity 的onDestory()方法中加上,
1.CoverVideoPlayerView.;
2.在CoverVideoPlayerView方法中,添加個(gè)cancel()方法,
加上
mContext = null
mNetChangeListener = null;
NetInfoModeule = null;
然后廣播記得unregister()一下,這樣不就完事了。
嗯,一切都入想象的那般甜美,but,殘酷的事實(shí)擺在眼前,這特么是第三方庫的代碼,你咋改?
下源碼改? 得 ,可行是可行,那要是很多個(gè)庫呢? 你還下不?還改不?
算了,不改了,嗯也行,反正也好像沒啥影響,而且我們的VideoPlayerActivity用的SingleTop 啟動模式,
但是你如果用戶不是在播放頁面點(diǎn)擊跳轉(zhuǎn)的呢,退出再進(jìn)來,退出再進(jìn)來,最后十幾個(gè)activity的實(shí)例,
連帶著我們的model 、viewmodel 、統(tǒng)統(tǒng)都是十幾份,你確定你不會被老大拿出去祭天?
界面如下:
那要怎么改呢? 你說說唄,
改當(dāng)然能改了,那就是用java反射,通過反射去拿到對象,進(jìn)行修改,
直接上代碼吧
/**
* 利用反射 解決gsy庫中導(dǎo)致的內(nèi)存泄漏
*/
public void cancel()
{
mAudioManager.abandonAudioFocus(onAudioFocusChangeListener);
try
{
// 拿到NetInfoModule對象中 mConnectivityBroadcastReceiver字段.
Field mConnectivityBroadcastReceiver = NetInfoModule.class
.getDeclaredField("mConnectivityBroadcastReceiver");
// 由于是私有字段,所以需要調(diào)用setAccessible(true),否則會報(bào)錯
mConnectivityBroadcastReceiver.setAccessible(true);
// 根據(jù)當(dāng)前mNetInfoModule對象的 mConnectivityBroadcastReceiver字段值為null
mConnectivityBroadcastReceiver.set(mNetInfoModule, null);
Field mNetChangeListener =
NetInfoModule.class.getDeclaredField("mNetChangeListener");
mNetChangeListener.setAccessible(true);
mNetChangeListener.set(mNetInfoModule, null);
}
catch (NoSuchFieldException e)
{
e.printStackTrace();
}
catch (IllegalAccessException e)
{
e.printStackTrace();
}
mAudioManager = null;
mContext = null;
}
好了,改完了,我們在來抓一下內(nèi)存情況
直接上圖:
看見沒有,啥也沒有了,說明我們的activiy再沒有被其他對象引用了,說的不對,糾正一下,是沒有被其他對象強(qiáng)引用了,而只要沒有強(qiáng)引用關(guān)系,gc肯定能夠回收的,自然你就也不需要擔(dān)心內(nèi)存泄漏了,
但是為什么還是會顯示有3個(gè)activity對象存在呢?
那我們排除一下弱應(yīng)用,看一看
看見沒有,這里有一個(gè)FinalizeRefernce對象,說明我們的activity被FinalizeRefernce對象所引用,而我們知道,只要一個(gè)對象將要被gc回收了,那么他就會被這個(gè)FinalizeRefernce所引用,這是為了讓gc知道我們不需要這個(gè)對象了,你可以回收了,
所以我們的activity 雖然顯示有兩個(gè),但是只是gc還沒來的及回收而已,并沒有內(nèi)存泄漏的風(fēng)險(xiǎn)。
上面的例子是我的開源項(xiàng)目里的真實(shí)實(shí)例,感興趣的可以前往支持 start 一下,
項(xiàng)目地址
總結(jié)
內(nèi)存抖動的分析用profile工具完全就夠用了,當(dāng)然我們這里沒有詳講怎么用profile分析解決內(nèi)存抖動,之后有時(shí)間在補(bǔ)上吧。而我們的內(nèi)存泄漏通過profile是不能看出他到底有沒有發(fā)生內(nèi)存泄漏的,所以我們還需要借助mat工具進(jìn)一步分析。
當(dāng)然還有一些第三方的檢測庫,比較知名的比如leakCannary工具,騰訊的xxxdog,但是個(gè)人還是推薦使用mat來分析,mat操作起來也很方便,好了,今天就到這吧,感謝!
歡迎關(guān)注作者darryrzhong,更多干貨等你來拿喲.
請賞個(gè)小紅心!因?yàn)槟愕墓膭钍俏覍懽鞯淖畲髣恿Γ?/h3>
更多精彩文章請關(guān)注
更多精彩文章請關(guān)注