最近在項(xiàng)目中偶爾會(huì)發(fā)現(xiàn)內(nèi)存泄漏現(xiàn)象。一開始還是一臉懵逼的查來(lái)查去郁稍,一直沒(méi)有個(gè)清晰地思路。這幾天閑下來(lái)胜宇,打算認(rèn)真整理學(xué)習(xí)一下艺晴。我在這里從一個(gè)“如何主動(dòng)造成內(nèi)存泄漏”的角度來(lái)學(xué)習(xí),然后熟悉一下不同方法檢測(cè)的結(jié)果如何掸屡,這樣以后再遇到相關(guān)問(wèn)題時(shí)就能夠很快的解決了封寞。
java gc
首先要有一個(gè)大前提,也就是java gc仅财。在大部分虛擬機(jī)(包括Android的ART)中狈究,Java都采用了“可達(dá)性分析”算法來(lái)進(jìn)行內(nèi)存回收,原理是:會(huì)有幾個(gè)引用作為root節(jié)點(diǎn)盏求,對(duì)于任意對(duì)象來(lái)說(shuō)抖锥,如果從root層層遍歷,如果找不到對(duì)于他的引用鏈碎罚,那么這個(gè)對(duì)象就被標(biāo)記為無(wú)用磅废,就會(huì)在gc時(shí)被銷毀。
何為泄漏
內(nèi)存泄漏荆烈,即部分對(duì)象雖然已經(jīng)不再使用拯勉,但是因?yàn)橛衦oot持有引用,所以并沒(méi)有被銷毀憔购,所占用的內(nèi)存一直沒(méi)有被釋放宫峦。一次兩次發(fā)生影響不大。如果頻繁發(fā)生玫鸟,那么可用內(nèi)存會(huì)漸漸不足导绷,最終在某一次請(qǐng)求內(nèi)存時(shí)發(fā)現(xiàn)內(nèi)存不足而發(fā)生oom。這里要明確一個(gè)概念屎飘,只有強(qiáng)引用會(huì)發(fā)生內(nèi)存泄漏妥曲,而weak等引用因?yàn)槠涮厥鈾C(jī)制贾费,所以影響不大。
什么會(huì)泄露
泄露影響比較大的就是一些大對(duì)象檐盟,常見(jiàn)的比如某些資源褂萧,bitmap,以及activity遵堵。
如何發(fā)生泄露
首先讓我們從另一個(gè)角度來(lái)看箱玷,如何主動(dòng)發(fā)生內(nèi)存泄漏呢怨规?當(dāng)然是想辦法給他一個(gè)一直存在的強(qiáng)引用了陌宿。
static
static這個(gè)關(guān)鍵字使一個(gè)變量變?yōu)橹缓瓦@個(gè)類相關(guān)的類變量,和實(shí)例無(wú)關(guān)波丰。他的生命周期是很長(zhǎng)的壳坪,貫穿于app的啟動(dòng)到關(guān)閉。因此只要用一個(gè)static引用一個(gè)大對(duì)象掰烟,就可以泄漏了爽蝴!舉個(gè)例子:
static Activity activity;
這是最簡(jiǎn)單粗暴的持有一個(gè)activity的引用,這樣這個(gè)activity退出之后對(duì)象并沒(méi)有被銷毀纫骑。
static View view;
一個(gè)View初始化時(shí)會(huì)用到context蝎亚,我們?cè)谧远xView,重寫構(gòu)造方法時(shí)就知道這個(gè)了先馆。因此如果一個(gè)View也像這樣被持有发框,那個(gè)context也不會(huì)被釋放。
innerClass
內(nèi)部類有個(gè)特性煤墙,是他會(huì)持有一個(gè)外部類的引用梅惯。如果內(nèi)部類的實(shí)例一直存活,那么外部類activity的實(shí)例也就一直在仿野。比如持有一個(gè)static的內(nèi)部類引用:
static LeakInnerClass context;
class LeakInnerClass {
Context context;
}
或者以前我們用asynctask時(shí)喜歡搞一個(gè)匿名內(nèi)部類執(zhí)行異步任務(wù)铣减,那當(dāng)我們activity退出后這個(gè)異步任務(wù)還在執(zhí)行的話,就會(huì)泄露了脚作。
void leakAsyncTask(){
new AsyncTask<Void,Void,Void>(){
@Override
protected Void doInBackground(Void... params) {
while(true){
//哇啦啦啦啦啦啦我就是耗時(shí)操作
}
return null;
}
};
}
還有自己開個(gè)匿名線程:
void leakThread(){
new Thread(){
@Override
public void run() {
while (true){
//哇啦啦啦啦啦啦我是耗時(shí)操作
}
}
}.start();
}
還有在使用handler時(shí)葫哗,如果用了匿名handler,那么這個(gè)handler會(huì)帶著activity的引用藏到消息隊(duì)列中球涛。消息沒(méi)有被處理魄梯,就會(huì)造成內(nèi)存泄漏。類似的宾符,還有timertask等酿秸。
register
我們平時(shí)會(huì)用到很多第三方庫(kù),比如ButterKnife EventBus RxJava等等魏烫,有的時(shí)候要獲取系統(tǒng)服務(wù)辣苏,getSystemService肝箱。在使用的時(shí)候,都有一個(gè)先registerd或者bind的操作稀蟋,而且在創(chuàng)建的時(shí)候會(huì)把a(bǔ)ctivity的引用傳過(guò)去煌张。如果在activity結(jié)束時(shí)沒(méi)有unregister或者unbind,就會(huì)造成內(nèi)存泄漏退客。
如何檢測(cè)泄漏
最簡(jiǎn)單的方法自然就是使用leakcanary了骏融。只要給自己的項(xiàng)目加上這個(gè)工具,在發(fā)生泄漏的時(shí)候很快就會(huì)有提示萌狂。具體使用方法看這里档玻。
除此之外,android studio的刀耕火種的方式也不錯(cuò)茫藏,在這里我拿一個(gè)例子來(lái)示范一下我是怎么用的误趴。
一次leak檢測(cè)過(guò)程
準(zhǔn)備工作
首先,我寫了兩個(gè)activity务傲,一個(gè)MainActivity凉当,一個(gè)MemoryLeakActivity,邏輯是:MainActivity中有個(gè)按鈕售葡,點(diǎn)擊會(huì)調(diào)到MemoryLeakActivity看杭,在這個(gè)activity中會(huì)故意發(fā)生內(nèi)存泄漏,代碼如下:
在開始之前挟伙,再熟悉一下這個(gè)
(原諒我拙劣的畫筆)
這個(gè)Monitors可以觀察當(dāng)前選中app的運(yùn)行狀態(tài)楼雹,現(xiàn)在只需要關(guān)注我標(biāo)了123的地方。
首先這個(gè)Memory就是當(dāng)前app的內(nèi)存使用狀況:
1.產(chǎn)生一個(gè)當(dāng)前java堆的.hprof文件像寒,這個(gè)文件反映了當(dāng)前時(shí)刻java堆中內(nèi)存詳情烘豹,記住這個(gè)玩意有大用!
2.手動(dòng)進(jìn)行一次gc
3.這一塊很重要诺祸,首先他有兩個(gè)部分携悯,藍(lán)色和灰色。藍(lán)色部分是當(dāng)前內(nèi)存使用大小筷笨,灰色部分是這個(gè)app被限制的最大內(nèi)存大小憔鬼。當(dāng)藍(lán)色部分越來(lái)越大,最后和灰色部分一樣時(shí)胃夏,說(shuō)明我們內(nèi)存使用很多了即將內(nèi)存不足轴或,此時(shí)會(huì)進(jìn)行一次gc同時(shí)將回灰色部分即限制的大小提高。
肉眼觀察
好了仰禀,介紹完這個(gè)工具照雁,我們開始動(dòng)手實(shí)踐。首先打開app答恶,點(diǎn)擊按鈕跳到會(huì)發(fā)生泄漏的activity上饺蚊,再按返回鍵萍诱,然后再次按下按鈕……這樣反復(fù)操作:
與此同時(shí),觀察monitors的memory窗口污呼,會(huì)發(fā)現(xiàn)藍(lán)色部分在每一次開啟新activity時(shí)會(huì)增長(zhǎng)一部分裕坊,這很正常。但是在返回時(shí)燕酷,明明activity被“退出”了籍凝,但是藍(lán)色部分還是沒(méi)有變化。反復(fù)幾次之后苗缩,藍(lán)色部分一直在增長(zhǎng)饵蒂。也就是說(shuō)當(dāng)前內(nèi)存越用越多,可以推斷已經(jīng)發(fā)生內(nèi)存泄漏啦~
自動(dòng)分析
接下來(lái)由android studio來(lái)分析一下挤渐。在反復(fù)幾次上面的操作之后苹享,返回MainActivity双絮,然后點(diǎn)擊dump java heap按鈕浴麻,然后等一會(huì)兒,android studio在為我們dump此時(shí)的horof文件囤攀。在成功后软免,會(huì)自動(dòng)打開:
如圖在這個(gè)界面中,我們看最右面有一個(gè)欄叫 Analyzer Tasks焚挠,打開它膏萧,會(huì)發(fā)現(xiàn)有兩個(gè)選項(xiàng)。我們是來(lái)看activity的內(nèi)存泄漏的蝌衔,那就把那個(gè)查重復(fù)字符串的√去掉榛泛。然后點(diǎn)右邊那個(gè)綠色小三角,會(huì)發(fā)現(xiàn)下面Analysis Results欄里面展示出了當(dāng)前泄露的Activity引用:
點(diǎn)擊第一個(gè)item噩斟,最下方Reference Tree欄中便展示出了具體的引用:
一般來(lái)說(shuō)曹锨,第一個(gè)就是我們發(fā)生泄漏的地方。在圖中剃允,this$0的意思是隱式的引用沛简。也就是說(shuō),我們的activity是因?yàn)橐粋€(gè)內(nèi)部類而發(fā)生了內(nèi)存泄漏斥废。
再點(diǎn)擊剛才results中第二個(gè)item椒楣,看一下下方的reference tree:
可以看到顯式的有一個(gè)leakCntextRef引用,這說(shuō)明我們有一個(gè)名為leakCntextRef的引用持有了activity牡肉∨趸遥回過(guò)頭看看我們的代碼,果然统锤,驗(yàn)證的沒(méi)錯(cuò)毛俏。
拓展
android studio的分析還算比較簡(jiǎn)單而且內(nèi)容較少吩屹,我們可以把這個(gè)hprof導(dǎo)出,然后用mat來(lái)分析拧抖,具體看這里煤搜。
怎么解決泄漏
既然發(fā)生了泄漏,那就要解決它唧席,避免問(wèn)題出現(xiàn)擦盾。那么怎么解決呢?很簡(jiǎn)單淌哟,泄漏是因?yàn)槌钟辛薬ctivity引用導(dǎo)致無(wú)法被銷毀迹卢,那么只有兩個(gè)選擇:及時(shí)取消引用,或者讓這個(gè)引用多待一會(huì)徒仓,但是該gc的時(shí)候就銷毀腐碱。
根據(jù)這個(gè)思路:
- 我們?cè)诖a中能不用static變量持有contxt就不用,非要用就用weak引用掉弛。
- 對(duì)于內(nèi)部類症见,盡量用靜態(tài)內(nèi)部類,這樣就不會(huì)持有外部類引用殃饿。如果需要外部類引用做一些事谋作,就手動(dòng)賦給一個(gè)weak引用。
- 對(duì)于匿名內(nèi)部類乎芳,不要圖簡(jiǎn)單方便遵蚜,實(shí)在不行就乖乖的寫成外部類。
- 異步操作奈惑,盡量用可以方便管理的吭净,比如rxJava,而不是用老古董AsyncTask了肴甸。非要用也最好加一個(gè)終止條件寂殉,在退出Activity時(shí)就該結(jié)束了。
- 在用rx時(shí)雷滋,可以在subscribe()的時(shí)候獲取到Subscripeion不撑,在不用的時(shí)候手動(dòng)unSubscribe(),或者直接bind()到Activity的生命周期上晤斩,比如使用RxActivity管理焕檬。
- 在使用handler時(shí),記得在activity的onDestroy()中加上remove()
- 在獲取到某些資源時(shí)澳泵,使用完記得釋放
- 在用到一些大對(duì)象比如Bitmap啊什么的实愚,要記得回收
- 最后,在使用各種第三方庫(kù)或者系統(tǒng)服務(wù)的時(shí)候還要記得有注冊(cè)或綁定就要有解除注冊(cè)、解綁定腊敲。