Android處理圖片OOM的若干方法小結

yangxi_001發表於2013-11-22

前言

眾所周知,每個Android應用程式在執行時都有一定的記憶體限制,限制大小一般為16MB或24MB(視平臺而定)。因此在開發應用時需要特別關注自身的記憶體使用量,而一般最耗記憶體量的資源,一般是圖片、音訊檔案、視訊檔案等多媒體資源;由於Android系統對音訊、視訊等資源做了邊解析便播放的處理,使用時並不會把整個檔案載入到記憶體中,一般不會出現記憶體溢位(以下簡稱OOM)的錯誤,因此它們的記憶體消耗問題暫不在本文的討論範圍。本文重點討論的是圖片的記憶體消耗問題,如果你要開發的是一款圖片瀏覽器應用,例如像Android系統自帶的Gallery那樣的應用,這個問題將變得尤為突出;如果你開發的是目前的購物客戶端,有時候處理不當也會碰到這種問題。

目前碰到的OOM場景,無外乎以下幾種情形,不過無論是哪種情形,解決問題的思路都是一致的。

(1)顯示單張圖片,圖片檔案體積達到3000*4000級別的時候;

(2)在ListView或Gallery等控制元件中一次性載入大量圖片時;

相關知識介紹

1.顏色模型

常見的顏色模型有RGB、YUV、CMYK等,在大多數影象API中採用的都是RGB模型,Android也是如此;另外,在Android中還有包含透明度Alpha的顏色模型,即ARGB。關於顏色模型更加詳細的資訊暫不在本文的討論範圍之內。

 \\

2.計算機中顏色值的數字化編碼

在不考慮透明度的情況下,一個畫素點的顏色值在計算機中的表示方法有以下3種:

(1)浮點數編碼:比如float: (1.0, 0.5, 0.75),每個顏色分量各佔1個float欄位,其中1.0表示該分量的值為全紅或全綠或全藍;

(2)24位的整數編碼:比如24-bit:(255, 128, 196),每個顏色分量各佔8位,取值範圍0-255,其中255表示該分量的值為全紅或全綠或全藍;

(3)16位的整數編碼:比如16-bit:(31, 45, 31),第1和第3個顏色分量各佔5位,取值範圍0-31,第2個顏色分量佔6位,取值範圍0-63;

在Java中,float型別的變數佔32位,int型別的變數佔32位,short和char型別的變數都在16位,因此可以看出,用浮點數表示法編碼一個畫素的顏色,記憶體佔用量是96位即12位元組;而用24位整數表示法編碼,只要一個int型別變數,佔用4個位元組(高8位空著,低24位用於表示顏色);用16位整數表示法編碼,只要一個short型別變數,佔2個位元組;因此可以看出採用整數表示法編碼顏色值,可以大大節省記憶體,當然,顏色質量也會相對低一些。在Android中獲取Bitmap的時候一般也採用整型編碼。

以上2種整型編碼的表示法中,R、G、B各分量的順序可以是RGB或BGR,Android裡採用的是RGB的順序,本文也都是遵循此順序來討論。在24位整型表示法中,由於R、G、B分量各佔8位,有時候業內也以RGB888來指代這一資訊;類似的,在16位整型表示法中,R、G、B分量分別佔5、6、5位,就以RGB565來指代這一資訊。

現在再考慮有透明度的顏色編碼,其實方式與無透明度的編碼方式一樣:24位整型編碼RGB模型採用int型別變數,其閒置的高8位正好用於放置透明度分量,其中0表示全透明,255表示完全不透明;按照A、R、G、B的順序,就可以以ARGB8888來概括這一情形;而16位整型編碼的RGB模型採用short型別變數,調整各分量所佔為數分別至4位,那麼正好可以空出4位來編碼透明度值;按照A、R、G、B的順序,就可以以ARGB4444來概括這一情形。回想一下Android的BitmapConfig類中,有ARGB_8888、ARGB_4444、RGB565等常量,現在可以知道它們分別代表了什麼含義。同時也可以計算一張圖片在記憶體中可能佔用的大小,比如採用ARGB_8888編碼載入一張1920*1200的圖片,大概就會佔用1920*1200*4/1024/1024=8.79MB的記憶體。

3.Bitmap在記憶體中的儲存區域

http://www.7dot9.com/2010/08/android-bitmap%E5%86%85%E5%AD%98%E9%99%90%E5%88%B6/ 一文中對Android記憶體限制問題做了一些探討,作者認為Bitmap物件通過棧上的引用來指向堆上的Bitmap物件,而Bitmap物件又對應了一個使用了外部儲存的native影象,實際上使用的是byte[]來儲存的記憶體空間。但為了確保外部分配記憶體成功,應該保證當前已分配的記憶體加上當前需要分配的記憶體值,大小不能超過當前堆的最大記憶體值,而且記憶體管理上將外部記憶體完全當成了當前堆的一部分。

