Android 效能優化之被忽視的記憶體洩漏

PleaseCallMeCoder發表於2016-07-26

起因

寫部落格就像講故事,得有起因,經過,結果,人物,地點和時間。今天就容我給大家講一個故事。人物呢,肯定是我了。故事則發生在最近的這兩天,地點在coder君上班的公司。那天無意中我發現了一個奇怪的現象,隨著我點開我們App的頁面,Memory Monitor中顯示佔用的記憶體越來越多(前面的頁面已經finish掉了)。咦?什麼鬼?

經過

有了問題就解決嘛,俗話說的好,有bug要上,沒有bug寫個bug也要上。那到底是是什麼問題會引起這個現象呢?

Android中記憶體相關的問題無非就是這麼幾點:

  • Memory Leaks 記憶體洩漏
  • Memory Churn 記憶體抖動
  • OutOfMemory 記憶體溢位

阿西吧,仔細想想怎麼這麼像記憶體洩漏呢。那到底是不是呢?那我們就一點一點分析一下唄。

記憶體相關資料

關於記憶體我們可能想了解的資料大概有三點:

總記憶體

  • 系統當前可用記憶體

  • 我們可以使用的記憶體

每一個Android裝置都會有不同的RAM總大小與可用空間,因此不同裝置為app提供了不同大小的heap限制。你可以通過呼叫getMemoryClass())來獲取你的app的可用heap大小。如果你的app嘗試申請更多的記憶體,會出現OutOfMemory的錯誤。

在一些特殊的情景下,你可以通過在manifest的application標籤下新增largeHeap=true的屬性來宣告一個更大的heap空間。如果你這樣做,你可以通過getLargeMemoryClass())來獲取到一個更大的heap size。

然而,能夠獲取更大heap的設計本意是為了一小部分會消耗大量RAM的應用(例如一個大圖片的編輯應用)。不要輕易的因為你需要使用大量的記憶體而去請求一個大的heap size。只有當你清楚的知道哪裡會使用大量的記憶體並且為什麼這些記憶體必須被保留時才去使用large heap. 因此請儘量少使用large heap。使用額外的記憶體會影響系統整體的使用者體驗,並且會使得GC的每次執行時間更長。在任務切換時,系統的效能會變得大打折扣。

另外, large heap並不一定能夠獲取到更大的heap。在某些有嚴格限制的機器上,large heap的大小和通常的heap size是一樣的。因此即使你申請了large heap,你還是應該通過執行getMemoryClass()來檢查實際獲取到的heap大小。

Java中的四種引用

開始分析之前,有必要先了解下Java的記憶體分配與回收。

Java的資料型別分為兩類:基本資料型別、引用資料型別。

基本資料型別的值儲存在棧記憶體中,而引用資料型別需要開闢兩塊儲存空間,一塊在堆記憶體中,用於儲存該型別的物件;另一塊在棧記憶體中,用於儲存堆記憶體中該物件的引用。

