如何檢查 Android 應用的記憶體使用情況

will發表於2015-05-01

Android是為移動裝置而設計的,所以應該關注應用的記憶體使用情況。儘管Android的Dalvik虛擬機器會定期執行垃圾回收操作,但這也不意味著就可以忽視應用在何時何處進行記憶體分配和釋放。為了提供良好的使用者體驗,做到系統在不同應用間流暢切換,當使用者和應用無互動時,避免應用不必要的記憶體消耗是很重要的。

儘管在開發過程中很好的遵守了《管理應用記憶體》Managing Your App Memory )中的原則(也是應該遵守的),仍然可能會有物件洩露或引入其他的記憶體bug。唯一來確定應用使用了儘可能少的記憶體的方法,就是使用工具來分析應用的記憶體使用情況。本指南介紹瞭如何去調查記憶體使用情況。

解析日誌資訊

最簡單的調查應用記憶體使用情況的地方就是Dalvik日誌資訊。可以在logcat(輸出資訊可以在Device Monitor或者IDE中檢視到,例如Eclipse和Android Studio)中找到這些日誌資訊。每次有垃圾回收發生,logcat會列印出帶有下面資訊的日誌訊息:

GC原因

觸發垃圾回收執行的原因和垃圾回收的型別。原因主要包括:

GC_CONCURRENT

併發垃圾回收,當堆開始填滿時觸發來釋放記憶體。

GC_FOR_MALLOC

堆已經滿了時應用再去嘗試分配記憶體觸發的垃圾回收,這時系統必須暫停應用執行來回收記憶體。

GC_HPROF_DUMP_HEAP

建立HPROF檔案來分析應用時觸發的垃圾回收。

GC_EXPLICIT

顯式垃圾回收,例如當呼叫 gc()(應該避免手動呼叫而是要讓垃圾回收器在需要時主動呼叫)時會觸發。

GC_EXTERNAL_ALLOC

這種只會在API 10和更低的版本(新版本記憶體都只在Dalvik堆中分配)中會有。回收外部分配的記憶體(例如儲存在本地記憶體或NIO位元組緩衝區的畫素資料)。

釋放數量

執行垃圾回收後記憶體釋放的數量。

堆狀態

空閒的百分比和(活動物件的數量)/(總的堆大小)。

外部記憶體狀態

API 10和更低版本中的外部分配的記憶體(分配的記憶體大小)/(回收發生時的限制值)。

暫停時間

越大的堆的暫停時間就越長。併發回收暫停時間分為兩部分:一部分在回收開始時,另一部分在回收將近結束時。

例如:

隨著這些日誌訊息的增多,注意堆狀態(上面例子中的3571K/9991K)的變化。如果值一直增大並且不會減小下來,那麼就可能有記憶體洩露了。

檢視堆的更新

為了得到應用記憶體的使用型別和時間,可以在Device Monitor中實時檢視應用堆的更新:

1.開啟Device Monitor。

從<sdk>/tools/路徑下載入monitor工具。

2.在Debug Monitor視窗,從左邊的程式列表中選擇要檢視的應用程式。

3.點選程式列表上面的Update Heap

4.在右側皮膚中選擇Heap標籤頁。

 

Heap檢視顯示了堆記憶體使用的基本狀況,每次垃圾回收後會更新。要看更新後的狀態,點選Gause GC按鈕。

圖1.Device Monitor工具顯示[1] Update Heap和 [2] Cause GC按鈕。右邊的Heap標籤頁顯示堆的情況。

跟蹤記憶體分配

當要減少記憶體問題時,應該使用Allocation Tracker來更好的瞭解記憶體消耗大戶在哪分配。Allocation Tracker不僅在檢視記憶體的具體使用上很有用,也可以分析應用中的關鍵程式碼路徑,例如滑動。

例如,在應用中滑動列表時跟蹤記憶體分配,可以看到記憶體分配的動作,包括在哪些執行緒上分配和哪裡進行的分配。這對優化程式碼路徑來減輕工作量和改善UI流暢性都極其有用。

