1. 程式人生 > >android之 MAT、DDMS 等記憶體檢視工具

android之 MAT、DDMS 等記憶體檢視工具

三、記憶體監測工具 DDMS --> Heap

    無論怎麼小心,想完全避免bad code是不可能的,此時就需要一些工具來幫助我們檢查程式碼中是否存在會造成記憶體洩漏的地方。Android tools中的DDMS就帶有一個很不錯的記憶體監測工具Heap(這裡我使用eclipse的ADT外掛,並以真機為例,在模擬器中的情況類似)。用Heap監測應用程序使用記憶體情況的步驟如下:

1. 啟動eclipse後,切換到DDMS透檢視,並確認Devices檢視、Heap檢視都是開啟的;

2. 將手機通過USB連結至電腦,連結時需要確認手機是處於“USB除錯”模式,而不是作為“Mass Storage”;

3. 連結成功後,在DDMS的Devices檢視中將會顯示手機裝置的序列號,以及裝置中正在執行的部分程序資訊;

4. 點選選中想要監測的程序,比如system_process程序;

5. 點選選中Devices檢視介面中最上方一排圖示中的“Update Heap”圖示;

6. 點選Heap檢視中的“Cause GC”按鈕;

7. 此時在Heap檢視中就會看到當前選中的程序的記憶體使用量的詳細情況[如圖所示]。

說明:

a) 點選“Cause GC”按鈕相當於向虛擬機器請求了一次gc操作;

b) 當記憶體使用資訊第一次顯示以後,無須再不斷的點選“Cause GC”,Heap檢視介面會定時重新整理,在對應用的不斷的操作過程中就可以看到記憶體使用的變化;

c) 記憶體使用資訊的各項引數根據名稱即可知道其意思,在此不再贅述。

    如何才能知道我們的程式是否有記憶體洩漏的可能性呢。這裡需要注意一個值:Heap檢視中部有一個Type叫做data object,即資料物件,也就是我們的程式中大量存在的類型別的物件。在data object一行中有一列是“Total Size”,其值就是當前程序中所有Java資料物件的記憶體總量,一般情況下,這個值的大小決定了是否會有記憶體洩漏。可以這樣判斷:

a) 不斷的操作當前應用,同時注意觀察data object的Total Size值;

b) 正常情況下Total Size值都會穩定在一個有限的範圍內,也就是說由於程式中的的程式碼良好,沒有造成物件不被垃圾回收的情況,所以說雖然我們不斷的操作會不斷的生成很多物件,而在虛擬機器不斷的進行GC的過程中,這些物件都被回收了,記憶體佔用量會會落到一個穩定的水平;

c) 反之如果程式碼中存在沒有釋放物件引用的情況,則data object的Total Size值在每次GC後不會有明顯的回落,隨著操作次數的增多Total Size的值會越來越大,

    直到到達一個上限後導致程序被kill掉。

d) 此處已system_process程序為例,在我的測試環境中system_process程序所佔用的記憶體的data object的Total Size正常情況下會穩定在2.2~2.8之間,而當其值超過3.55後進程就會被kill。

    總之,使用DDMS的Heap檢視工具可以很方便的確認我們的程式是否存在記憶體洩漏的可能性。

四、記憶體分析工具 MAT(Memory Analyzer Tool)

    如果使用DDMS確實發現了我們的程式中存在記憶體洩漏,那又如何定位到具體出現問題的程式碼片段,最終找到問題所在呢?如果從頭到尾的分析程式碼邏輯,那肯定會把人逼瘋,特別是在維護別人寫的程式碼的時候。這裡介紹一個極好的記憶體分析工具 -- Memory Analyzer Tool(MAT)。

    MAT是一個Eclipse外掛,同時也有單獨的RCP客戶端。官方下載地址、MAT介紹和詳細的使用教程請參見:www.eclipse.org/mat,在此不進行說明了。另外在MAT安裝後的幫助文件裡也有完備的使用教程。在此僅舉例說明其使用方法。我自己使用的是MAT的eclipse外掛,使用外掛要比RCP稍微方便一些。

    使用MAT進行記憶體分析需要幾個步驟,包括:生成.hprof檔案、開啟MAT並匯入.hprof檔案、使用MAT的檢視工具分析記憶體。以下詳細介紹。

(一) 生成.hprof檔案

    生成.hprof檔案的方法有很多,而且Android的不同版本中生成.hprof的方式也稍有差別,我使用的版本的是2.1,各個版本中生成.prof檔案的方法請參考:

http://android.git.kernel.org/?p=platform/dalvik.git;a=blob_plain;f=docs/heap-profiling.html;hb=HEAD。

