記憶體洩漏和溢位整理(二)
一、Android的記憶體機制
android應用層是由java開發的,android的davlik虛擬機器與jvm也類似,只不過它是基於暫存器的。在java中,通過new為物件分配記憶體,所有物件在java堆內分配空間;而記憶體的釋放是由垃圾收集器(GC)來回收的。 Java採用了有向圖的原理。Java將引用關係考慮為圖的有向邊,有向邊從引用者指向引用物件。執行緒物件可以作為有向圖的起始頂點,該圖就是從起始頂點(GC roots)開始的一棵樹,根頂點可以到達的物件都是有效物件,GC不會回收這些物件。如果某個物件
(連通子圖)與這個根頂點不可達(注意,該圖為有向圖),那麼我們認為這個(這些)物件不再被引用,可以被GC回收。
二、Android的記憶體溢位原因
1、記憶體洩露導致
由於我們程式的失誤,長期保持某些資源(如Context)的引用,垃圾回收器就無法回收它,當然該物件佔用的記憶體就無法被使用,這就造成記憶體洩露。
Android 中常見就是Activity被引用在呼叫finish之後卻沒有釋放,第二次開啟activity又重新建立,這樣的記憶體洩露不斷的發生,則會導致記憶體的溢位。
Android的每個應用程式都會使用一個專有的Dalvik虛擬機器例項來執行,它是由Zygote服務程序孵化出來的,也就是說每個應用程式都是在屬於自己的程序中執行的。Android為不同型別的程序分配了不同的記憶體使用上限,如果程式在執行過程中出現了記憶體洩漏的而造成應用程序使用的記憶體超過了這個上限,則會被系統視為記憶體洩漏,從而被kill掉,這使得僅僅自己的程序被kill掉,而不會影響其他程序.
2、佔用記憶體較多的物件
儲存了多個耗用記憶體過大的物件(如Bitmap)或載入單個超大的圖片,造成記憶體超出限制。
三、常見的記憶體洩漏問題及其解決方案
1、引用沒釋放造成的記憶體洩露
1.1註冊沒取消造成的記憶體洩露
這種Android的記憶體洩露比純Java的記憶體洩漏還要嚴重,因為其他一些Android程式可能引用系統的Android程式的物件(比如註冊機制)。即使Android程式已經結束了,但是別的應用程式仍然還有對Android程式的某個物件的引用,洩漏的記憶體依然不能被垃圾回收。
1.2集合中物件沒清理造成的記憶體洩露
我們通常把一些物件的引用加入到了集合中,當我們不需要該物件時,並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是
1.3 static
static是Java中的一個關鍵字,當用它來修飾成員變數時,那麼該變數就屬於該類,而不是該類的例項。
private static ActivitymContext; //省略
如何才能有效的避免這種引用的發生呢?
第一,應該儘量避免static成員變數引用資源耗費過多的例項,比如Context。
第二、Context儘量使用ApplicationContext,因為Application的Context的生命週期比較長,引用它不會出現記憶體洩露的問題。
- 看使用的週期是否在activity週期內,如果超出,必須用application;常見的情景包括:AsyncTask,Thread,第三方庫初始化等等。
- 還有些情景,只能用activity:比如,對話方塊,各種View,需要startActivity的等。
- 總之,儘可能使用Application。
第三、使用WeakReference代替強引用。比如可以使用WeakReference<Context>mContextRef;
1.4、執行緒(內部類的使用)
執行緒產生記憶體洩露的主要原因在於執行緒生命週期的不可控。如果我們的執行緒是Activity的內部類,所以MyThread中儲存了Activity的一個引用,當MyThread的run函式沒有結束時,MyThread是不會被銷燬的,因此它所引用的老的Activity也不會被銷燬,因此就出現了記憶體洩露的問題。
如果非靜態內部類的方法中,有生命週期大於其所在類的,那就有問題了。比如:AsyncTask、Handler,這兩個類都是方便開發者執行非同步任務的,但是,這兩個都跳出了Activity/Fragment的生命週期。
解決方案
第一、將執行緒的內部類,改為靜態內部類。
原因:
因為非靜態內部類會自動持有一個所屬類的例項,如果所屬類的例項已經結束生命週期,但內部類的方法仍在執行,就會hold其主體(引用)。也就使主體不能被釋放,亦即記憶體洩露。靜態類編譯後和非內部類是一樣的,有自己獨立的類名。不會悄悄引用所屬類的例項,所以就不容易洩露。
第二、如果需要引用Acitivity,使用弱引用。
2、資源物件沒關閉造成的記憶體洩露
資源性物件比如(Cursor,File檔案等)往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收記憶體。而不是等待GC來處理。它們的緩衝不僅存在於java虛擬機器內,還存在於java虛擬機器外。如果我們僅僅是把它的引用設定為null,而不關閉它們,往往會造成記憶體洩露。因為有些資源性物件,比如SQLiteCursor(在解構函式finalize(),如果我們沒有關閉它,它自己會調close()關閉),如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。而且android資料庫中對Cursor資源的是又限制個數的,如果不及時close掉,會導致別的地方無法獲得。
3、一些不良程式碼成記憶體壓力
有些程式碼並不造成記憶體洩露,但是它們,或是對沒使用的記憶體沒進行有效及時的釋放,或是沒有有效的利用已有的物件而是頻繁的申請新記憶體,對記憶體的回收和分配造成很大影響的,容易迫使虛擬機器不得不給該應用程序分配更多的記憶體,造成不必要的記憶體開支。
3.1 Bitmap沒呼叫recycle()
Bitmap物件在不使用時,我們應該先呼叫recycle()釋放記憶體,然後才設定為null.
雖然recycle()從原始碼上看,呼叫它應該能立即釋放Bitmap的主要記憶體,但是測試結果顯示它並沒能立即釋放記憶體。但是我猜應該還是能大大的加速Bitmap的主要記憶體的釋放。
3.2 構造Adapter時,沒有使用快取的 convertView
以構造ListView的BaseAdapter為例,在BaseAdapter中提共了方法:
public View getView(int position, View convertView, ViewGroup parent)
來向ListView提供每一個item所需要的view物件。初始時ListView會從BaseAdapter中根據當前的屏幕布局例項化一定數量的view物件,同時ListView會將這些view物件快取起來。當向上滾動ListView時,原先位於最上面的list item的view物件會被回收,然後被用來構造新出現的最下面的list item。這個構造過程就是由getView()方法完成的,getView()的第二個形參 View convertView就是被快取起來的list item的view物件(初始化時快取中沒有view物件則convertView是null)。
由此可以看出,如果我們不去使用convertView,而是每次都在getView()中重新例項化一個View物件的話,即浪費時間,也造成記憶體垃圾,給垃圾回收增加壓力,如果垃圾回收來不及的話,虛擬機器將不得不給該應用程序分配更多的記憶體,造成不必要的記憶體開支。
四、佔用記憶體較多的物件(圖片過大)造成記憶體溢位及其解決方案
因為Bitmap佔用的記憶體實在是太多了,特別是解析度大的圖片,如果要顯示多張那問題就更顯著了。Android分配給Bitmap的大小隻有8M。
方法1:等比例縮小圖片
有時候,我們要顯示的區域很小,沒有必要將整個圖片都加載出來,而只需要記載一個縮小過的圖片,這時候可以設定一定的取樣率,那麼就可以大大減小佔用的記憶體。
1 |
BitmapFactory.Options
options = new BitmapFactory.Options(); |
2 |
options.inSampleSize
= 2 ; //圖片寬高都為原來的二分之一,即圖片為原來的四分之一 |
因為這些函式在完成decode後,最終都是通過java層的createBitmap來完成的,需要消耗更多記憶體。
因此,改用先通過BitmapFactory.decodeStream方法,創建出一個bitmap,再將其設為ImageView的 source,
decodeStream最大的祕密在於其直接呼叫JNI>>nativeDecodeAsset()來完成decode,
無需再使用java層的createBitmap,從而節省了java層的空間。
方法2:對圖片採用軟引用,及時地進行recycle()操作
雖然,系統能夠確認Bitmap分配的記憶體最終會被銷燬,但是由於它佔用的記憶體過多,所以很可能會超過java堆的限制。因此,在用完Bitmap時,要及時的recycle掉。recycle並不能確定立即就會將Bitmap釋放掉,但是會給虛擬機器一個暗示:“該圖片可以釋放了”。
1 |
SoftReference<Bitmap>
bitmap; |
2 |
bitmap
= new SoftReference<Bitmap>(pBitmap); |
3 |
4 |
if (bitmap
!= null ){ |
5 |
if (bitmap.get()
!= null &&
!bitmap.get().isRecycled()){ |
6 |
bitmap.get().recycle(); |
7 |
bitmap
= null ; |
8 |
} |
9 |
} |
方法3: 單個頁面,橫豎屏切換N次後 OOM
1. 看看頁面佈局當中有沒有大的圖片,比如背景圖之類的。去除xml中相關設定,改在程式中設定背景圖(放在onCreate()方法中):
1 |
Drawable
bg = getResources().getDrawable(R.drawable.bg); |
2 |
XXX.setBackgroundDrawable(rlAdDetailone_bg); |
在Activity destory時注意,bg.setCallback(null); 防止Activity得不到及時的釋放。
2. 跟上面方法相似,直接把xml配置檔案載入成view 再放到一個容器裡,然後直接呼叫 this.setContentView(View view);避免xml的重複載入。
方法4:在頁面切換時儘可能少地重複使用一些程式碼。比如:重複呼叫資料庫,反覆使用某些物件等等.....
方法5:Android堆記憶體也可以自己定義大小和優化Dalvik虛擬機器的記憶體
--參考資料:http://blog.csdn.net/wenhaiyan/article/details/5519567
注意:若使用這種方法:project build target 只能選擇 <= 2.2 版本,否則編譯將通不過。所以不建議用這種方式。
五、Android中記憶體洩露監測
記憶體監測工具 DDMS --> Heap
使用方法比較簡單:
· 選擇DDMS檢視,並開啟Devices檢視和Heap檢視
· 點選選擇要監控的程序,比如:上圖中我選擇的是system_process
· 選中Devices檢視介面上的"update heap" 圖示
· 點選Heap檢視中的"Cause GC" 按鈕(相當於向虛擬機發送了一次GC請求的操作)
在Heap檢視中選擇想要監控的Type,一般我們會觀察dataobject的 total size的變化,正常情況下total size的值會穩定在一個有限的範圍內,也就說程式中的程式碼良好,沒有造成程式中的物件不被回收的情況。如果程式碼中存在沒有釋放物件引用的情況,那麼data object的total size在每次GC之後都不會有明顯的回落,隨著操作次數的增加而total size也在不斷的增加。(說明:選擇好data object後,不斷的操作應用,這樣才可以看出total size的變化)。如果totalsize確實是在不斷增加而沒有回落,說明程式中有沒有被釋放的資源引用。那麼我們應該怎麼來定位呢?
Android中記憶體洩露定位
通過DDMS工具可以判斷應用程式中是否存在記憶體洩漏的問題,那又如何定位到具體出現問題的程式碼片段,最終找到問題所在呢?記憶體分析工具MAT Memory Analyzer Tool解決了這一難題。MAT工具是一個Eclipse 外掛,同時也有單獨的RCP 客戶端,MAT工具的解析檔案是.hprof,這個檔案存放了某程序的記憶體快照。MAT工具定位記憶體洩漏具體位置的方法如下:
① 生成.hprof檔案。Eclipse中生成.hprof檔案的方法有很多,不同Android版本中生成.hprof的方式也稍有差別,但它們整體思路是一樣的。我們在DDMS介面選中想要分析的應用程序,在Devices檢視介面上方的一行圖示按鈕中,同時選中“Update Heap”和“Dump HPROF file”兩個按鈕,這時DDMS將會自動生成當前選中程序的.hprof檔案。
② 將.hprof 檔案匯入到MAT工具中,MAT工具會自動解析並生成報告,點選“Dominator Tree”按鈕,並按包分組,選擇已定義的包類點右鍵,在彈出的選單中選擇List objects﹥With incoming references,這時會列出所有可疑的類。右鍵點選某一項,並選擇Path to GC Roots﹥excludeweak/soft references,MAT工具會進一步篩選出跟程式相關的所有記憶體洩漏的類。這樣就可以追蹤到某一個產生記憶體洩漏的類的具體程式碼中。
使用MAT記憶體分析工具查詢記憶體洩漏的根本思路是找到哪個類的物件的引用沒有被釋放,然後分析沒有被釋放的原因,最終定位到程式碼中哪些片段存在著記憶體洩漏。