使用Allocation Tracker:

1.開啟Device Monitor 。

從<sdk>/tools/路徑下載入monitor工具。

2.在DDMS視窗,從左側皮膚選擇應用程式。
3.在右側皮膚中選擇Allocation Tracker標籤頁。
4.點選Start Tracking
5.執行應用到需要分析的程式碼路徑處。
6.點選Get Allocations來更新分配列表。

列表顯示了所有的當前分配和512大小限制的環形緩衝區的情況。點選行可以檢視分配的堆疊跟蹤資訊。堆疊不只顯示了分配的物件型別,還顯示了屬於哪個執行緒哪個類哪個檔案和哪一行。


圖2. Device Monitor工具顯示了在Allocation Tracker中當前應用的記憶體分配和堆疊跟蹤的情況。

注意:總會有一些分配是來自與 DdmVmInternal 和 allocation tracker本身。

儘管移除掉所有嚴重影響效能的程式碼是不必要的(也是不可能的),但是allocation tracker還是可以幫助定位程式碼中的嚴重問題。例如,應用可能在每個draw操作上建立新的Paint物件。把物件改成全域性變數就是一個很簡單的改善效能的修改。

檢視總體記憶體分配

為了進一步的分析,檢視應用記憶體中不同記憶體型別的分配情況,可以使用下面的 adb  命令:

應用當前的記憶體分配輸出列表,單位是千位元組。

當檢視這些資訊時,應當熟悉下面的分配型別:

私有(Clean and Dirty) 記憶體

程式獨佔的記憶體。也就是應用程式銷燬時系統可以直接回收的記憶體容量。通常來說,“private dirty”記憶體是其最重要的部分,因為只被自己的程式使用。它只在記憶體中儲存,因此不能做分頁儲存到外存(Android不支援swap)。所有分配的Dalvik堆和本地堆都是“private dirty”記憶體;Dalvik堆和本地堆中和Zygote程式共享的部分是共享dirty記憶體。

 實際使用記憶體 (PSS)

這是另一種應用記憶體使用的計算方式,把跨程式的共享頁也計算在內。任何獨佔的記憶體頁直接計算它的PSS值,而和其它程式共享的頁則按照共享的比例計算PSS值。例如,在兩個程式間共享的頁,計算進每個程式PPS的值是它的一半大小。

PSS計算方式的一個好處是:把所有程式的PSS值加起來就可以確定所有程式總共佔用的記憶體。這意味著用PSS來計算程式的實際記憶體使用、程式間對比記憶體使用和總共剩餘記憶體大小是很好的方式。

例如,下面是平板裝置中Gmail程式的輸出資訊。它顯示了很多資訊,但是具體要講解的是下面列出的一些關鍵資訊。

注意:實際看到的資訊可能和這裡的稍有不同,輸出的詳細資訊可能會根據平臺版本的不同而不同。

通常來說,只需關心Pss Total列和Private Dirty列就可以了。在一些情況下,Private Clean列和Heap Alloc列也會提供很有用的資訊。下面是一些應該檢視的記憶體分配型別(行中列出的型別):

Dalvik Heap

應用中Dalvik分配使用的記憶體。Pss Total包含所有的Zygote分配(如上面PSS定義所描述的,共享跨程式的加權)。Private Dirty是應用堆獨佔的記憶體大小,包含了獨自分配的部分和應用程式從Zygote複製分裂時被修改的Zygote分配的記憶體頁。

 注意:新平臺版本有Dalvik Other這一項。Dalvik Heap中的Pss Total和Private Dirty不包括Dalvik的開銷,例如即時編譯(JIT)和垃圾回收(GC),然而老版本都包含在Dalvik的開銷裡面。

 Heap Alloc是應用中Dalvik堆和本地堆已經分配使用的大小。它的值比Pss Total和Private Dirty大,因為程式是從Zygote中複製分裂出來的,包含了程式共享的分配部分。

 .so mmap和.dex mmap