4.Java物件的引用型別

(1)強引用(StrongReference)如果一個物件具有強引用,那垃圾回收器絕不會回收它。當記憶體空間不足,Java虛擬機器寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足的問題。

(2)軟引用(SoftReference)如果一個物件只具有軟引用,則記憶體空間足夠,垃圾回收器就不會回收它;如果記憶體空間不足了,就會回收這些物件的記憶體。只要垃圾回收器沒有回收它,該物件就可以被程式使用。

(3)弱引用(WeakReference)弱引用與軟引用的區別在於:只具有弱引用的物件擁有更短暫的生命週期。在垃圾回收器執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。

(4)虛引用(PhantomReference)“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定物件的生命週期。如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。

解決OOM的常用方案

記憶體限制是Android對應用的一個系統級限制,作為應用層開發人員,沒有辦法徹底去消滅這個限制,但是可以通過一些手段去合理使用記憶體,從而規避這個問題。以下是個人總結的一些常用方法:

(1)快取影象到記憶體,採用軟引用快取到記憶體,而不是在每次使用的時候都從新載入到記憶體;

(2)調整影象大小,手機螢幕尺寸有限,分配給影象的顯示區域本身就更小,有時影象大小可以做適當調整;

(3)採用低記憶體佔用量的編碼方式,比如Bitmap.Config.ARGB_4444比Bitmap.Config.ARGB_8888更省記憶體;

(4)及時回收影象,如果引用了大量Bitmap物件,而應用又不需要同時顯示所有圖片,可以將暫時用不到的Bitmap物件及時回收掉;

(5)自定義堆記憶體分配大小,優化Dalvik虛擬機器的堆記憶體分配;

本文主要將對前面4種方式做演示和分析。

演示試驗說明

為了說明出現OOM的場景和解決OOM的方法,本人制作了一個Android應用——OomDemo來演示,此應用的基本情況說明如下:

(1)該應用展示一個gallery,該gallery只載入圖片,gallery的adapter中傳入圖片的路徑而不是圖片物件本身,adapter動態載入圖片;

(2)演示所用的圖片預儲存到sdcard的cache目錄下,檔名分別為a.jpg,b.jpg…r.jpg,總共18張;

(3)圖片為規格1920*1200的jpg圖片,檔案大小在423KB-1.48MB範圍內;

(4)執行環境:模擬器——android2.2版本系統——480*320螢幕尺寸;Moto Defy——2.3.4版本CM7系統——854*480螢幕尺寸;

(5)程式基本結構圖:

 \

演示結果與說明

1.演示一

首先採用最簡單的圖片載入方式,不帶任何圖片快取、調整大小或者回收,SimpleImageLoader.class便是承擔此職責。載入圖片部分的程式碼如下:

@Override

public Bitmap loadBitmapImage(String path) {

       return BitmapFactory.decodeFile(path);

}

@Override

public Drawable loadDrawableImage(String path) {

       return new BitmapDrawable(path);

}

演示結果:在模擬器上圖片只能載入1-3張,之後便會出現OOM錯誤;在Defy上不會出現錯誤;原因是兩者記憶體限制不同,Defy上執行的是第三方ROM,記憶體分配有40MB。另外gallery每次顯示一張圖片時,都要重新解析獲得一張圖片,儘管在Defy上還未曾出錯,但當圖片量加大,GC回收不及時時,還是有可能出現OOM。

2.演示二

為圖片載入的新增一個軟引用快取,每次圖片從快取中獲取圖片物件,若快取中不存在,才會從Sdcard載入圖片,並將該物件加入快取。同時軟引用的物件也有助於GC在記憶體不足的時候回收它們。ImageLoaderWithCache.class負責這個職責,關鍵程式碼如下:

private HashMap<String, SoftReference<Bitmap>> mImageCache;

       @Override

       public Bitmap loadBitmapImage(String path) {

              if(mImageCache.containsKey(path)) {

                     SoftReference<Bitmap> softReference = mImageCache.get(path);

                     Bitmap bitmap = softReference.get();

                     if(null != bitmap)

                            return bitmap;

              }

              Bitmap bitmap = BitmapFactory.decodeFile(path);

              mImageCache.put(path, new SoftReference<Bitmap>(bitmap));

              return bitmap;

       }

       @Override

       public Drawable loadDrawableImage(String path) {

              return new BitmapDrawable(loadBitmapImage(path));

       }

