編輯:關於Android編程
內存洩漏基本概念
內存檢測這部分,相關的知識有JVM虛擬機垃圾收集機制,類加載機制,內存模型等。編寫沒有內存洩漏的程序,對提高程序穩定性,提高用戶體驗具有重要的意義。因此,學習Java利用java編寫程序的時候,要特別注意內存洩漏相關的問題。雖然JVM提供了自動垃圾回收機制,但是還是有很多情況會導致內存洩漏。
內存洩漏主要原因就是一個生命周期長的對象,持有了一個生命周期短的對象的引用。這樣,會導致短的對象在該回收時候無法被回收。Android中比較典型的有:1、靜態變量持有Activity的context。2、或者Handler持有某個組件的context,同時如果Looper的消息隊列中有針對該Handler的消息沒有被處理,那麼會被作為target持有強引用,最終的導致context無法釋放,導致相應組件在退出時無法被內存回收。3、非靜態內部類默認持有外部類的引用,這樣如果我們在Activity中定義了一個Thread內部類,同時直接通過new Thread的方式去運行線程,那麼在線程運行結束之前,線程都會持有Activity的引用,從而導致Activity無法被釋放。
內存檢測工具
LeakCananry
LeakCanary,主要監測的是使用過程中Activity,Fragment等組件是否沒被內存回收。使用方法也十分簡單,相當於裝了一個監聽器,然後通過正常 操作去尋找內存洩漏,發生內存洩漏的時候會有Toast,同時可以在相應程序查看哪裡發生內存洩漏。
方法比較簡單,添加leakcanary依賴以後,新建一個Application入口,在Oncreate方法中安裝Leakcanary即可。
當發生內存洩漏時,屏幕會出現Toast,同時打開桌面上的Leaks程序,顯示洩漏的內存,如下圖:
LeakCananry實現步驟大致是:
實現大致步驟是:
1、自動把activity加入到KeyedWeakReference
2、在background線程中,檢查onDestroy後reference是否被清除,且沒有觸發gc
3、如果reference沒有被清除,則dump heap到一個hprof文件並保存到app文件系統中
4、在一個單獨進程中啟動HeapAnalyzerService,HeapAnalyzer使用HAHA來分析heap dump。
5、HeapAnalyzer在heap dump中根據reference key找到KeyedWeakReference。
6、HeapAnalyzer計算出到GC Roots的最短強引用路徑來判斷是否存在洩露,然後build出造成這個洩露的引用鏈。
7、結果被傳回來app進程的DisplayLeakService,並展示一個洩露的notification。
方法的有點是簡單易行,但是只能檢測Activity、Fragment是否發生內存洩漏。
觀看整體內存使用情況
詳情參見官方文檔: https://developer.android.com/studio/profile/investigate-ram.html#ViewingAllocations
使用adb shell,進入手機adb,執行命令:
dumpsys meminfo <包名> [-參數]
可以查看應用不同部分內存分配情況。比如Java heap,Native heap等
輸出是目前具體應用的內存分配,單位是kilobytes
因為程序涉及jni,經常會分配本地內存,所以會使用adb shell 的方式去查看native heap的分配情況。
結果如下:
分析各個參數:
Private Clean/Dirty RAM:
這部分內存是app的私有內存,當app銷毀是操作系統可以回收到的內存。其中private dirty只能被你的進程使用,同時只能存在在內存當中,當內存不夠,也不能通過分頁技術存儲到硬盤(操作系統相關知識),dalvik和native heap上的分配都是private dirty RAM。因為是dalvik heap和native heap共享的內存,所以命名dirty?
DDMS
使用流程
如何檢測內存洩漏?
Heap視圖中部有一個Type叫做dataobject,即數據對象,也就是我們的程序中實例化的對象。在data object一行中有一列是“Total Size”,其值就是當前進程中所有Java數據對象的內存總量,一般情況下,這個值的大小決定了是否會有內存洩漏。
正常情況下Total Size值都會穩定在一個有限的范圍內,也就是說沒有造成對象不被垃圾回收的情況,所以說雖然我們不斷的操作會不斷的生成很多對象,而在虛擬機不斷的進行GC的過程中,這些對象都被回收了,內存占用量會會落到一個穩定的水平。如果代碼中存在沒有釋放對象引用的情況,則dataobject的Total Size值在每次GC後不會有明顯的回落,隨著操作次數的增多Total Size的值會越來越大
通過DDMS方式,DataObject 的totalSize如果穩定在一個大概范圍內,則可以確定沒有發生內存洩漏。
MAT
然而,並不是所有的內存洩漏都十分明顯,並且會最終導致OOM。有時候只有幾個對象被洩漏,雖然影響不大,但是無疑浪費了內存。
要發現這種比較隱蔽的內存洩漏,我們需要使用MAT工具。
在了解支配樹之前,要先了解一些相關概念。
支配樹
支配樹體現了對象實例間的支配關系,在對象引用圖中,所有指向對象B的路徑都經過對象A,則認為對象A支配對象B。
在這張圖裡,左邊是對象引用關系,對於A和B,要抵達這兩個點必須經過GC root。而對於C可以從A也可以從B抵達,但都必須經過GC root,所以最近的支配點同樣也是GC root。
對於點D,不管是從C->D還是C->D->F->D,都必須經過的最近的點是C,所以C是D的支配點。同理可得EFHG在支配樹中的位置。
SHALLOWHEAP和RETAINED HEAP
Shallow heap表示對象本身所占內存大小,一個內存大小100bytes的對象Shallow heap就是100bytes。
Retained heap表示通過回收這一個對象總共能回收的內存,比方說一個100bytes的對象還直接或者間接地持有了另外3個100bytes的對象引用,回收這個對象的時候如果另外3個對象沒有其他引用也能被回收掉的時候,Retained heap就是400bytes。
在使用mat進行分析時,我們常常接觸到的數據就是shallow size和retained size: Shallow Size
對象自身占用的內存大小,不包括它引用的對象。
針對非數組類型的對象,它的大小就是對象與它所有的成員變量大小的總和。當然這裡面還會包括一些java語言特性的數據存儲單元。
針對數組類型的對象,它的大小是數組元素對象的大小總和。
Retained Size
Retained Size=當前對象大小+當前對象可直接或間接引用到的對象的大小總和。(間接引用的含義:A->B->C, C就是間接引用)
換句話說,Retained Size就是當前對象被GC後,從Heap上總共能釋放掉的內存。
不過,釋放的時候還要排除被GC Roots直接或間接引用的對象。他們暫時不會被回收。如下圖:
A對象的Retained Size=A對象的Shallow Size
B對象的Retained Size=B對象的Shallow Size + C對象的Shallow Size
因為B對象被釋放時,C同時被釋放,而D由於被GC roots直接引用所以不會被釋放。而Retained Size就是當前對象被GC後,從Heap上總共能釋放掉的內存。
以上概念,都是在使用MAT進行內存分析經常使用的,所以要記住。
MAT的下載與使用
下載地址:https://eclipse.org/mat/downloads.php
這裡沒有作為eclipse插件的方式下載mat,而是通過下載單獨的軟件客戶端。
首先,在DDMS中選擇要檢測的進程並dump HPROF file,如下圖:
HPROF中存儲的是當前內存的快照,因此,在dump快照之前先點擊cause GC手動觸發一次垃圾回收,這樣可以避免軟引用、弱引用等不必要的對象保留在內存中影響我們的分析。
轉儲出來的hprof文件,還有使用sdk自帶工具進行一下格式轉化,工具在sdk路徑下的platform-tools下,名稱為hprof-conv。
使用方法:
/.hprof-conv.exe a.hprof b.hprof
a 是輸入hprof文件名,b是輸出文件名。
然後將b.hprof在eclipse memory Analyzer中打開,注意要轉換格式,不然無法成功打開。
如下:
利用MAT分析內存洩漏
分析過程中,主要使用的是Histogram直方圖,和Dominater tree支配樹。
在Histogram視圖中查找retained heap值最大的項,並分析這裡是否發生內存洩漏。
注意,一般情況下我們忽略java、android系統自帶的對象,而著重分析我們自己程序中的對象。所以在上面輸入過濾Class Name。
Retained heap表示因為這個對象,會導致多少對象無法回收。
右擊相應類,list objects->with incoming references。表明引用這個類的某個實例的其它類,也就是它在引用樹中的父節點。通過分析該對象被誰引用,來判斷為何沒被垃圾回收。
outcoming reference就是子節點,查看一些當前對象引用著的對象。
此外看,Merge shortest path to gc root,可以找到一條到GC root的最短路徑,來看為什麼當前對象無法被回收。
實戰分析
下面記錄了本人對一個項目的具體分析過程,以及各個工具的使用方法。
1、使用DDMS查看內存
使用DDMS的過程中,針對應用分別進行了多次檢測,主要查看程序運行前的內存使用情況和程序運行後的內存使用情況:
使用前:
使用後:
通過上述數據可以看到,在程序運行前data object也就是在堆上分配的數據是180KB左右,而運行後內存大概在300KB上下浮動,沒有呈現一個明顯的一直上升的情況,故而沒有明顯的內存洩漏,基本沒有導致OOM的可能。
但是,可以發現,程序運行一次以後,放置一段時間,即便手動觸發GC,堆上的內存雖然回落,但是仍然是288KB,與執行前的180KB相差較大,說明有一些對象被GC roots引用,無法完成釋放。
下面采用MAT工具進行進一步分析。在上面的過程中,轉出了三個hprof文件,將hprof文件利用Android sdk tools下的工具進行格式轉換,進行對比分析:
2、使用MAT分析內存轉儲
前面分析內存使用發現,使用前和使用後有一個100KB左右的差值,同時即便放置一段時間仍然無法使用。將before和after的直方圖加入對比欄,在MAT中進行對比:
點擊右上角的紅色歎號:
對比發現兩個shallow heap大小基本相同,多出的部分是UpdatePartResultThread,系統類而不是我們自己編寫程序造成的。
再看一下使用前後直方圖中的retained heap:
可以看出,程序執行後,newActivity強引用了一些對象,在newAcitivity沒有推出前,retainedheap部分內存無法被回收。這也就是我們在DDMS中發現堆內存差異的主要原因。
右擊直方圖中的NewActivity,可以看見如下選項:
用的比較多的是List objects和Merger shortest Paths to GC Roots。
List objects:
Outgoing reference是支配樹中當前對象的子節點,也就是當前對象持有哪些引用。
Incoming reference是父節點,即當前對象被誰引用,為什麼沒被回收。
Merger shortest Paths to GC Roots:找到當前無法被釋放的對象到GC roots的最短路徑。即排查當前對象被誰引用,為什麼沒有被釋放。這裡因為我們的對象是一個Activity,當它顯示在前台的時候,不會被垃圾回收,所以不是我們分析的點。
在這裡,我們查看outgoing reference,查看當前對象擁有哪些強引用:
排除系統的對象,還是主要分析我們編寫的程序。
最後發現,我們在之前使用LeakCanary時,注冊的相應監聽器沒有回收,發現了內存洩漏 :)。
去掉LeakCanary,再次測試發現data object的值確實下降了不少。
繼續分析,發現newActivity引用了一個
致使一部分內存無法被釋放。這個問題屬於客戶端實現問題,不在內存洩漏的范圍內。
接下來,在直方圖中過濾出服務端的類:
可以看到,服務端的類大部分shallow heap都為0,也就是已經被垃圾回收。
結論
在使用MAT分析內存時,最關鍵的就是找引用關系。如果一個應該被釋放的對象沒有被釋放,那麼我們往往要查看它的incoming reference,看看是誰持有了它的強引用。同時利用Merger shortest GC roots找到到GC root的最短路徑,確定是由於被誰引用而導致無法GC。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持本站。
前面章節我們說了如何定義屬性、如何定義寬高,這樣之後組件的簡單外形或輪廓就已經出來,或者說已經定義出了畫布的大小,解下來就是如何在畫布上揮毫潑墨了。組件(除了容器組件)實
前言:總想寫點自己的東西,因為很多Android知識網上大部分都有教程,這樣寫的話總是忍不住借鑒別人寫的東西,再加入點自己的一些元素,我只好對網上的各種知識,我認為很多知
以下是TextView及其子類的層次結構:TextView基本用法TextView直接繼承了View,它還是EditText和Button兩個UI組件的子類。從功能上來看
我們在做Android開發的時候經常會遇到後台線程執行的比如說下載文件的時候,這個時候我們希望讓客戶能看到後台有操作進行,這時候我們就可以使用進度條,那麼既然在後台運行,