mmap對映的.so(本地) 和.dex(Dalvik)程式碼使用的記憶體。Pss Total 包含了跨應用共享的平臺程式碼;Private Clean是應用獨享的程式碼。通常來說,實際對映的記憶體大小要大一點——這裡顯示的記憶體大小是執行了當前操作後應用使用的記憶體大小。然而,.so mmap 的private dirty比較大,這是由於在載入到最終地址時已經為原生程式碼分配好了記憶體空間。

 Unknown

無法歸類到其它項的記憶體頁。目前,這主要包含大部分的本地分配,就是那些在工具收集資料時由於地址空間佈局隨機化(Address Space Layout Randomization ,ASLR)不能被計算在內的部分。和Dalvik堆一樣, Unknown中的Pss Total把和Zygote共享的部分計算在內,Unknown中的Private Dirty只計算應用獨自使用的記憶體。

TOTAL

程式總使用的實際使用記憶體(PSS),是上面所有PSS項的總和。它表明了程式總的記憶體使用量,可以直接用來和其它程式或總的可以記憶體進行比較。

Private Dirty和Private Clean是程式獨自佔用的總記憶體,不會和其它程式共享。當程式銷燬時,它們(特別是Private Dirty)佔用的記憶體會重新釋放回系統。Dirty記憶體是已經被修改的記憶體頁,因此必須常駐記憶體(因為沒有swap);Clean記憶體是已經對映持久檔案使用的記憶體頁(例如正在被執行的程式碼),因此一段時間不使用的話就可以置換出去。

ViewRootImpl

程式中活動的根檢視的數量。每個根檢視與一個視窗關聯,因此可以幫助確定涉及對話方塊和視窗的記憶體洩露。

AppContexts和Activities

當前駐留在程式中的ContextActivity物件的數量。可以很快的確認常見的由於靜態引用而不能被垃圾回收的洩露的 Activity物件。這些物件通常有很多其它相關聯的分配,因此這是追查大的記憶體洩露的很好辦法。

注意:View 和 Drawable 物件也持有所在Activity的引用,因此,持有View 或 Drawable 物件也可能會導致應用Activity洩露。

獲取堆轉儲

堆轉儲是應用堆中所有物件的快照,以二進位制檔案HPROF的形式儲存。應用堆轉儲提供了應用堆的整體狀態,因此在檢視堆更新的同時,可以跟蹤可能已經確認的問題。

檢索堆轉儲:

1.開啟Device Monitor。

從<sdk>/tools/路徑下載入monitor工具。

2.在DDMS視窗,從左側皮膚選擇應用程式。

3.點選Dump HPROF file,顯示見圖3。

4.在彈出的視窗中,命名HPROF檔案,選擇存放位置,然後點選Save

圖3.Device Monitor工具顯示了[1] Dump HPROF file按鈕。

如果需要能更精確定位問題的堆轉儲,可以在應用程式碼中呼叫dumpHprofData()來生成堆轉儲。

堆轉儲的格式基本相同,但與Java HPROF檔案不完全相同。Android堆轉儲的主要不同是由於很多的記憶體分配是在Zygote程式中。但是由於Zygote的記憶體分配是所有應用程式共享的,這些對分析應用堆沒什麼關係。

為了分析堆轉儲,你需要像jhat或Eclipse記憶體分析工具(MAT)一樣的標準工具。當然,第一步需要做的是把HPROF檔案從Android的檔案格式轉換成J2SE HRPOF的檔案格式。可以使用<sdk>/platform-tools/路徑下的hprof-conv工具來轉換。hprof-conv的使用很簡單,只要帶上兩個引數就可以:原始的HPROF檔案和轉換後的HPROF檔案的存放位置。例如:

注意:如果使用的是整合在Eclipse中的DDMS,那麼就不需要再執行HPROF轉換操作——預設已經轉換過了。

