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

前言

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

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

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

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

相關知識介紹

1.顏色模型

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

 vcm1hdA==” width=”150″ />

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,本文也沒有給予演示;再比如在上文的演示試驗裡,沒有把內存占用情況的詳細信息用圖像形式直觀地展示出來;還有演示所用的圖片數量過少、規格單一、測試環境偏少,所有沒能進行更加嚴謹科學的對比試驗,遺漏瞭某些意外情況。最後歡迎大傢來共同探索、交流並提出建議。

 作者:awp258
 

發佈留言