天天看點

Android之記憶體洩漏調試學習與總結

  大家有或經常碰到OOM的問題,對吧?很多這樣的問題隻要一出現相信大家的想法跟小馬的一樣,就是自己的應用:優化、優化、再優化!而且如果出現類似于OOM這樣級别的問題,根本就不好處理,LogCat日志中顯示的資訊僅僅是OOM,并不會給你提示如何解決的方法或思路,因為引起OOM的原因是你應用的問題,不是系統問題!應該想下,在優化之前找到需要優化的地方,再去做優化操作不是更直接嗎?相信大多數朋友應該經常聽過或使用Jnuit調試吧,好了,廢話不多說,今天小馬就跟大家一起來學習總結下OOM的調試方法,來找到需要優化的地方,要知道OOM也是可以一步步調試的:

首先,先一起來做些小小的知識鋪墊:

     Android(Java)中常見的容易引起記憶體洩漏的不良代碼:

           1. 查詢資料庫沒有關閉遊标

              程式中經常會進行查詢資料庫的操作,但是經常會有使用完畢Cursor後沒有關閉的情況。如果我們的查詢結果集比較小,對記憶體的消耗不容易被發現,隻有在常時間大量操作的情況下才會複現記憶體問題,這樣就會給以後的測試和問題排查帶來困難和風險示例代如下碼:

  1. Cursor cursor = getContentResolver().query(uri ...); 
  2. if (cursor.moveToNext()) { 
  3.   ... ... 
  4.  修正示例代碼: 
  5. Cursor cursor = null; 
  6. try { 
  7.   cursor = getContentResolver().query(uri ...); 
  8.   if (cursor != null && cursor.moveToNext()) { 
  9.   ... ... 
  10.   } 
  11. } finally { 
  12.   if (cursor != null) { 
  13.   try { 
  14.   cursor.close(); 
  15.   } catch (Exception e) { 
  16.   //ignore this 
  17.   } 
  18.   } 

 2. 構造Adapter時,沒有使用緩存的 convertView ,這個問題小馬上一篇:ListView加載資料原理及優化總結(二十一)中已經講過了,大家可以回過頭看下

                3. Bitmap對象不在使用時調用recycle()沒有及時釋放 

如果一個Bitmap對象比較占記憶體,當它不在被使用的時候,可以調用Bitmap.recycle()方法回收此對象的像素所占用的記憶體

               4.沒有及時釋放對象的引用

                   簡單舉個例子:比如兩個Activity之間傳遞的Context 或其它的自定義對象,使用完後必須立即釋放 即:Activity = null ;    Context = null ; Object = null;可以的話在這釋放對象之後通知系統來回收:System.gc();這樣最好了!

                 Android主要應用在嵌入式裝置當中,而嵌入式裝置由于一些衆所周知的條件限制,通常都不會有很高的配置,特别是記憶體是比較有限的。如果我們編寫的代 碼當中有太多的對記憶體使用不當的地方,難免會使得我們的裝置運作緩慢,甚至是當機。為了能夠使得Android應用程式安全且快速的運作,Android 的每個應用程式都會使用一個專有的Dalvik虛拟機執行個體來運作,它是由Zygote服務程序演變過來的,也就是說每個應用程式都是在屬于自己的程序中運作的(問題一:這個地方小馬怎麼知道是在屬于自己的程序中運作的?大家繼續,答案小馬會在下面詳細介紹)。一方面,如果程式在運作過程中出現了記憶體洩漏的問題,僅僅會使得自己的程序被殺掉,而不會影響其他程序(如果是system_process 等系統程序出問題的話,則會引起系統重新開機)。另一方面Android為不同類型的程序配置設定了不同的記憶體使用上限,如果應用程序使用的記憶體超過了這個上限, 則會被系統視為記憶體洩漏,進而被殺掉

                下面小馬來解釋下問題一:”每個應用程式都是在屬于自己的程序中運作的”這句話,對于這句話,大家隻記住一點:“當一個程式第一次啟動的時候,Android會啟動一個LINUX程序和一個主線程。預設的情況下,所有該程式的元件都将在該程序和線程中運作。”~^_^   O_O !!!

同時,Android會為每個應用程式配置設定一個單獨的LINUX使用者。Android會盡量保留一個正在運作程序,隻在記憶體資源出現不足時,Android會嘗試停止一些程序進而釋放足夠的資源給其他新的程序使用, 也能保證使用者正在通路的目前程序有足夠的資源去及時地響應使用者的事件。Android會根據程序中運作的元件類别以及元件的狀态來判斷該程序的重要性,Android會首先停止那些不重要的程序。按照重要性從高到低一共有五個級别就是我們常說的:前台程序、可見程序、服務程序、背景程序、空程序(此處概念略,大家自己查)

                還有個小擴充:當一個程式第一次啟動時,Android會同時啟動一個對應的主線程(Main Thread),主線程主要負責處理與UI相關的事件,如使用者的按鍵事件,使用者接觸螢幕的事件以及螢幕繪圖事件,并把相關的事件分發到對應的元件進行處理。是以主線程通常又被叫做UI線程。在開發Android應用時必須遵守單線程模型的原則: Android UI操作并不是線程安全的并且這些操作必須在UI線程中執行。Android的UI是單線程(Single-threaded)的。為了避免拖住GUI,一些較費時的對象應該交給獨立的線程去執行。如果幕後的線程來執行UI對象,Android就會發出錯誤訊息 CalledFromWrongThreadException。以後遇到這樣的異常抛出時就要知道怎麼回事咯!

            好了,鋪墊知識就寫這麼多了,下面直接進入主題了:OOM調試

方式一:使用記憶體監測工具 DDMS –> Heap:(真機、模拟器均可使用)

               1. 啟動eclipse後,切換到DDMS透視圖,并确認Devices視圖、Heap視圖都是打開的,沒打開的直接Window>ShowView>自己選;

               2. 将手機通過USB連結至電腦,連結時需要确認手機是處于“USB調試”模式

               3. 連結成功後,在DDMS的Devices視圖中将會顯示手機裝置的序列号,以及裝置中正在運作的部分程序資訊;

               4. 點選選中想要監測的程序,如果在程序清單中未出現你的程序的話随便選中一條讓Device一排的工具處于可用狀态,再點選下Update Heap讓其自動找到我們跑的應用的程序,比如小馬臨時跑的兩個應用程序如圖;

Android之記憶體洩漏調試學習與總結

 5. 點選Heap視圖中的“Cause GC”按鈕;

Android之記憶體洩漏調試學習與總結

 6.點選Cause GC之後就可以看到我們應用的記憶體情況如下圖:

Android之記憶體洩漏調試學習與總結

說明:

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

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

c) 記憶體使用資訊的各項參數根據名稱即可知道其意思,不知道具體意思的朋友自行用工具(有道、詞霸查去)

       知道工具使用了,那麼如何才能知道我們的程式是否有記憶體洩漏的可能性呢。這裡需要注意一個值:Heap視圖中部有一個Type叫做data object,即資料對象,也就是我們的程式中大量存在的類類型的對象。在data object一行中有一列是“Total Size”,其值就是目前程序中所有Java資料對象的記憶體總量,如果大家想要看“Total Size”是配置設定的具體資訊可以點選“data object這一行來檢視詳細資訊,如下圖”(大家看不清楚的點選看大圖)

Android之記憶體洩漏調試學習與總結

一般情況下,在data object行的“Total Size”這個值的大小決定了是否會有記憶體洩漏。可以這樣判斷:

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

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

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

  直到到達一個上限後導緻程序被殺掉。

             Android為應用程序配置設定的記憶體上限如下所示:(下面這些是小馬網上查到的,小馬不懂下面的文法,但知道有限制這麼一回事就夠了,此處不研究下面的代碼)

位置: /ANDROID_SOURCE/system/core/rootdir/init.rc 部分腳本

# Define the oom_adj values for the classes of processes that can be

# killed by the kernel. These are used in ActivityManagerService.

  setprop ro.FOREGROUND_APP_ADJ 0

  setprop ro.VISIBLE_APP_ADJ 1

  setprop ro.SECONDARY_SERVER_ADJ 2

  setprop ro.BACKUP_APP_ADJ 2

  setprop ro.HOME_APP_ADJ 4

  setprop ro.HIDDEN_APP_MIN_ADJ 7

  setprop ro.CONTENT_PROVIDER_ADJ 14

  setprop ro.EMPTY_APP_ADJ 15

# Define the memory thresholds at which the above process classes will

# be killed. These numbers are in pages (4k).

  setprop ro.FOREGROUND_APP_MEM 1536

  setprop ro.VISIBLE_APP_MEM 2048

  setprop ro.SECONDARY_SERVER_MEM 4096

  setprop ro.BACKUP_APP_MEM 4096

  setprop ro.HOME_APP_MEM 4096

  setprop ro.HIDDEN_APP_MEM 5120

  setprop ro.CONTENT_PROVIDER_MEM 5632

  setprop ro.EMPTY_APP_MEM 6144

# Write value must be consistent with the above properties.

# Note that the driver only supports 6 slots, so we have HOME_APP at the

# same memory level as services.

  write /sys/module/lowmemorykiller/parameters/adj 0,1,2,7,14,15

  write /proc/sys/vm/overcommit_memory 1

  write /proc/sys/vm/min_free_order_shift 4

  write /sys/module/lowmemorykiller/parameters/minfree 1536,2048,4096,5120,5632,6144

  # Set init its forked children’s oom_adj.

  write /proc/1/oom_adj -16

 d) 此處以com.xiaoma.www程序為例,在我的測試環境中com.xiaoma.www程序所占用的記憶體的data object的Total Size正常情況下穩定在0.8~1.0M之間,而當其值超過3~5M每次啟動應用該值不穩定的時候程序就會被系統殺掉啦!

方式二:

記憶體監測工具 DDMS –> Heap 

使用記憶體分析工具 MAT(Memory Analyzer Tool) 

(一) 生成.hprof檔案 (生成很簡單,直接點選Device 工具欄中的 Dump HPROF file即可生成)

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

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

  總之,使用DDMS的Heap視圖工具可以很友善的确認我們的程式是否存在記憶體洩漏的可能性。這個地方順帶着也簡單的說一下,如果大家想要跟蹤更詳細的記憶體是怎樣配置設定的話,可以學着使用下Windows>ShowView>Allocation Tracker工具來定位你的記憶體什麼時候被什麼東西占用了,是bundlea或HashMap或ArrayList焦點或是其它的什麼占用了這些都可以在這個插件中檢視到的,簡單的使用方法就是:選中Device程序清單中的某一個程序,讓Allocation Tracker裡面的跟蹤工具處于可用狀态,點選Stop Tracking來跟蹤程式,過一定時間後,點選Get Allocations來擷取記憶體配置設定的消息就可以檢視更為詳細的記憶體配置設定情況了,如下圖:

繼續閱讀