Android應用中OOM問題剖析和解決方案

1.什麼是OOM?

  1. 03-21 21:05:28.771: E/dalvikvm-heap(13316): Out of memory on a 10485776-byte allocation.
  2. 03-21 21:05:28.779: E/AndroidRuntime(13316): java.lang.OutOfMemoryError

這幾句的意思是,我們程序申請需要10485776byte太大了,虛擬機無法滿足我們,羞愧的shutdown自殺了。這個現象通常出現在用到很多圖片或者很大圖片的APP開發中。通俗講就是當我們的APP需要申請一塊內存來裝圖片的時候,系統覺得我們的APP所使用的內存已經夠多了。即使它有1G的空餘內存,它不同意給我的APP更多的內存裏,然後即使系統馬上拋出OOM錯誤,而程序沒有捕捉該錯誤,故彈框崩潰了。

2.爲什麼會有OOM?

因爲android系統的app的每個進程或者每個虛擬機有個最大內存限制,如果申請的內存資源超過這個限制,系統就會拋出OOM錯誤。跟整個設備的剩餘內存沒太大關係。比如比較早的android系統的一個虛擬機最多16M內存,當一個app啓動後,虛擬機不停的申請內存資源來裝載圖片,當超過內存上限時就出現OOM。Android系統的APP內存限制怎麼確定?

2.1 Android的APP內存組成:

APP內存由 dalvik內存 和 native內存 2部分組成,dalvik也就是java堆,創建的對象就是就是在這裏分配的,而native是通過c/c++方式申請的內存,Bitmap就是以這種方式分配的。(android3.0以後,系統都默認通過dalvik分配的,native作爲堆來管理)。這2部分加起來不能超過android對單個進程,虛擬機的內存限制。

每個手機的內存限制大小是多少?

  1. ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)
  2. activityManager.getMemoryClass();

以上方法會返回以M爲單位的數字,不同的系統平臺或設備上的值都不太一樣,比如HTC默認24M, Galaxy36M, emulator-2.3 24M,等等。我的moto xt681是42M。3

上面取到是虛擬機的最大內存資源。

而對於head堆的大小限制,可以查看/system/build.prop文件。

  1. dalvik.vm.heapstartsize  =  5m
  2. dalvik.vm.heapgrowthlimit = 48m
  3. dalvik.vm.heapsize = 256m

注: heapsize參數表示單個進程heap可用的最大內存,但如果存在以下參數"dalvik.vm.headgrowthlimit =48m"表示單個進程heap內存被限定在48m,即程序運行過程實際只能使用48m內存。

2.2 爲什麼android系統設定APP的內存限制?

1   要使開發者內存使用更爲合理。 限制每個應用的可用內存上限,可以放置某些應用程序惡意或者無意的使用過多的內存。而導致其它應用無法正常運行。Android是有多進程的,如果一個進程(就是一個應用)耗費過多的內存,其他應用就無法運行了。因爲有了限制,使得開發者必須好好利用有限資源,優化資源的使用。

 屏幕顯示內容有限,內存足夠即可。 即使有萬千圖片千萬數據需要使用到,但在特定時刻需要展示給用戶看的總是有限的,因爲屏幕顯示就那麼大,上面可以放的信息就是很有限的。大部分信息都是處於準備顯示狀態,所以沒必要給予太多heap內存。也就是說出現 OOM現象,絕大部分原因是我們的程序設計上有問題,需要優化 。優化方法很多,比如通過時間換空間,不停的加載要用的的圖片,不停的回收不用的圖片,把大圖片解析成適合手機屏幕大小的圖片等。

3   多APP多個虛擬機davlik的限制需要。 android上的app使用獨立虛擬機,每開一個應用就會打開至少一個獨立的虛擬機。這樣可以避免虛擬機崩潰導致整個系統崩潰,同時代價就是需要浪費更多的內存。這樣設計保證了android的穩定性。

2.3 不是GC自動回收資源麼,爲什麼還會OOM?

Android不是用GC會自動回收資源麼,爲什麼app的那些不用的資源不回收呢?