現在就可以在MAT中載入轉換過的HPROF檔案了,或者是在可以解析J2SE HPROF格式的其它堆分析工具中載入。

分析應用堆時,應該查詢由下導致的記憶體洩露:

  • 對Activity、Context、View、Drawable的長期引用,以及其它可能持有Activity或Context容器引用的物件
  • 非靜態內部類(例如持有Activity例項的Runnable)
  • 不必要的長期持有物件的快取

使用Eclipse記憶體分析工具

Eclipse記憶體分析工具(MAT)是一個可以分析堆轉儲的工具。它是一個功能相當強大的工具,功能遠遠超過這篇文件的介紹,這裡只是一些入門的介紹。

 

在MAT中開啟型別轉換過的HPROF檔案,在總覽介面會看到一張餅狀圖,它展示了佔用堆的最大物件。在圖表下面是幾個功能的連結:

  •  Histogram view顯示所有類的列表和每個類有多少例項。

正常來說類的例項的數量應該是確定的,可以用這個檢視找到額外的類的例項。例如,一個常見的原始碼洩露就是Activity類有額外的例項,而正確的是在同一時間應該只有一個例項。要找到特定類的例項,在列表頂部的<Regex>域中輸入類名查詢。

當一個類有太多的例項時,右擊選擇List objects>with incoming references。在顯示的列表中,通過右擊選擇Path To GC Roots> exclude weak references來確定保留的例項。

  • Dominator tree是按照保留堆大小來顯示的物件列表。

應該注意的是那些保留的部分堆大小粗略等於通過GC logsheap updatesallocation tracker觀察到的洩露大小的物件。

當看到可疑項時,右擊選擇Path To GC Roots>exclude weak references。開啟新的標籤頁,標籤頁中列出了可疑洩露的物件的引用。

注意:在靠近餅狀圖中大塊堆的頂部,大部分應用會顯示Resources的例項,但這通常只是因為在應用使用了很多res/路徑下的資源。

圖4.MAT顯示了Histogram view和搜尋”MainActivity”的結果。

想要獲得更多關於MAT的資訊,請觀看2011年Google I/O大會的演講–《Android 應用記憶體管理》(Memory management for Android apps),在大約21:10 的時候有關於MAT的實戰演講。也可以參考文件《Eclipse 記憶體分析文件》(Eclipse Memory Analyzer documentation)。

對比堆轉儲

為了檢視記憶體分配的變化,比較不同時間點應用的堆狀態是很有用的方法。對比兩個堆轉儲可以使用MAT:

1.按照上面描述得到兩個HPROF檔案,具體檢視獲取堆轉儲章節。

2.在MAT中開啟第一個HPROF檔案(File>Open Heap Dump)。

3.在Navigation History檢視(如果不可見,選擇Window>Navigation History),右擊Histogram,選擇Add to Comp are Basket

4.開啟第二個HRPOF檔案,重複步驟2和3。

5.切換到Compare Basket檢視,點選Compare the Results(在檢視右上角的紅色“!”圖示)。

觸發記憶體洩露

使用上述描述工具的同時,還應該對應用程式碼做壓力測試來嘗試復現記憶體洩露。一個檢查應用潛在記憶體洩露的方法,就是在檢查堆之前先執行一會。洩露會慢慢達到分配堆的大小的上限值。當然,洩露越小,就要執行應用越長的時間來複現。

也可以使用下面的方法來觸發記憶體洩露:

1.在不同Activity狀態時,重複做橫豎屏切換操作。旋轉螢幕可能導致應用洩露 ActivityContext 或 View物件,因為系統會重新建立 Activity,如果應用在其它地方持有這些物件的引用,那麼系統就不能回收它們。

2.在不同Activity狀態時,做切換應用操作(切換到主螢幕,然後回到應用中)。

提示:也可以使用monkey測試來執行上述步驟。想要獲得更多執行 monkey 測試的資訊,請查閱 monkeyrunner 文件。

相關文章