Android內(nèi)存泄漏原理分析和優(yōu)化實(shí)踐

內(nèi)存泄漏是Android開發(fā)必須重視的問題,它可能導(dǎo)致應(yīng)用性能低下,內(nèi)存抖動(dòng),甚至OOM.如何檢測(cè)和分析OOM是必須要掌握的知識(shí).本文將從三個(gè)方面來介紹內(nèi)存泄漏問題,分別是內(nèi)存泄漏的基礎(chǔ)知識(shí),常見的內(nèi)存泄漏場(chǎng)景和內(nèi)存泄漏分析和解決辦法.

一. 內(nèi)存泄漏的基本知識(shí)

1. 什么是內(nèi)存泄漏堂油?

沒有用的對(duì)象無法被回收

2. 內(nèi)存泄漏的危害?

? 應(yīng)用可用的內(nèi)存減少,增加了堆內(nèi)存的壓力
? 嚴(yán)重的時(shí)候可能會(huì)導(dǎo)致OOM Error
? 觸發(fā)更頻繁的GC 雪侥,造成卡頓,內(nèi)存抖動(dòng)等現(xiàn)象

3. 為什么會(huì)內(nèi)存泄漏稼稿?

長(zhǎng)生命周期的對(duì)象持有短生命周期對(duì)象“強(qiáng)/軟引用”,導(dǎo)致本應(yīng)該被回收的短生命周期的對(duì)象卻無法被正呈扌梗回收。

4. 對(duì)象什么時(shí)候被回收——可達(dá)性分析算法介紹
可達(dá)性分析算法

能夠被GC Root引用的對(duì)象是否可達(dá)


GC Root

由GCRoot不得不說一下Java內(nèi)存模型了


java內(nèi)存模型

簡(jiǎn)單的介紹一下:
1.程序計(jì)數(shù)器:線程私有刀闷,記錄各個(gè)線程運(yùn)行的位置
2.虛擬機(jī)棧:每個(gè)method,線程私有描姚,存放局部變量涩赢,方法結(jié)束后自動(dòng)釋放內(nèi)存。
3.本地方法棧:native method轩勘。
4.方法區(qū):它主要存放靜態(tài)數(shù)據(jù)筒扒,全局變量,編譯時(shí)就分配好绊寻,在程序整個(gè)運(yùn)行期間都存在花墩。
5.堆:通常用來存放 new 出來的對(duì)象。由 GC 負(fù)責(zé)回收澄步。

由長(zhǎng)生命周期的對(duì)象持有短生命周期對(duì)象“強(qiáng)/軟引用”,介紹一下四種引用類型:

二,常見的內(nèi)存泄漏場(chǎng)景

注意:Activity 內(nèi)存泄漏預(yù)防

Activity內(nèi)部持有大量的資源引用以及與系統(tǒng)交互的 Context冰蘑,這會(huì)導(dǎo)致一個(gè) Activity 對(duì)象的 retained size 特別大。一旦 Activity 因?yàn)楸煌獠肯到y(tǒng)所持有而導(dǎo)致發(fā)生內(nèi)存泄漏村缸,被牽連導(dǎo)致其他對(duì)象的內(nèi)存泄漏也會(huì)非常多祠肥。
這里介紹一下Retained Size:

Shallow size:對(duì)象本身占用內(nèi)存的大小。 Retained Size: 對(duì)象本身的Shallow Size + 對(duì)象能直接或間接訪問到的對(duì)象的Shallow Size

1.靜態(tài)變量造成的內(nèi)存泄漏——將 Context 或者 View 置為 static梯皿,

原因:靜態(tài)變量的生命周期與Application相同仇箱,編譯時(shí)就分配好县恕,在程序整個(gè)運(yùn)行期間都存在。

View 默認(rèn)會(huì)持有一個(gè) Context 的引用剂桥,如果將其置為 static 將會(huì)造成 View 在方法區(qū)中無法被快速回收忠烛,最終導(dǎo)致 Activity 內(nèi)存泄漏。

由于static handler中要使用view進(jìn)行更新权逗,這里提供的方法是去掉static 修飾美尸,提供一個(gè)方法,獲得對(duì)應(yīng)的對(duì)象斟薇。

2. 非靜態(tài) Handler 導(dǎo)致 Activity 泄漏

原因:由于 Handler 屬于 TLS(Thread Local Storage)變量师坎,對(duì)應(yīng)GCRoot:仍處于存活狀態(tài)中的線程對(duì)象,導(dǎo)致它的生命周期和 Activity 不一致奔垦。

非靜態(tài)內(nèi)部類屹耐,持有外部類的強(qiáng)引用。因此通過 Handler 來更新 UI 一般很難保證跟 View 或者 Activity 的生命周期一致椿猎,故很容易導(dǎo)致無法正確釋放惶岭。