Android的gc會按照特定的算法回收程序不用的內存資源,避免app的內存申請約積越多,但是gc一般回收的資源是那些無主的對象內存或者軟飲用的資源,或者更軟引用的引用資源。比如:

  1. Bitmap bt = BitmapFactory.decodeResource( this .getResources(), R.drawable.splash);  //此時的圖片資源是強引用,是有主的資源。
  2. bt =  null ;  //此時這個圖片資源就是無主的了。gc心情號的時候就會去回收它。
  3. SoftReference<Bitmap> softRef =  new  SoftReference<Bitmap>(bt);
  4. bt =  null ;
  5. 其他代碼...

當程序申請很多內存資源時,gc有可能會釋放softref引用的這個圖片內存。bt=softRef.get(),此時可能得到的是null,需要重新加載圖片。

當然這也說明了用軟引用圖片資源的好處,就是gc會自動根據需要釋放資源,一定程度上避免OOM。

TIPS:編程要養成習慣,不用的對象設置爲null。其實更好的是,不用的圖片直接recycle。因爲通過設置null讓gc來回收,有時候還是會來不及。

2.4 怎麼查看APP內存分配情況?

       1  通過DDMS中的heap選項卡監視內存情況:

Heap視圖中部有一個叫做data object, 即數據對象,也就是我們的程序中大量存在的類類型的對象。

在data object一行中有一列是“Total Size”, 其值就是當前進程中所有Java數據對象的內存總量。如果代碼中存在沒有釋放對象引用的情況,則data object的“Total Size”值在每次gc後不會有美線的回落。隨着操作次數的增加“Total Size”的值會越來越大。直到到達一個上限 後導致進程被kill掉。

2  在App裏面我們可以通過totalMemory與freeMemory:

  1. Runtime.getRuntime().freeMemory()
  2. RUntime.getRuntime().totalMemory()

3  adb shell dumpsys meminfo com.android.demo

3. 常見避免OOM的幾個注意點:

3.1 適當調整圖像大小 。因爲手機屏幕尺寸有限,分配給圖像的顯示區域有限,尤其對於超大圖片,加載自網絡或者sd卡,圖片文件提及達到幾M或者十幾個M的:

加載到內存前,先算出該bitmap的大小,然後通過適當調節採樣率使得加載的圖片剛好,或稍大捷克在手機屏幕上顯示就滿意了:

  1. BimtapFactory.Option opts =  new   BitampFactory.Option();
  2.         opts.inJustDecodeBounds =  true ;
  3.         opts.inSampleSize=computeSample(opts, minSideLength, maxNumOfPixels);  // Android 提供了一種動態計算的方法 computeSampleSize
  4.         opts.inJustDecodeBounds =  false ;
  5.          try {
  6.                 return  BitmapFactory.decodeFile(imageFile, opts);
  7.         }  catch (OutOfMemoryError err){
  8. }

3.2 圖像緩存 。在listview或Gallery等控件中一次性加載大量圖片時,只加載屏幕顯示的資源,尚未顯示的不加載,移出屏幕的資源及時釋放,採用強引用+軟引用2級緩存,提高加載性能。緩存圖像到內存,採用軟引用緩存到內存,而不是在每次使用的時候都從新加載到內存。

3.3 採用低內存佔用量的編碼方式 。比如Bitmap.Config.ARGB_4444比Bitmap.Config.ARGB_8888更省內存。

3.4 及時回收圖像 。如果引用了大量的Bitmap對象,而應用又不需要同時顯示所有圖片。可以將暫時不用到的Bitmap對象及時回收掉。對於一些明確直到圖片使用情況的場景可以主動recycle回收

App的啓動splash畫面上的圖片資源,使用完就recycle。對於幀動畫,可以加載一張,畫一張,釋放一張。

3.5 不要在循環中創建過多的本地變量 。慎用static,用static來修飾成員變量時,該變量就屬於該類,而不是該類實例,它的生命週期是很長的。如果用它來引用一些內存佔用太多的實例,這時候就要謹慎對待了。

3.6 自定義堆內存分配大小 。優化Dalvik虛擬機的堆內存分配。

  1. public   class  ClassName{
  2.                 private   static  Context mContext;
  3.                    // 省略
  4.            }

4. App使用圖片時避免OOM的幾種方式:

4.1  直接null或recycle

對於app裏使用的大量圖片,採用方式:使用時加載,不顯示時直接置null或recycle。

這樣處理是個好習慣,記本可以杜絕OOM,但是缺憾是代碼多了,可能會忘記某些資源recycle。

而有些情況下會出現特定圖片反覆加載,釋放,再加載等,低效率的事情。

  4.2 簡單通過SoftReference引用方式管理圖片資源