1. 開啟eclipse並切換到DDMS透檢視,同時確認Devices、Heap和logcat檢視已經打開了;

2. 將手機裝置連結到電腦,並確保使用“USB 除錯”模式連結,而不是“Mass Storage“模式;

3. 連結成功後在Devices檢視中就會看到裝置的序列號,和裝置中正在執行的部分程序;

4. 點選選中想要分析的應用的程序,在Devices檢視上方的一行圖示按鈕中,同時選中“Update Heap”和“Dump HPROF file”兩個按鈕;

5. 這是DDMS工具將會自動生成當前選中程序的.hprof檔案,並將其進行轉換後存放在sdcard當中,如果你已經安裝了MAT外掛,那麼此時MAT將會自動被啟用,並開始對.hprof檔案進行分析;

    注意:第4步和第5步能夠正常使用前提是我們需要有sdcard,並且當前程序有向sdcard中寫入的許可權(WRITE_EXTERNAL_STORAGE),否則.hprof檔案不會被生成,在logcat中會顯示諸如

     ERROR/dalvikvm(8574): hprof: can't open /sdcard/com.xxx.hprof-hptemp: Permission denied.

    的資訊。

    如果我們沒有sdcard,或者當前程序沒有向sdcard寫入的許可權(如system_process),那我們可以這樣做:

6. 在當前程式中,例如framework中某些程式碼中,可以使用android.os.Debug中的:

   public static void dumpHprofData(String fileName) throws IOException

   方法,手動的指定.hprof檔案的生成位置。例如:

   xxxButton.setOnClickListener(new View.OnClickListener() {

       public void onClick(View view) {

          android.os.Debug.dumpHprofData("/data/temp/myapp.hprof");

          ... ...

       }

   }

    上述程式碼意圖是希望在xxxButton被點選的時候開始抓取記憶體使用資訊,並儲存在我們指定的位置:/data/temp/myapp.hprof,這樣就沒有許可權的限制了,而且也無須用sdcard。但要保證/data/temp目錄是存在的。這個路徑可以自己定義,當然也可以寫成sdcard當中的某個路徑。

(二) 使用MAT匯入.hprof檔案

1. 如果是eclipse自動生成的.hprof檔案,可以使用MAT外掛直接開啟(可能是比較新的ADT才支援);

2. 如果eclipse自動生成的.hprof檔案不能被MAT直接開啟,或者是使用android.os.Debug.dumpHprofData()方法手動生成的.hprof檔案,則需要將.hprof檔案進行轉換,轉換的方法:

    例如我將.hprof檔案拷貝到PC上的/ANDROID_SDK/tools目錄下,並輸入命令hprof-conv xxx.hprof yyy.hprof,其中xxx.hprof為原始檔案,yyy.hprof為轉換過後的檔案。轉換過後的檔案自動放在/ANDROID_SDK/tools目錄下。OK,到此為止,.hprof檔案處理完畢,可以用來分析記憶體洩露情況了。

3. 在Eclipse中點選Windows->Open Perspective->Other->Memory Analyzer,或者打Memory Analyzer Tool的RCP。在MAT中點選File->Open File,瀏覽並匯入剛剛轉換而得到的.hprof檔案。

(三) 使用MAT的檢視工具分析記憶體

    匯入.hprof檔案以後,MAT會自動解析並生成報告,點選Dominator Tree,並按Package分組,選擇自己所定義的Package類點右鍵,在彈出選單中選擇List objects->With incoming references。這時會列出所有可疑類,右鍵點選某一項,並選擇Path to GC Roots -> exclude weak/soft references,會進一步篩選出跟程式相關的所有有記憶體洩露的類。據此,可以追蹤到程式碼中的某一個產生洩露的類。

    MAT的介面如下圖所示。

    具體的分析方法在此不做說明了,因為在MAT的官方網站和客戶端的幫助文件中有十分詳盡的介紹。

    瞭解MAT中各個檢視的作用很重要,例如www.eclipse.org/mat/about/screenshots.php中介紹的。

    總之使用MAT分析記憶體查詢記憶體洩漏的根本思路,就是找到哪個類的物件的引用沒有被釋放,找到沒有被釋放的原因,也就可以很容易定位程式碼中的哪些片段的邏輯有問題了

用 Heap監測應用程序使用記憶體情況的步驟如下:

1. 啟動eclipse後,切換到DDMS透檢視,並確認Devices檢視、Heap檢視都是開啟的;
2. 將手機通過USB連結至電腦,連結時需要確認手機是處於“USB除錯”模式,而不是作為“Mass Storage”;
3. 連結成功後,在DDMS的Devices檢視中將會顯示手機裝置的序列號,以及裝置中正在執行的部分程序資訊;
4. 點選選中想要監測的程序,比如system_process程序;
5. 點選選中Devices檢視介面中最上方一排圖示中的“Update Heap”圖示;
6. 點選Heap檢視中的“Cause GC”按鈕;
7. 此時在Heap檢視中就會看到當前選中的程序的記憶體使用量的詳細情況。
說明:
a) 點選“Cause GC”按鈕相當於向虛擬機器請求了一次gc操作;
b) 當記憶體使用資訊第一次顯示以後,無須再不斷的點選“Cause GC”,Heap檢視介面會定時重新整理,在對應用的不斷的操作過程中就可以看到記憶體使用的變化;
c) 記憶體使用資訊的各項引數根據名稱即可知道其意思,在此不再贅述。
  如何才能知道我們的程式是否有記憶體洩漏的可能性呢。這裡需要注意一個值:Heap檢視中部有一個Type叫做data object,即資料物件,也就是我們的程式中大量存在的類型別的物件。在data object一行中有一列是“Total Size”,其值就是當前程序中所有Java資料物件的記憶體總量,一般情況下,這個值的大小決定了是否會有記憶體洩漏。可以這樣判斷:
a) 不斷的操作當前應用,同時注意觀察data object的Total Size值;
b) 正常情況下Total Size值都會穩定在一個有限的範圍內,也就是說由於程式中的的程式碼良好,沒有造成物件不被垃圾回收的情況,所以說雖然我們不斷的操作會不斷的生成很多對 象,而在虛擬機器不斷的進行GC的過程中,這些物件都被回收了,記憶體佔用量會會落到一個穩定的水平;
c) 反之如果程式碼中存在沒有釋放物件引用的情況,則data object的Total Size值在每次GC後不會有明顯的回落,隨著操作次數的增多Total Size的值會越來越大,
  直到到達一個上限後導致程序被kill掉。
d) 此處已system_process程序為例,在我的測試環境中system_process程序所佔用的記憶體的data object的Total Size正常情況下會穩定在2.2~2.8之間,而當其值超過3.55後進程就會被kill。

在DDMS裡檢查heap的使用情況

Dalvik Debug Monitor Server(DDMS)是主要的Android除錯工具之一,也是ADT Eclipse plug-in 的一部分,獨立的程式版本也可以在Android SDK的根目錄下的tools/下面找到。關於DDMS更多的資訊,請參考使用DDMS

我們來使用DDMS檢查這個應用的heap使用情況。你可以使用下面的兩種方法啟動DDMS:

  • from Eclipse: click Window > Open Perspective > Other... > DDMS
  • or from the command line: run ddms (or ./ddms on Mac/Linux) in thetools/ directory

在左邊的面板選擇程序com.example.android.hcgallery,然後在 工具條上邊點選Show heap updates按鈕。這個時候切換到DDMS的VM Heap分頁。它會顯示每次gc後heap記憶體的一些基本資料。要看第一次gc後的資料內容,點選Cause GC按鈕:

我們可以看到現在的值(Allocated列)是有一些超過8MB。現在滑動相片,這時看到 資料在增大。因為只有僅僅13個相片在程式裡邊,所以洩露的記憶體只有這麼大。在某種程度上來說,這時最壞的一種記憶體洩露,因為我們沒法得到 OutOfMemoryError來提醒我們說現在記憶體溢位了。

生成heap dump

我們現在使用heap dump來追蹤這個問題。點選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[]例項在頂端。自從Android3.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的內容(有一點複雜):

  1. 第一個HPROF 檔案(using File > Open Heap Dump ).
  2. 開啟 Histogram view.
  3. 在Navigation History view裡 (如果看不到就從Window > Navigation History找 ), 右擊histogram 然後選擇Add to Compare Basket .
  4. 開啟第二個HPROF 檔案然後重做步驟2和3.
  5. 切換到Compare Basket view, 然後點選Compare the Results (檢視右上角的紅色"!"圖示)。

Android模擬器命令列啟動模式
在android-sdk-windows-1.1\tools執行emulator以執行模擬器
加上-skin引數,指定顯示模式為HVGA-L,則可轉為橫向
emulator - skin HVGA-L (480*320,水平顯示)
emulator - skin HVGA-L (320*480,垂直顯示,模擬器預設模式)
emulator - skin HVGA-L (320*240,水平顯示)
emulator - skin HVGA-L (240*320,垂直顯示)


使用mksdcard指令模擬1GB的記憶卡
mksdcard 1024M sacard.img

模擬插入 SD 卡的模擬器
emulator - sdcard sdcard.img