3.單例或三方庫(kù)使用Activity context

原因:由于單例模式的靜態(tài)特性,使得它的生命周期和我們的應(yīng)用一樣長(zhǎng)犯眠,一不小心讓單例無限制的持有 Activity 的強(qiáng)引用就會(huì)導(dǎo)致內(nèi)存泄漏按灶。
解決方案:把傳入的 Context 改為同應(yīng)用生命周期一樣長(zhǎng)的 Application 中的 Context。兩種方式:
1.context.getApplicationContext()筐咧。

  1. 通過重寫 Application鸯旁,提供 getContext ()方法,那樣就不需要在獲取單例時(shí)傳入 context。

提示:
1.在實(shí)現(xiàn) SDK 時(shí)量蕊,也盡量避免造成外部 Context 的泄漏铺罢。對(duì)傳入的Context,主動(dòng)轉(zhuǎn)換為application Context
2.并不是所有的context能用applicationContext残炮。

Context使用規(guī)則

? NO1 表示 Application 和 Service 可以啟動(dòng)一個(gè) Activity韭赘,不過需要?jiǎng)?chuàng)建一個(gè)新的 task 任務(wù)隊(duì)列。
? 對(duì)于 Dialog 而言势就,只有在 Activity 中才能創(chuàng)建泉瞻。

5.非靜態(tài)內(nèi)部類和匿名類導(dǎo)致的內(nèi)存泄漏

使用非靜態(tài)內(nèi)部類和匿名類都會(huì)默認(rèn)持有外部類的引用,如果生命周期不一致苞冯,就會(huì)導(dǎo)致內(nèi)存泄漏袖牙。

非靜態(tài)內(nèi)部類默認(rèn)會(huì)持有外部類的引用,而外部類中又有一個(gè)該非靜態(tài)內(nèi)部類的靜態(tài)實(shí)例舅锄,該靜態(tài)實(shí)例的生命周期和應(yīng)用的一樣長(zhǎng)鞭达,而靜態(tài)實(shí)例又持有Activity 的引用,因此導(dǎo)致 Activity 的內(nèi)存資源不能正常回收碉怔。

將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類 也可以將該內(nèi)部類抽取出來封裝成一個(gè)單例烘贴,Handler 泄漏也屬于這種.

6. Listener 注冊(cè)和解綁的聲明周期不一致導(dǎo)致

Activity的onResume 注冊(cè),在onDestory 解綁撮胧,導(dǎo)致多次注冊(cè)listener 并添加到對(duì)應(yīng)的list中,只解鎖最后一個(gè)導(dǎo)致內(nèi)存泄漏老翘。

6.廣播芹啥,F(xiàn)ile,IO流铺峭,Cursor, Listener資源未釋放

三,內(nèi)存泄漏檢測(cè)分析實(shí)踐

3.1 分析已知的內(nèi)存泄漏

方式一:MAT分析和使用技巧

MAT是Memory Analyzer tool的縮寫墓怀,是一種快速,功能豐富的Java堆分析工具卫键,能幫助你查找內(nèi)存泄漏和減少內(nèi)存消耗傀履。

很多情況下,需要用hprof-conv處理測(cè)試提供的hprof文件莉炉,否則會(huì)報(bào)下邊面的錯(cuò)誤钓账。

MAT 的使用技巧

技巧一:通過OQL(Objective Query Language )查詢對(duì)象

由于內(nèi)存泄漏一般發(fā)生在Activity中,查詢Activity對(duì)象

點(diǎn)擊下圖中標(biāo)記的OQL圖標(biāo) 輸入 select * from instanceof android.app.Activity絮宁,感嘆號(hào)執(zhí)行查詢梆暮。
看到this0引用了這個(gè)Activity而this0是表示內(nèi)部類的意思,this$0又被mLisener引用

技巧二:通過搜索關(guān)鍵字發(fā)現(xiàn)內(nèi)存泄漏對(duì)象

方式二:Android Profile 內(nèi)存泄漏分析

Android Profiler是Android Studio3.0用來替換之前Android Monitor的觀察工具绍昂,Android Profile 可以分析CPU啦粹,Memory,Network窘游,Energy

1.點(diǎn)擊View > Tool Windows > Profiler或者點(diǎn)擊工具欄的圖標(biāo)即可打開Load file 或者進(jìn)程

Android Profiler hprof內(nèi)存泄漏分析

Android Profile 進(jìn)程內(nèi)存泄漏分析

這里GC的原因是回收掉軟/弱/虛引用唠椭。

3.2 尋找并解決未知的內(nèi)存泄漏

