1. 程式人生 > >記一次記憶體洩露優化過程

記一次記憶體洩露優化過程

背景

專案目前存在使用久了或者重複開啟關閉某個頁面,記憶體會一直飆升,居高不下,頻繁發生GC。靜置一段時間後,情況有所改善,但是問題依舊明顯,如圖1-1、1-2。


圖1-1.操作時的記憶體使用情況


圖1-2.靜置時的記憶體使用情況

如上圖1-1,是通過Android Studio檢視記憶體(灰色)和CPU(紅色)使用情況,可以看出記憶體有發生抖動並且是處於比較高的狀態,再者,從logcat可以看到一直髮生GC,如下圖1-3:

這裡寫圖片描述
圖1-3.

出現這些情況,是有很多因素造成的,最主要的原因是發生了記憶體洩露:頁面關閉之後,該頁面物件理應被回收,但由於某種原因導致它沒有被及時回收掉,一直佔據著記憶體,導致記憶體居高不下。

目標

本目標就是排查出應用內發生記憶體洩露的地方,並解決問題,避免組員踩重複的坑,減少應用記憶體洩露現象,減少記憶體抖動,減少OOM發生的機率,提高應用的效能、流暢度,減少卡頓。

記憶體洩露排查優化過程

記憶體洩露排查過程,有兩種方法,一種是通過MAT手動分析排查,另一種是通過LeakCanary注入到應用內輔助排查。

使用MAT分析記憶體洩露

