眾所周知,每個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的。
5.優化Dalvik虛擬機的堆內存分配
對於Android平台來說,其托管層使用的Dalvik JavaVM從目前的表現來看還有很多地方可以優化處理,比如我們在開發一些大型游戲或耗資源的應用中可能考慮手動干涉GC處理,使用 dalvik.system.VMRuntime類提供的setTargetHeapUtilization方法可以增強程序堆內存的處理效率。當然具體原理我們可以參考開源工程,這里我們僅說下使用方法: 代碼如下:
private final static floatTARGET_HEAP_UTILIZATION = 0.75f;
在程序onCreate時就可以調用
VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);
即可
6.自定義我們的應用需要多大的內存
private final static int CWJ_HEAP_SIZE = 6* 1024* 1024 ;
//設置最小heap內存為6MB大小
VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);
小結
本文介紹了軟引用緩存、調整大小、回收等手段來避免OOM,總體來說效果還是明顯的。但實際應用場景中,圖片的應用不想本文所演示的那樣簡單,有時候圖片資源可能來自與網絡,這時需要配合異步加載的方式先下載圖片並通過回調的方法來顯示;有時候圖片資源還需要加邊框、加文字等額外修飾,所以在圖片加載之后還要另做處理。