使用 adb+push 上載檔案到SD記憶卡
adb push 001.jpg /sdcard (複製檔案到 /sdcard 目錄下)
adb push pictures /sdcard (複製 picture 照片目錄到 /sdcard 目錄下)
adb push mp3 /sdcard (複製 mp3 音樂目錄到 /sdcard 目錄下)
adb shell (Android 模擬器啟動命令列模式)
#cd /sdcard (進入 /sdcard 目錄)
#ls (檢視 SD 記憶卡中的檔案)


使用 adb+pull 從 SD 記憶卡下載檔案
adb pull /sdcard/001.jpg . (下載 /sdcard 目錄下的檔案)
adb pull /sdcard/pictures . (下載 sdcard 目錄下的 pictures 目錄)


刪除 SD 卡裡面的檔案
adb shell
#ced /sdcard
#rm 001.jpg (刪除 SD 記憶卡裡的檔案)
#rm -r * (刪除 SD 記憶卡裡所有檔案與目錄)


Android模擬器影片播放方法
mksdcard 4096M video.img (製作一個影像檔的 SD 記憶卡)
adb push video.avi /sdcard (從電腦複製影像檔到 SD 卡中)
emulator -sdcard video.img (啟動模擬器並載入 SD 卡)

下載免費的影片播放軟體,ex: Meridian Video Player (iiivpa.apk)
http://sites.google.com/site/eternalsandbox/Home/meridian-video-player
adb install iiivpa.apk (安裝Meridian Video Player)
接下來就可以用裝上去的player播放.mp4、3gp與.wmv三種檔案格式

安裝 APK 應用程式
adb install filename.apk (安裝filename.apk)
adb install -r filename.apk (保留已設定資料,重新安裝filename.apk)
adb -s emulator-5554 install filename.apk (指定安裝 APK 套件在 5554 的 Android 模擬器中)


移除 APK 應用程式
adb uninstall package
adb uninstall -k package (移除程式時,保留資料)

此package名稱不是安裝APK套裝時的檔名或顯示在模擬器中的應用程式名稱
可以先到/data/data或data/app目錄下,查詢想移除的package名稱
adb shell
ls /data/data 或 /data/app (查詢 Package 名稱)
exit
adb uninstall package (移除查詢到的 Package)


ADB 系統除錯與連結工具
$adb devices (顯示目前有多少個模擬器正在執行)
$adb -s <serialNumber> <command> (指定模擬器來操作)
adb -s emulator-5554 install email.apk

$adb install apkfile (安裝 APK 應用程式套件)
adb install email.apk
$adb uninstall package (移除 APK 應用程式套件)
adb uninstall com.android.email

$adb shell (進入 Android 系統指令列模式)
$ls
$dmesg (檢視 Android Linux Kernel 運作訊息)

ls - 顯示檔案目錄
cd - 進入目錄
rm - 刪除檔案
mv - 移動檔案
mkdir - 產生目錄
rmdir - 刪除目錄

$adb push <file/dir> (複製檔案到 SD 卡)
adb push mp3 /sdcard
$adb pull <file/dir> . (從 Android 系統下載檔案)
adb pull /data/app/com.android.email

$adb logcat (監控模擬器運作紀錄,以Ctrl + c 離開監控模式)
$adb bugreport (產生 adb 除錯報告)
$adb get-state (獲得 adb 伺服器運作狀態)
$adb start-server (啟動 adb 伺服器)
$adb kill-server (關掉 adb 伺服器)

$adb forward tcp:6100 tcp:7100 (更改模擬器網路 TCP 通訊埠)
$adb shell ps -x (顯示 Android 上所有正在執行的行程)
$adb version (顯示 adb 版本)
$adb help (顯示 adb 指令引數)

Emulator 命令列啟動引數
emulator -timezone Asia/Taipei (指定時區)
emulator -no-boo-anim (省略開機小機器人動畫畫面)
emulator -scale auto (調整模擬器視窗大小)
emulator - scale factor (factor: 0.1-3.0)

emulator -dpi-device 300 (更改模擬器的解析度,default為 165dpi)
emulator -skin <skinID> (更改模擬器顯示模式)
emulator -help-keys (顯示鍵盤快速鍵說明)
emulator -shell (相當於adb shell 功能)
emulator -data data.img (使 /data 目錄使用 data.img 的檔案空間)
emulator -sdcard sdcard.img (使 /sdcard 目錄使用 sdcard.img 的檔案空間)
emulator -cache cache.img (瀏覽器暫存檔儲存空間)

emulator -wipe-data (使模擬器恢復到原廠設定)
emulator -help (顯示 emulator 指令引數)