建個SoftReference的hashmap

使用圖片時先查詢這個hashmap是否有softreference, softreference裏的圖片是否爲空,

如果爲空就加載圖片到softreference並加入hashmap。

無需再代碼裏顯式的處理圖片的回收與釋放,gc會自動處理資源的釋放。

這種方式處理起來簡單實用,能一定程度上避免前一種方法反覆加載釋放的低效率。但還不夠優化。

  4.3 強引用+軟引用二級緩存

Android示範程序ImageDownloader.java, 使用了一個二級緩存機制。就是有一個數據結構直接持有解碼成功的Bitmap對象引用,同時使用一個二級緩存數據結構保持淘汰的Bitmap的softreference對象,由於softreference對象的特殊性,系統會再需要內存的時候首先將softreference持有的對象釋放掉,也就是說當vm發現可用的內存較少需要出發gc的時候,二級緩存中的bitmap對象將被回收,而持有一級緩存的bitmap對象用於顯示。

其實這個解決方案最爲關鍵的一點是使用了一個比較合適的數據結構,那就是LinkedHashMap類型來進行一級緩存Bitmap的容器。由於LinkeHashMap的特殊性,我們可以控制其內存存儲對象的個數並且將不在使用的對象從容器中移除,放到softreference二級緩存裏,我們可以在一級緩存中一致保存最近被訪問到的bitmap對象,而已經被訪問過的圖片在LinkedHashMap的容量超過我們預設值時將會把容器中存在的時間最長的對象移除,這個時候我麼可以將被移除的LinkedHashMap中的放到二級緩存容器,而二級緩存中的對象管理就交給系統來做了,當系統需要gc時就會首先回收二級緩存容器的Bitmap對象了。

在獲取圖片對象時候先從一級緩存容器中查找,如果有對應對象並可用直接返回,如果沒有的話從二級緩存中查找對應的SoftReference, 判斷SoftReference對象持有的Bitmap是否可用,可用直接返回,否則返回空。如果二級緩存都找不到圖片,那就直接加載圖片資源。

   4, LruCache  + sd的緩存方式

5. 兩種容易OOM的場景建議:

   5.1 網絡下載大量圖片

比如微博客戶端: 多線程異步網絡,小兔直接用LRUCache+SoftRef+Sd,大圖按需下載:

   

    5.2 對於需要加載非常多條目信息的listview,gridview等的情況

在adapter的getView函數裏有個convertView參數,告知你是否有可重用的view對象。 如果不使用convertView的話,每次調用getView時每次都會重新創建view,這樣之前的view可能還沒銷燬,加之不斷的新建view勢必會造成內存劇增,從而導致OOM。另外在重用convertView時,裏面原有的圖片等資源就會變成無主的了。

這裏Google官方推薦使用:“convertview+靜態類viewholder

官方給出解釋是:

a 重用緩存convertView傳遞給getView()方法來避免填充不必要的視圖。

  b 使用ViewHolder模式來避免沒有必要的調用findViewById;因爲太多的findViewById也會影響性能。

 

附ViewHolder類的作用:ViewHolder模式通過在getView方法返回的視圖的標籤(tag)中存儲一個數據結構。這個數據結構包含了指向我們要綁定數據的視圖的引用,從而避免每次調用getView()的時候調用findViewById();

6 申請超過內存限制的內存分配方式:

 6.1 從Native C分配內存。使用NDK(本地開發工具包)和JNI, 它可能從C級(如malloc/free或新建/刪除)分配內存,這樣的分配是不計入24MB的限制。這是真的,從本機代碼分配內存是爲了java方便,但它可以被用來存儲在ram的數據(即使圖片數據)的一些打擊呢。

 6.2 使用OpenGL的紋理。紋理內存不計入限制,要查看你的應用程序確實分配了多少內存可以使用android.os.Debug.getNativeHeapAllocatedSize(), 可以使用上面介紹的兩種技術的Nexus之一,我可以輕鬆地爲一個單一的前臺進程分配300MB-10倍以上的默認24MB 的限制,從上面看來使用native代碼分配內存是不在24MB的限制內的(開放的GL的質地也是使用native代碼分配內存)。

但是,這兩個方法的風險就是,本地堆分配內存超過系統可用內存限制的話,通常都是直接崩潰。

發佈了6 篇原創文章 · 獲贊 2 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章