1. 程式人生 > >Android記憶體洩漏定位與解決

Android記憶體洩漏定位與解決

問題現象

反覆點選被測試的Android App的toolbar介面,然後返回再點選。在此重複過程中,發現到一定次數時,頁面開啟速度變慢,有時達到5s,十分影響使用者體驗。該問題涉及app所採用的webview框架的所有介面,影響面大。

初步分析

載入介面慢,一般有2種情況:一是每次都慢,那麼與該介面的佈局(layout)效率或業務邏輯(主執行緒動畫/同步的業務邏輯)關係更大;另外一種是重複開啟幾次,會遇到一次變慢,並且迴圈發生,根據多次記憶體問題的分析經驗,會與記憶體洩漏關係更大。至於為何有如此的判斷依據,下文會進行解釋。

該問題屬於後者,因此首先從記憶體的角度進行分析。使用Android官方提供的DDMS工具,選中App所在程序,監控該程序的堆記憶體狀況,如下表所示,隨著重複開啟介面次數的增加,堆記憶體一直呈上升趨勢,而且,測試過程中,即使點選DDMS的Gause GC(Garbage Collector 記憶體垃圾回收)來主動觸發記憶體垃圾回收,堆記憶體也沒有下降。


從該圖可以判斷,無論是系統自發的GC或QA主動觸發,對記憶體都不會下降,說明有相當一部分物件被一直引用著,導致GC時不會去釋放這部分物件的記憶體,而每開啟一次介面,又會在堆上為新的物件分配記憶體,最終結果就是記憶體洩漏。

對記憶體洩漏有了初步判斷後,下一步是使用MAT工具分析該場景所有物件的記憶體佔用和引用關係,從而定位出具體導致洩漏的類。首先同樣地重複開啟該介面,在開啟第5次、10次、20次的時候,分別dump出當時的hprof檔案(相當於所有物件的記憶體畫像),3個檔案在MAT的sumary分析中,都指出DynamicBgDrawable這個類的物件存在記憶體洩漏的風險


於是問題的分析有了下一步的目標。繼續使用MAT檢視DynamicBgDrawable物件的引用鏈:


從上圖看出,GraphicContent這個類的mContents成員(WeakHashMap型別)是mBackground物件(DynamicBgDrawable型別)的root引用,這說明正是因為GraphicContent中mContents對mBackground的間接引用一直未釋放,才導致DynamicBgDrawable物件記憶體的洩漏,到這裡就可以從GraphicContent.java的程式碼繼續尋找問題的原因了。

程式碼&業務分析


從GraphicContent類的兩處程式碼可以看出,GraphicContent一直持有著View,而WeakHashMap這個結構,如果作為key的View沒有被釋放,作為value的GraphicContent也不會被釋放,而這裡的View,在實際執行時,傳的就是

DynamicBgDrawable物件,所以形成DynamicBgDrawable與GraphicContent迴圈引用,互不釋放的局面,隨著重複地呼叫次數增多,無法釋放的物件越來越多,最終導致記憶體洩漏。

真相大白

Android App是執行在Dalvik虛擬機器之上的Java程式,Dalvik是Java虛擬機器(JVM)針對Android改造的版本,許多機制沿襲了JVM的設計,包括記憶體管理。對JVM的記憶體管理和回收機制進行了解,能更深入地理解該Bug的分析手段和定位過程。

以上是Java虛擬機器(JVM)的記憶體區域劃分,Java只能在堆中存放物件,而不能在棧上分配物件,所有執行時產生的物件全部都存放於堆中,包括陣列。是一個執行緒的執行區域, 它儲存著一個執行緒中的方法的呼叫狀態,也可以說,一個Java執行緒的執行狀態,都由一個Java棧來儲存。每個執行緒都會有自己的Java棧, 不會相互訪問其他Java棧中的資料。同時,基本資料型別也是在棧中儲存,包括boolean、byte、char、short、int、float、long、double。所以在分析這個Bug時,只關心堆記憶體,而不關心棧記憶體。因為物件記憶體的洩漏(溢位)只會發生在堆記憶體上。

下面解釋開頭的問題:為什麼概率性載入緩慢,更有可能與記憶體相關。首先需要了解Dalvik虛擬機器的記憶體垃圾回收原理:


Dalvik中會維護一個物件的引用關係圖,如上圖所示,方塊代表一個物件,mark後的數字代表這個物件被持有的引用個數。當Dalvik進行GC時,首先會做“標記”,將每個物件被引用的次數進行標記。上圖中,Root是引用關係圖的起點,藍色方塊代表該物件被持有了引用,那麼它的記憶體不會被回收。白色方塊代表該物件沒有或即將不被持有引用。”標記”過程結束後,GC就進入”清除”過程,會把所有mark為0的物件記憶體釋放掉,從而完成一次GC的操作。

 

上圖就是GC進行“清除”操作前後的示意圖。可以看出,在回收前,連續的可用記憶體較少,等同於碎片較多,在回收後,連續的可用記憶體變多了。我們回到bug本身,由於每次開啟介面,都會為新的物件分配記憶體,於是上圖中的存活物件方塊會會越來越多,連續的未使用區域會越來越少,這時當下一次開啟介面時,因為碎片過多,無法分配記憶體給物件,特別是大物件,就會過早的引起GC。而物件越多,一次GC的時間會越長,從而加大了系統的負載,增加了App介面的調起時間。下一步,當完成GC後,如果有足夠的記憶體可分配,則是較好的情況,如果像該Bug的情況,佔用大片記憶體的物件一直被引用著而不被GC釋放,在下一次開啟介面時,Android系統就需要為這個App分配更大的堆記憶體,以保證記憶體分配成功。當記憶體洩漏到一定程度,系統無法保證為App分配足夠記憶體時,則記憶體溢位(Out Of Memory, OOM)就會發生。


上圖解釋了介面概率性開啟緩慢與記憶體更相關的原因。圖中黑色的步驟都會增加介面的載入時間,上面的黑色步驟,是由於記憶體碎片引起,會隨著開啟次數增多,發生概率加大。下面的黑色步驟,是根據GC後App所生堆記憶體大小相關,每次開啟介面的情況都會不一樣,因此,才會出現概率性的開啟緩慢,從而為我們分析這種問題提供思路——就是開頭提到的,如果是概率性開啟緩慢,可以優先考慮和記憶體問題相關。

解決方案

將mView改為弱引用,每次垃圾回收(GC)時,都會回收View物件,從而使WeakHashMap中的value(GraphicContent)物件也能自動得到釋放。


總結

該Bug的分析和解決過程,具有典型的代表性,在實際專案中,有多個Android記憶體洩漏的Bug,都是採用上述的定位方法和分析工具進行解決的,是一套通用且有效的Bug定位方案。而相互引用的問題,等價於死鎖情況,也是程式中典型的問題場景。弱引用的使用有效地解決業務和記憶體洩漏的問題,在Android app的記憶體洩漏和溢位的解決中,經常會被採用,也可以作為Code Review的一個關注點進行推廣。

更多幹貨分享請關注”百度MTC學院“http://mtc.baidu.com/academy/article