天天看點

記憶體溢出問題 如何定位和解決Andorid的記憶體溢出問題(大總結)

如何定位和解決Andorid的記憶體溢出問題(大總結)

分類: android  |  标簽: 記憶體溢出,虛拟機,記憶體洩漏,oom,bitmap  |  作者: zhoubin1992  相關  |  釋出日期 : 2015-06-28  |  熱度 : 8°

我們經常在做項目過程中遇到記憶體溢出的問題,同時面試中關于OOM的問題也常常出現。

這裡,我将前輩們解決Andorid記憶體溢出的方法重新整理一番,友善自己以後使用。最後附上參考博文。

一、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集合中對象沒清理造成的記憶體洩露

我們通常把一些對象的引用加入到了集合中,當我們不需要該對象時,并沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。

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

;

//圖檔寬高都為原來的二分之一,即圖檔為原來的四分之一

盡量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource來設定一張大圖,

因為這些函數在完成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

記憶體溢出問題 如何定位和解決Andorid的記憶體溢出問題(大總結)

使用方法比較簡單:

·        選擇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記憶體分析工具查找記憶體洩漏的根本思路是找到哪個類的對象的引用沒有被釋放,然後分析沒有被釋放的原因,最終定位到代碼中哪些片段存在着記憶體洩漏。