演示結果:在模擬器上,能不無快取時多載入1-2張圖片,但還是會出現OOM;在Defy上不曾出錯。由於本次所用的圖片都相對比較佔記憶體,在GC還未來得及回收軟引用物件時,就又要申請超出剩餘量的記憶體空間,因此仍然沒能完全避免OOM。如果換成載入大量的小圖片,比如100*100規格的,快取中軟引用的作用可能就發揮出來了。(這一假設可以進一步試驗證明一下)

3.演示三

為了進一步避免OOM,除了快取,還可以對圖片進行壓縮,進一步節省記憶體,多數情況下調整圖片大小並不會影響應用的表現力。ImageLoaderWithScale.class便是負責這個職責,調整大小的程式碼如下:

BitmapFactory.Options options = new BitmapFactory.Options();

       options.inJustDecodeBounds = true;

       BitmapFactory.decodeFile(path, options);

       if (options.mCancel || options.outWidth == -1 || options.outHeight == -1) {

              Log.d(“OomDemo”, “alert!!!” + String.valueOf(options.mCancel) + ” ” + options.outWidth + options.outHeight);

              return null;

       }

       options.inSampleSize = Util.computeSampleSize(options, 600, (int) (1 * 1024 * 1024));

       Log.d(“OomDemo”, “inSampleSize: ” + options.inSampleSize);

       options.inJustDecodeBounds = false;

       options.inDither = false;

       options.inPreferredConfig = Bitmap.Config.ARGB_8888;

       Bitmap bitmap = BitmapFactory.decodeFile(path, options);

演示結果:在上述程式碼中,首先解碼圖片的邊界,在不需要得到Bitmap物件的前提下就能獲得影象寬高(寬高值分別被設定到options.outWidth和options.outHeight兩個屬性中)。computeSampleSize這個方法的引數分別為“解析圖片所需的BitmapFactory.Options”、“調整後圖片最小的寬或高值”、“調整後圖片的記憶體佔用量上限”。結合原始圖片的寬高,此方法可以計算得到一個調整比例,再用此比例調整原始圖片並載入到記憶體中,此時圖片所消耗的記憶體不會超出事先指定的大小。在模擬器中,限制圖片所佔記憶體大小為1*1024*1024時,比未壓縮過時能載入更多圖片,但仍然會出現OOM;若限制圖片所佔記憶體大小為0.5*1024*1024,則能完整的載入所有圖片。所以調整圖片大小還是能夠有效節省記憶體的。在Defy中不會出錯,原因同上。

4.演示四

在有些情況下,嚴重縮小圖片還是會影響應用的顯示效果的,所以有必要在儘可能少地縮小圖片的前提下展示圖片,此時手動去回收圖片就變得尤為重要。在類ImageLoaderWithRecyle.class中,便增加了回收圖片資源的方法:

@Override

       public void releaseImage(String path) {

              if(mImageCache.containsKey(path)) {

                     SoftReference<Bitmap> reference = mImageCache.get(path);

                     Bitmap bitmap = reference.get();

                     if(null != bitmap) {

                            Log.d(“OomDemo”, “recyling ” + path);

                            bitmap.recycle();

                     }

                     mImageCache.remove(path);

              }

       }

演示結果:圖片壓縮限制仍然維持在1*1024*1024,在adapter中,及時呼叫releaseImage方法,回收暫時不需要的圖片。此時模擬器中也從未出現過OOM,所以總的來講,綜合快取、調整大小、回收等各種手段,還是能夠有效避免OOM的。

小結

本文介紹了軟引用快取、調整大小、回收等手段來避免OOM,總體來說效果還是明顯的。但實際應用場景中,圖片的應用不想本文所演示的那樣簡單,有時候圖片資源可能來自與網路,這時需要配合非同步載入的方式先下載圖片並通過回撥的方法來顯示;有時候圖片資源還需要加邊框、加文字等額外修飾,所以在圖片載入之後還要另做處理。

另外由於本人能力所限以及時間關係,本文還有諸多不完善之處。比如對Android記憶體分配的理解不深,沒能透徹地解釋Bitmap的記憶體佔用情況;通過自定義堆記憶體分配大小,優化Dalvik虛擬機器的堆記憶體分配的方法來解決OOM,本文也沒有給予演示;再比如在上文的演示試驗裡,沒有把記憶體佔用情況的詳細資訊用影象形式直觀地展示出來;還有演示所用的圖片數量過少、規格單一、測試環境偏少,所有沒能進行更加嚴謹科學的對比試驗,遺漏了某些意外情況。最後歡迎大家來共同探索、交流並提出建議。

相關文章