Dalvik 虛拟機支援垃圾收集,但是這不意味着你可以不用關心記憶體管理。你應該格外注意移動裝置的記憶體使用,手機和平闆的記憶體空間是受到限制的。
在這篇文章裡面,我們來看看Android SDK裡面的一些記憶體剖析工具(profiling tools)是如何幫助我們修整應用程式的記憶體使用。
一、 記憶體洩露
一些記憶體使用問題是很明顯的,例如,如果在每次使用者觸摸螢幕的時候應用程式有記憶體洩露,将會有可能觸發OutOfMemoryError,最終程式崩潰。
另外一些問題卻很微妙,也許隻是降低應用程式和整個系統的性能(當高頻率和長時間地運作垃圾收集器的時候)。
二、 記憶體工具
Android SDK 提供了2個主要的剖析應用程式記憶體使用情況的工具:Allocation Tracker 和 heap dumps
1)Allocation Tracker是很有用的,特别是當你想得到程式在一定的時間裡記憶體的配置設定情況的一種感性認識的時候。但是它不能給你任何關于程式heap總體情況的任何 資訊。關于Allocation Tracker的更多資訊,請看文章Tracking Memory Allocations
2)heap dumps,它是更強大的記憶體分析工具,一個heap dump就是一個程式heap的快照,它儲存為一種叫做HPROF的二進制格式。Dalvik用的也是類似的格式,但是不完全一樣,這裡是Java 的HPROF工具 。有很多方法去生成一個運作時應用程式的heap dump,其中一種就是使用在DDMS裡邊的Dump HPROF file按鈕,如果想産生更精确的dump資料,可以在程式中使用android.os.Debug.dumpHprofData() 方法。
分析heap dump,你可以使用一些标準的工具比如 jhat 或者Eclipse MAT(Memory Analyzer Tool) 。不過,首先需要把.hprof檔案從Dalvik格式轉換成J2SE HPROF格式,你可以使用Android SDK提供的hprof-conv工具。例如:
hprof-conv dump.hprof converted-dump.hprof
三、 記憶體調試
Dalvik 運作時裡,程式員不能顯式地配置設定和釋放記憶體,是以這裡的記憶體洩露跟C和C++裡面的不同。在你的代碼裡,記憶體洩露就是你保留了一個并不再需要的類對象的引用,有時候僅僅一個引用就會阻礙gc對一大堆對象的回收。
我們來過一個實際的例子,Android SDK裡面提供的範例程式Honeycomb Gallery sample app 。它是一個photo gallery程式,用來示範一些新的Honeycomb API的使用。(下載下傳和編譯這些代碼,請看這些 指令 )我們會有意地加入一個記憶體洩露在程式裡邊,然後來示範如何調試它。
想象一下我們想修改程式讓它從網絡下載下傳圖檔,為了讓它更具備靈活性,我們可以考慮實作一個緩存,儲存最近檢視過的圖檔,我們可以對ContentFragment.java做一些小的修改來達到這個目的。在class頂部,我們增加一個新的靜态變量:
private static HashMap<String,Bitmap> sBitmapCache = new HashMap<String,Bitmap>();
這裡是我們儲存緩存的地方,現在可以修改updateContentAndRecycleBitmap()方法,讓它在下載下傳之前先檢視是否資料已經存在,如果不存在就去下載下傳,然後添加資料到緩存。
[java] view plain copy print ?
- void updateContentAndRecycleBitmap (int category, int position) {
- if (mCurrentActionMode != null) {
- mCurrentActionMode.finish();
- }
- // Get the bitmap that needs to be drawn and update the ImageView.
- // Check if the Bitmap is already in the cache
- String bitmapId = "" + category + "." + position;
- mBitmap = sBitmapCache.get(bitmapId);
- if (mBitmap == null) {
- // It's not in the cache, so load the Bitmap and add it to the cache.
- // DANGER! We add items to this cache without ever removing any.
- mBitmap = Directory.getCategory(category).getEntry(position).getBitmap(getResources());
- sBitmapCache.put(bitmapId, mBitmap);
- }
- ((ImageView) getView().findViewById(R.id.image)).setImageBitmap(mBitmap);
- }
void updateContentAndRecycleBitmap (int category, int position) {
if (mCurrentActionMode != null) {
mCurrentActionMode.finish();
}
// Get the bitmap that needs to be drawn and update the ImageView.
// Check if the Bitmap is already in the cache
String bitmapId = "" + category + "." + position;
mBitmap = sBitmapCache.get(bitmapId);
if (mBitmap == null) {
// It's not in the cache, so load the Bitmap and add it to the cache.
// DANGER! We add items to this cache without ever removing any.
mBitmap = Directory.getCategory(category).getEntry(position).getBitmap(getResources());
sBitmapCache.put(bitmapId, mBitmap);
}
((ImageView) getView().findViewById(R.id.image)).setImageBitmap(mBitmap);
}
在這裡故意引入了一個記憶體洩露的問題:我們把圖檔加入了緩存但是從來沒有移除他們,在真實的應用裡,我們可以會用某種方法來限制緩存的大小。
四、 DDMS檢查heap
Dalvik Debug Monitor Server(DDMS)是主要的Android調試工具之一,也是 ADT Plugin for Eclipse 的一部分,獨立的程式版本也可以在Android SDK的根目錄下的tools/下面找到。關于DDMS更多的資訊,請參考使用DDMS 。
我們來使用DDMS檢查這個應用的heap使用情況,你可以使用下面的兩種方法啟動DDMS:
- from Eclipse: click Window —> Open Perspective —> Other... —> DDMS
- from the command line: run
(orddms
on Mac/Linux) in the./ddms
directorytools/