首先,需要下載、安裝記憶體分析工具MAT(http://www.eclipse.org/mat/)。接著,需要dump取記憶體,並進行分析。開啟應用,先進行幾個簡單的操作:進入首頁->進入社群首頁->進入一條問答詳情介面->迅速關閉頁面->再次進入問答詳情介面->再關閉,以此重複幾次。然後通過Android Studio的Android Monitor將此時的記憶體dump下來,dump下來的hprof檔案,存放在專案目錄下的captures資料夾下,如圖1-4、1-5。


圖1-4.dump記憶體


圖1-5. hprof檔案

dump下來的hprof檔案還不能用MAT開啟,還需要進一步處理。需要藉助Android SDK的一個轉換工具,如圖1-6。


圖1-6.hprof-conv轉換工具

通過命令列執行命令:hprof-conv.exe ,是指原始檔,是指目標檔案,如下圖1-7。

圖1-7. hprof-conv命令

然後使用MAT開啟轉換後的1.hprof檔案,如下圖1-8。


圖1-8.記憶體概覽

上圖顯示的是,當前記憶體使用情況。開啟紅色箭頭按鈕,這是OQL,類似於SQL,可以通過查詢語句,查詢物件。
為了排查出哪些介面(Activity)發生了記憶體洩露,可以使用下面的OQL語句查詢,如圖1-9。


圖1-9.

查詢結果顯示有HomeActivity、QaDetailActivity,是符合之前的操作(進入首頁->進入問答詳情,詳情重複進出)。這裡可以看出QaDetailActivity肯定是發生了記憶體洩露,因為查詢結果出現了兩個QaDetailActivity,說明記憶體中存在了兩個QaDetailActivity物件,但是當QaDetailActivity被關閉時,是應該被回收的。接著,執行圖1-10操作,進一步排查是什麼導致了記憶體洩露。


圖1-10.執行Path To GC Roots


圖1-11.GC Roots 執行結果

由於JAVA的垃圾回收機制是,當一個物件被持有引用時,如果發生GC時,是不會被回收的。如圖1-11所示,當除去弱引用和軟引用,執行GC後,this$0(QaDetailActivity)物件還是被mErrorListener所引用,mErrorListener是專案中Volley網路請求庫中的一個錯誤回撥介面。可以看下,專案中程式碼是如何實現的,如圖1-12。


圖1-12.JsonGet的使用

圖1-12是獲取問答詳情資訊發起的JsonGet請求,而Response.Listener和Response.ErrorListener都是匿名物件,一個非同步回撥,回撥之後處理相關邏輯。試想一下,當JsonGet發起請求後,或因網路阻塞,不能及時處理回撥,這時候關閉了QaDetailActivity,但是由於Response回撥未執行完,還持有QaDetailActivity物件,所以此時QaDetailActivity並不能被回收。所以,目前專案中所有使用該方式請求資料理論上都存在著記憶體洩露的風險。那麼如何解決此問題引起的記憶體洩露呢?這個和Android常見的會引起記憶體洩露的Handler一樣,都是沒有適時的移除回撥導致的。優化後的程式碼如圖1-13。


圖1-13.

就是將JsonGet等網路請求通過觀察者模式進行封裝, 在Activity onCreate addObserver,在onDestory時removeObserver,這樣就可以避免上述所遇到的記憶體洩露問題了。

上面是一個完整的記憶體洩露排查過程,但是,你會發現,這是在一個黑箱檢測,你不知道什麼時候該去dump下記憶體進行分析,有可能你操作了很多介面之後dump下的記憶體並沒有記憶體洩露問題。如果進行黑箱測試的話,這是一個耗時、耗力的過程,那麼接下來介紹LeakCanary的使用,這是一個記憶體洩露檢測神器。

使用LeakCanary檢測記憶體洩露

LeakCanary是square開源的一個專案https://github.com/square/leakcanary,它是一個Android和Java的記憶體洩露檢測庫,可以大幅度減少了開發中遇到的OOM問題。在專案中,專門開了一個程式碼分支dev_LeakCanary,用來檢測記憶體洩露。關於LeakCanary的工作原理,可以從github開源專案上的wiki中獲知https://github.com/square/leakcanary/wiki/FAQ

  1. RefWatcher.watch()建立一個KeyedWeakReference去檢測物件;
  2. 接著,在後臺執行緒,它將會檢查是否有引用在不是GC觸發的情況下需要被清除的;
  3. 如果引用引用仍然沒有被清除,將會轉儲堆到.hprof檔案到系統檔案中(it them dumps the heap into a .hprof file stored on the app file system.);
  4. HeapAnalyzerService是在一個分離的程序中開始的,HeapAnalyzer通過使用HAHA(https://github.com/square/haha )解析heap dump;
  5. 由於一個特殊的引用key和定位的洩露引用,HeapAnalyzer可以在heap dump中找到KeyedWeakReference;
  6. 如果有一個洩露,HeapAnalyzer計算到GC Roots的最短的強引用路徑,然後建立造成洩露的引用鏈;
  7. 結果在app的程序中傳回到DisplayLeakService,並展示洩露的通知訊息;

那麼如何使用它呢?
首先,新增相關依賴,如圖1-14。


圖1-14.LeakCanary依賴

然後在Application初始化,並獲取一個RefWatcher物件,如圖1-15。


圖1-15.初始化LeakCanary

最後註冊要監聽的物件,比如,要監聽Activity有沒有記憶體洩露,那麼,在BaseActivity$onDestory註冊監聽,如圖1-16。


圖1-16.監聽Activity

這樣所有繼承BaseActivity都會被檢測。同樣進行開始的那樣操作介面(進入首頁->問答詳情->重複進出)。這時候,會發現通知欄有訊息,點開訊息,如下圖1-17。


圖1-17.LeakCanary記憶體洩露分析介面

LeakCanary的分析結果顯示,還是因為JsonGet的網路請求,非同步回撥引起的記憶體洩露,當然,如果要通過MAT檢視更多資訊,自己分析,可以在手機SD卡下Download/leakcanary資料夾下找到hprof檔案,匯出來之後根據之前的操作一樣,先通過轉換工具進行轉換,再用MAT開啟檢視。

顯而易見,通過LeakCanary進行記憶體洩露檢測簡單了不少,省時省力,但是如何解決記憶體洩露問題,還是要靠程式設計師自身的技能了,它只是告訴你哪裡發生了記憶體洩露。

優化後對比

通過上面的記憶體洩露排查優化之後,使用相同包名(dev),相同環境(local環境下),相同租戶和使用者,儘可能的進行相同操作。


圖1-18.優化後,操作時記憶體使用情況

與優化前圖1-1相比,優化前記憶體的使用大小一直處於12M以上,優化後圖1-18,記憶體使用在峰值在10M左右。靜置一段時間後(系統發生GC,回收記憶體),如圖1-19。


圖1-19.優化後,靜置時記憶體使用情況

靜置後與圖1-2相比,可以看出,兩者記憶體差不多都穩定在9M左右,但優化後記憶體抖動情況明顯減少。
說明:可能鑑於測試手段不足,不夠嚴謹,測試結果存在偏差在所難免,但是,還是可以看出優化後的效果還是不錯的。

總結

記憶體洩露,會導致應用在使用過程中,記憶體不斷攀升,導致記憶體不足,應用不流暢,卡頓,甚至導致發生OOM,程式崩潰。所以,在開發過程中,應該避免書寫一些會致使記憶體洩露的程式碼。這裡總結一些常見的記憶體洩露情景,有則改之無則加勉。

1、 Handler。在使用Handler時應該通過傳入一個runnable,來處理訊息,然後在適當的時機移除該runnable,比如Activity onDestory時。
2、 網路請求非同步回撥。同Handler一樣,應該在適當的時機移除非同步回撥操作,比如Activity onDestory時。
3、 匿名內部類。由於匿名內部類會隱式持有當前類的引用,所以應儘可能宣告為static
4、 儘可能少使用靜態成員變數。之前專案的一個bug就是這個情況引起的,當時的程式碼是這樣的。

5、 在傳遞Context時,需要特別注意,如果可以的話,儘量使用Application Context。