內(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á)性分析算法介紹
能夠被GC Root引用的對(duì)象是否可達(dá)
由GCRoot不得不說一下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é)回收澄步。
二,常見的內(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)行期間都存在。
由于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 不一致奔垦。
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()筐咧。
- 通過重寫 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)存泄漏袖牙。
將該內(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)存消耗傀履。
MAT 的使用技巧
技巧一:通過OQL(Objective Query Language )查詢對(duì)象
由于內(nèi)存泄漏一般發(fā)生在Activity中,查詢Activity對(duì)象
技巧二:通過搜索關(guān)鍵字發(fā)現(xiàn)內(nèi)存泄漏對(duì)象
方式二:Android Profile 內(nèi)存泄漏分析
Android Profiler是Android Studio3.0用來替換之前Android Monitor的觀察工具绍昂,Android Profile 可以分析CPU啦粹,Memory,Network窘游,EnergyAndroid Profiler hprof內(nèi)存泄漏分析
Android Profile 進(jìn)程內(nèi)存泄漏分析
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ā)人員。
- App build.gradle 添加依賴喘批,在logcat查看LeakCanary關(guān)鍵字確定是否集成成功
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
} -
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
使用強(qiáng)引用對(duì)象: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
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)存叔汁。