在左邊的面闆,選擇程序com.example.android.hcgallery,然後在工具條上邊點選Show heap updates按鈕。這個時候切換到DDMS的VM Heap分頁,它會顯示每次gc後heap記憶體的一些基本資料。
如果想檢視第一次gc後的資料内容,點選Cause GC按鈕:
我們可以看到現在的值(Allocated列)是有一些超過8MB。現在滑動相片,這時看到 資料在增大,因為隻有僅僅13個相片在程式裡邊,是以洩露的記憶體隻有這麼大。在某種程度上來說,這是最壞的一種記憶體洩露,因為我們沒法得到 OutOfMemoryError來提醒我們說現在記憶體溢出了。
五、 生成heap dump
為了使用heap dump來追蹤這個問題,首先要儲存HPROF檔案:
點選DDMS工具條上面的Dump HPROF檔案按鈕,選擇檔案存儲位置,然後在運作hprof-conv。在這個例子裡我們使用獨立的MAT版本(版本1.0.1),MAT下載下傳 。
如果使用ADT(它包含DDMS的插件)同時也在eclipse裡面安裝了MAT,點選“dump HPROF”按鈕将會自動地做轉換(用hprof-conv),同時會在eclipse裡面打開轉換後的hprof檔案(它其實用MAT打開)。
六、 MAT分析heap dumps
啟動MAT,然後加載剛才我們生成的HPROF檔案。
MAT是一個強大的工具,講述它所有的特性超出了本文的範圍,是以我隻想示範一種你可以用來檢測 洩露的方法:直方圖(Histogram)視圖。它顯示了一個可以排序的類執行個體的清單,内容包括:shallow heap(所有執行個體的記憶體使用總和),或者retained heap(所有類執行個體被配置設定的記憶體總和,裡面也包括他們所有引用的對象)。
如果我們按照shallow heap排序,我們可以看到byte[] 執行個體在頂端。自從Android 3.0(Honeycomb),Bitmap的像素資料被存儲在byte數組裡 (之前是被存儲在Dalvik的heap裡),是以基于這個對象的大小來判斷,不用說它一定是我們洩露掉的bitmap。
右擊byte[] 類,然後選擇List Objects —> with incoming references,它會生成一個heap上的所有byte數組的清單,在清單裡我們可以按照Shallow Heap的使用情況來排序。
選擇并展開一個比較大的對象,它将展示從根到這個對象的路徑 —— 就是一條保證對象有效的鍊條。注意看,這個就是我們的bitmap緩存!
MAT 不會明确告訴我們這就是洩露,因為它也不知道這個東西是不是程式還需要的,隻有程式員知道。
在這個案例裡面,緩存使用的大量的記憶體會影響到後面的應用程式,是以我們可以考慮限制緩存的大小。
七、 MAT 比較兩個heap dumps
調試記憶體洩露時,有時候适時比較2個地方的heap狀态是很有用的。這時你就需要生成2個單獨的HPROF檔案(不要忘了轉換格式),下面是一些關于如何在MAT裡比較2個heap dumps的内容(有一點複雜):
- 第一個HPROF 檔案(using File —> Open Heap Dump )
- 打開 Histogram view
- 在Navigation History view裡 (如果看不到就從Window —> Navigation History找 ), 右擊histogram然後選擇Add to Compare Basket
- 打開第二個HPROF 檔案然後重做步驟2和3
- 切換到Compare Basket view,然後點選Compare the Results (視圖右上角的紅色"!"圖示)。
八、 總結
這本篇文章裡面,我展示了Allocation Tracker 和 heap dumps是如何給你一種對程式記憶體使用的感性認識。我也展示了MAT可以幫助追逐我們程式裡面的記憶體洩露問題。
MAT是一個強大的工具,我也僅僅觸碰了一些皮毛,如果你想學習更多内容,我建議讀 一些下面的文章:
- Eclipse MAT project的官方部落格:Memory Analyzer Blog 或 Blog.sixxs.org
- Markus Kohler 的 有關Java性能的部落格:Java Performance blog 或 blog.sixxs.org
說明: 上面兩個部落格網址,可能被“牆”了,需要通過VPN 或 IPv6 + sixxs.org代理通路