LeakCanary + Monkey + MAT/Profiler
LeakCanary介紹和使用
LeakCanary 是 Square 公司的一個(gè)開源庫(kù)。通過它可以在 App 運(yùn)行過程中檢測(cè)內(nèi)存泄漏忍饰,當(dāng)內(nèi)存泄漏發(fā)生時(shí)會(huì)生成發(fā)生泄漏對(duì)象的引用鏈贪嫂,
并通知程序開發(fā)人員。

  1. App build.gradle 添加依賴喘批,在logcat查看LeakCanary關(guān)鍵字確定是否集成成功
    dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
    }
  2. Android TV 注意項(xiàng)

Monkey簡(jiǎn)介

monkey -p com.mediatek.wwtv.tvcenter -c android.intent.category.DEFAULT --throttle 500 --pct-nav 25 --pct-majornav 25 --pct-syskeys 25
--pct-appswitch 25 --ignore-crashes --ignore-timeouts --ignore-security-exceptions --kill-process-after-error -v -v 10000
命令解釋:

-c 指定activity的category類別撩荣,注意activity應(yīng)該指定,否則測(cè)試的腳本不會(huì)執(zhí)行該Activity
--throttle:后面接時(shí)間饶深,單位為ms,表示事件之間的固定延遲(即執(zhí)行每一個(gè)指令間隔的時(shí)間)餐曹,如果不接該項(xiàng),monkey將不會(huì)延遲
--pct-nav:后面接基本導(dǎo)航事件百分比敌厘,主要來自方向輸入設(shè)備的上台猴、下、左、右事件
--pct-marjornav:后面接主要導(dǎo)航事件百分比饱狂,通常指引發(fā)圖形界面的一些動(dòng)作曹步,如鍵盤中間按鍵、返回按鍵休讳、菜單按鍵等
--pct-syskeys:后面接系統(tǒng)按鍵事件百分比讲婚,通常指僅供系統(tǒng)使用的保留按鍵,如HOME鍵俊柔、BACK鍵筹麸、撥號(hào)鍵、掛斷鍵雏婶、音量鍵等
--pct-appswitch:后面接應(yīng)用啟動(dòng)事件百分比物赶,應(yīng)用啟動(dòng)事件(activity launches)即打開應(yīng)用,通過調(diào)用startActivity()方法最大限度地開啟該package下的所有應(yīng)用
--pct-touch:后面接觸摸事件百分比留晚,觸摸事件泛指發(fā)生在某一位置的一個(gè)down-up事件酵紫,點(diǎn)擊
--pct-motion:后面接動(dòng)作事件百分比,動(dòng)作事件泛指從某一位置接下(即down事件)后經(jīng)過一系列偽隨機(jī)事件后彈出(即up事件)
-v -v指定monkey報(bào)告等級(jí)错维,一個(gè) -v增加一個(gè)級(jí)別奖地,默認(rèn)缺省值是0級(jí),
-v需五,Level 0(缺省值)鹉动,除啟動(dòng)提示、測(cè)試完成和最終結(jié)果之外宏邮,提供較少信息
-v -v泽示,Level 1,提供較為詳細(xì)的測(cè)試信息蜜氨,如:逐個(gè)發(fā)送到Activity的事件
-v -v -v械筛,Level 2,提供更加詳細(xì)的設(shè)置信息飒炎,如:測(cè)試中被選中的或未被選中的Activity

3. TV 端查看泄漏列表詳情

adb shell am start -n "com.xxx/leakcanary.internal.activity.LeakLauncherActivity"

泄漏詳情


可以看到GC root和引用鏈.

Logcat也可查看泄漏

這里可以看到hprof文件位置,如果不是很明顯可以結(jié)合MAT或profiler進(jìn)行分析埋哟。

LeakCanary 原理淺析

LeakCanary 號(hào)稱在App 運(yùn)行過程中檢測(cè)內(nèi)存泄漏,當(dāng)內(nèi)存泄漏發(fā)生時(shí)會(huì)生成發(fā)生泄漏對(duì)象的引用鏈那它如何檢測(cè)內(nèi)存泄漏郎汪?
先說一下WeakReference和ReferenceQueue

WeakReference 的構(gòu)造函數(shù)可以傳入 ReferenceQueue赤赊,當(dāng) WeakReference 指向的對(duì)象被垃圾回收器回收時(shí),會(huì)把 WeakReference 放入ReferenceQueue 中
輸出:

before gc, reference.get is com.danny.lagoumemoryleak.WeakRefDemo$BigObject@7852e922
before gc, queue is null
after gc, reference.get is null
after gc, queue is java.lang.ref.WeakReference@4e25154f

使用強(qiáng)引用對(duì)象:

log輸出