其中引用型別變數分為四類:

  • 強引用最常用的引用形式。把一個物件賦給一個引用型別變數,則為強引用。只要一個引用是強引用,則垃圾回收器永遠都無法回收這個物件的記憶體空間,除非JVM終止。
  • 軟引用當記憶體資源充足的時候,垃圾回收器不會回收軟引用對應的物件的記憶體空間;但當記憶體資源緊張時,軟引用所對應的物件就會被垃圾回收器回收。
    • 弱引用不管JVM記憶體資源是否緊張,只要垃圾回收器執行,弱引用所對應的物件就會被釋放。
    • 虛引用虛引用等於沒有引用,無法通過虛引用訪問其對應的物件。軟引用和弱引用在其物件被回收之後,這些引用會被新增到引用佇列中去;而虛引用在其物件被回收之前,虛引用就被新增到引用佇列中去了。因此虛引用可以在其物件被釋放之前進行一些操作。虛引用和引用佇列繫結的方法:

    Garbage Collection Android中的垃圾回收

    Android系統會在適當的時機觸發GC操作,一旦進行GC操作,就會將一些不再使用的物件進行回收

    執行GC操作的時候,所有執行緒的任何操作都會需要暫停,等待GC操作完成之後,其他操作才能夠繼續執行。

    通常來說,單個的GC並不會佔用太多時間,但是大量不停的GC操作則會顯著佔用幀間隔時間(16ms)。如果在幀間隔時間裡面做了過多的GC操作,那麼自然其他類似計算,渲染等操作的可用時間就變得少了

    Memory Leaks記憶體洩漏

    記憶體洩漏表示的是不再用到的物件因為被錯誤引用而無法進行回收。發生記憶體洩漏會導致Memory Generation中的剩餘可用Heap Size越來越小,這樣會導致頻繁觸發GC,更進一步引起效能問題。

    總結起來其實很簡單:存在無效的引用!

    記憶體洩露可以引發很多的問題,常見的記憶體洩露導致問題如下:

    • 應用卡頓,響應速度慢(記憶體佔用高時JVM虛擬機器會頻繁觸發GC);
    • 應用被從後臺程式幹為空程式;
    • 應用莫名的崩潰(也就是超過了HeepSize閾值引起OOM);

    記憶體洩漏分析工具

    看到這些問題,突然發現好像離真相越來越近了0.0。

    想要更加清楚地實時知曉當前應用程式的記憶體使用情況,我們需要通過一些工具來實現。比較好用的工具有兩種:

    • Memory Analyzer Tool
    • LeakCanary

    下面我們分開介紹。

    Memory Analyzer Tool

    Memory Analysis Tools(點我下載)是一個專門分析Java堆資料記憶體引用的工具,我們可以使用它方便的定位記憶體洩露原因,核心任務就是找到GC ROOT位置。接下來說下使用步驟。

    抓取記憶體資訊

    AndriodStudio中抓取記憶體資訊還是很方便的,有兩種方法:

    • 使用Android Device Monitor點選Android Studio工具欄上的Tool–>Android Device Monitor

7ndnk8N

在Android Device Monitor介面中選在你要分析的應用程式的包名,點選Update Heap來更新統計資訊,然後點選Cause GC即可檢視當前堆的使用情況,點選Dump HPROF file,將該應用當前的記憶體資訊儲存成hprof檔案,放在桌面即可,操作如下圖

CuAzhDK

  • 直接獲取

在Android Device Monitor介面中選在你要分析的應用程式的包名,點選Update Heap來更新統計資訊,然後點選Cause GC即可檢視當前堆的使用情況,點選Dump HPROF file,將該應用當前的記憶體資訊儲存成hprof檔案,放在桌面即可,操作如下圖

mM8Dhr5

稍等片刻,生成的檔案會出現在captures中,然後選擇檔案,點選右鍵轉換成標準的hprof檔案,就可以在MAT中開啟了。

gFGFREY

使用MAT工具檢視分析

這裡我寫了個簡單的demo來測試,這個demo一共有兩個頁面,在跳轉到第二個頁面之後,新開一個現成去列印activity資訊。

多次進入SecondActivity之後會發現記憶體一直在增長,並沒有降低。

而且log裡會不停的輸出log,列印當前activity的name。

ETupC9m

3t9RMon

在MAT中開啟抓取到的檔案後如圖

Gv0jNbl

MAT中提供了非常多的功能,這裡我們只要學習幾個最常用的就可以了。上圖最中央的那個餅狀圖展示了最大的幾個物件所佔記憶體的比例,這張圖中提供的內容並不多,我們可以忽略它。紅色框中有兩個非常有用的工具是我們常用的。

Histogram可以列出記憶體中每個物件的名字、數量以及大小。

Dominator Tree會將所有記憶體中的物件按大小進行排序,並且我們可以分析物件之間的引用結構。

我們先來看Histogram

lBC3TnL

我們應該如何去分析記憶體洩漏呢?即分析大記憶體的物件。但是假如我們有目標物件的話,左上角值支援正規表示式的,我們輸入SecondActivity。這裡我們看到,我們有5個SecondActivity的例項,因為我們引用SecondActivity的現成沒有銷燬,導致會有很多例項。

接下來對著SecondActivity右鍵 -> List objects -> with incoming references檢視具體SecondActivity例項,如下圖所示:

eeWrRF0

如果想要檢視記憶體洩漏的具體原因,可以對著任意一個MainActivity的例項右鍵 -> Path to GC Roots -> exclude weak references,結果如下圖所示:

PmxCiLs

可以看到紅色框中,因為我們的執行緒持有SecondActivity的例項,所有導致記憶體洩漏。

此外,我們可以選擇以我們專案的包結構的形式來檢視

rKwJGtr

接下來我們看下Dominator Tree。

e4mc0Gc

關於Dominator Tree我們需要注意三點:

  • 首先Retained Heap表示這個物件以及它所持有的其它引用(包括直接和間接)所佔的總記憶體,因此從上圖中看,前兩行的Retained Heap是最大的,我們分析記憶體洩漏時,記憶體最大的物件也是最應該去懷疑的。
  • 帶有黃點的物件就表示是可以被GC Roots訪問到的,根據上面的講解,可以被GC Root訪問到的物件都是無法被回收的。
  • 並不是所有帶黃點的物件都是洩漏的物件,有些物件系統需要一直使用,本來就不應該被回收。我們可以注意到,有些帶黃點的物件最右邊會寫一個System Class,說明這是一個由系統管理的物件,並不是由我們自己建立並導致記憶體洩漏的物件。

現在我們可以對著我們想檢視的內容點選右鍵 -> Path to GC Roots -> exclude weak references,為什麼選擇exclude weak references呢?因為弱引用是不會阻止物件被垃圾回收器回收的,所以我們這裡直接把它排除掉,然後一步一步分析。

LeakCanary

leakcanary是一個開源專案,一個記憶體洩露自動檢測工具,是著名的GitHub開源組織Square貢獻的,它的主要優勢就在於自動化過早的發覺記憶體洩露、配置簡單、抓取貼心,缺點在於還存在一些bug,不過正常使用百分之九十情況是OK的,其核心原理與MAT工具類似。

因為配置十分簡單,這裡就不多說了,官方文件。

我們看下分析結果

BE8T7JP

簡單直白!

常見記憶體洩漏情況

  • 構造Adapter時,沒有使用快取的 convertView
  • Bitmap物件不在使用時呼叫recycle()釋放記憶體
  • Context使用不當造成記憶體洩露:不要對一個Activity Context保持長生命週期的引用。儘量在一切可以使用應用ApplicationContext代替Context的地方進行替換。
  • 非靜態內部類的靜態例項容易造成記憶體洩漏:即一個類中如果你不能夠控制它其中內部類的生命週期(譬如Activity中的一些特殊Handler等),則儘量使用靜態類和弱引用來處理(譬如ViewRoot的實現)。
  • 警惕執行緒未終止造成的記憶體洩露;譬如在Activity中關聯了一個生命週期超過Activity的Thread,在退出Activity時切記結束執行緒。一個典型的例子就是HandlerThread的run方法是一個死迴圈,它不會自己結束,執行緒的生命週期超過了Activity生命週期,我們必須手動在Activity的銷燬方法中中調運thread.getLooper().quit();才不會洩露。
  • 物件的註冊與反註冊沒有成對出現造成的記憶體洩露;譬如註冊廣播接收器、註冊觀察者(典型的譬如資料庫的監聽)等。
  • 建立與關閉沒有成對出現造成的洩露;譬如Cursor資源必須手動關閉,WebView必須手動銷燬,流等物件必須手動關閉等。
  • 不要在執行頻率很高的方法或者迴圈中建立物件(比如onmeasure),可以使用HashTable等建立一組物件容器從容器中取那些物件,而不用每次new與釋放。
  • 避免程式碼設計模式的錯誤造成記憶體洩露;譬如迴圈引用,A持有B,B持有C,C持有A,這樣的設計誰都得不到釋放。

結果

真相只有一個,那就是確實是由於記憶體洩漏才出現我遇到的情況。程式設計師嘛,誰還不踩個坑,跳出來,拍拍身上的灰塵,總結一下,過兩天又是一條幫幫的coder。原始碼

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

Android 效能優化之被忽視的記憶體洩漏 Android 效能優化之被忽視的記憶體洩漏

相關文章