before gc, reference.get is com.danny.lagoumemoryleak.WeakRefDemo BigObject@7852e922
before gc, queue is null
after gc, reference.get is com.danny.lagoumemoryleak.WeakRefDemo$BigObject@7852e922
after gc, queue is null

可以看出未GC成功,queue為空.這里請(qǐng)注意一下,這里的bigobject對(duì)象有強(qiáng)引用和虛引用兩種對(duì)象,只有只包含虛引用的對(duì)象在GC時(shí)會(huì)被回收.

LeakCanary 中對(duì)內(nèi)存泄漏檢測(cè)的核心原理就是基于 WeakReference 和 ReferenceQueue 實(shí)現(xiàn)的煞赢。
1.當(dāng)一個(gè) Activity 需要被回收時(shí)抛计,就將其包裝到一個(gè) WeakReference 中,并且在 WeakReference 的構(gòu)造器中傳入自定義的 ReferenceQueue照筑。
2.然后給包裝后的 WeakReference 做一個(gè)標(biāo)記 Key吹截,并且在一個(gè)強(qiáng)引用 Set 中添加相應(yīng)的 Key 記錄
3.最后主動(dòng)觸發(fā) GC瘦陈,遍歷自定義 ReferenceQueue 中所有的記錄,并根據(jù)獲取的 Reference 對(duì)象將 Set 中的記錄也刪除
經(jīng)過上面 3 步之后波俄,還保留在 Set 中的就是:應(yīng)當(dāng)被 GC 回收晨逝,但是實(shí)際還保留在內(nèi)存中的對(duì)象,也就是發(fā)生泄漏了的對(duì)象懦铺。
在一個(gè)Activity執(zhí)行完onDestroy()之后捉貌,將它放入WeakReference中,然后將這個(gè)WeakReference類型的Activity對(duì)象與
ReferenceQueque關(guān)聯(lián)冬念。這時(shí)再?gòu)腞eferenceQueque中查看是否有沒有該對(duì)象昏翰,如果沒有,執(zhí)行g(shù)c刘急,再次查看,還是沒有的話則判
斷發(fā)生內(nèi)存泄漏了浸踩。最后用HAHA這個(gè)開源庫(kù)去分析dump之后的heap內(nèi)存叔汁。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市检碗,隨后出現(xiàn)的幾起案子据块,更是在濱河造成了極大的恐慌,老刑警劉巖折剃,帶你破解...
    沈念sama閱讀 206,013評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件另假,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡怕犁,警方通過查閱死者的電腦和手機(jī)边篮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來奏甫,“玉大人戈轿,你說我怎么就攤上這事≌笞樱” “怎么了思杯?”我有些...
    開封第一講書人閱讀 152,370評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)挠进。 經(jīng)常有香客問我色乾,道長(zhǎng),這世上最難降的妖魔是什么领突? 我笑而不...
    開封第一講書人閱讀 55,168評(píng)論 1 278
  • 正文 為了忘掉前任暖璧,我火速辦了婚禮,結(jié)果婚禮上攘须,老公的妹妹穿的比我還像新娘漆撞。我一直安慰自己殴泰,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評(píng)論 5 371
  • 文/花漫 我一把揭開白布浮驳。 她就那樣靜靜地躺著悍汛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪至会。 梳的紋絲不亂的頭發(fā)上离咐,一...
    開封第一講書人閱讀 48,954評(píng)論 1 283
  • 那天,我揣著相機(jī)與錄音奉件,去河邊找鬼宵蛀。 笑死,一個(gè)胖子當(dāng)著我的面吹牛县貌,可吹牛的內(nèi)容都是我干的术陶。 我是一名探鬼主播,決...
    沈念sama閱讀 38,271評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼煤痕,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼梧宫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起摆碉,我...
    開封第一講書人閱讀 36,916評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤塘匣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后巷帝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體忌卤,經(jīng)...
    沈念sama閱讀 43,382評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評(píng)論 2 323
  • 正文 我和宋清朗相戀三年楞泼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了驰徊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,989評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡现拒,死狀恐怖辣垒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情印蔬,我是刑警寧澤勋桶,帶...
    沈念sama閱讀 33,624評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站侥猬,受9級(jí)特大地震影響例驹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜退唠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評(píng)論 3 307
  • 文/蒙蒙 一鹃锈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瞧预,春花似錦屎债、人聲如沸仅政。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)圆丹。三九已至,卻和暖如春躯喇,著一層夾襖步出監(jiān)牢的瞬間辫封,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評(píng)論 1 260
  • 我被黑心中介騙來泰國(guó)打工廉丽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留倦微,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,401評(píng)論 2 352
  • 正文 我出身青樓正压,卻偏偏與公主長(zhǎng)得像欣福,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子焦履,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評(píng)論 2 345