標籤:
原文地址:http://android.xsoftlab.net/training/displaying-bitmaps/index.html
引言
學習如何使用一種常規的手段來處理及載入Bitmap對象,這種方式除了使使用者介面是可響應的之外,還會避免超出記憶體的限制。如果你不小心點的話,位元影像會迅速的將那些可憐的記憶體消耗殆盡,並會導致程式崩潰,因為這會產生一種可怕的異常:
java.lang.OutofMemoryError: bitmap size exceeds VM budget.
這裡列舉出了一些原因來說明為什麼載入位元影像對於Android程式來說是非常棘手的:
- 行動裝置通常含有有限的資源。Android裝置對於單個程式只有少量的16MB可用記憶體。虛擬機器相容性(Virtual Machine Compatibility)針對於各種的螢幕尺寸和密度給出了最低限度的程式記憶體要求。程式應該在極小的記憶體空間下充分利用記憶體空間。無論如何要記住一點,很多裝置配備了更高的限制。
- 位元影像通常會消耗掉不少記憶體,尤其是豐富的圖片,就像照片這樣的。舉個例子,Galaxy Nexus上的相機拍的照片會達到2592x1936個像素(五百萬像素)。如果位元影像配置使用的是ARGB_8888(這在Android 2.3以前是預設的),那麼載入這張照片到記憶體中就需要花費掉19MB的記憶體(2592*1936*4個位元組),這會立即耗盡某些裝置上的所有記憶體。
- Android的APP介面有時會很頻繁的請求一些圖片來載入。有些組件比如ListView, GridView及ViewPager,它們有個共同的特性就是需要同時在螢幕上載入多個位元影像並會在螢幕之外的地方載入以便在手指滑動的時候顯示出來。
有效載入大圖
圖片會有各種形狀和大小。在很多情況下它們會比使用者介面上所要求的尺寸要大。舉個例子,系統的相簿應用所展示的用相機拍攝的照片的解析度通常要比螢幕的密度要高。
鑒於在有限的記憶體中工作,理想上只用載入低解析度的版本就可以。低解析度的版本應該匹配到展示這張圖片的控制項大小。圖片的更高解析度不會在視覺上有更佳的效果,但是這仍然會消耗寶貴的記憶體空間,由於額外的動態擴充,這會招致額外的效能開銷。
這節課會討論將大位元影像進行二次採樣並將採樣後的小版本載入到記憶體中的過程。這個過程並不會超出應用的記憶體限制。
讀取位元影像的尺寸及類型
類BitmapFactory提供了若干個解碼方法(decodeByteArray(), decodeFile(), decodeResource(), etc.)根據不同的資源來建立位元影像Bitmap。選擇更加適合的解碼方法取決於圖片的資料資源。這些方法會在構造位元影像時嘗試向記憶體申請空間,所以會輕易的造成OutOfMemory異常。每個解碼方法都有一個附屬特徵,這個特徵可以使你通過BitmapFactory.Options類來指定解碼選項。設定inJustDecodeBounds屬性為true可以避免在解碼時向記憶體申請空間,這會返回一個空的位元影像,但是outWidth、outHeight和outMimeType這些設定除外。這項技術可以使你在構造位元影像(申請記憶體)之前提前讀取映像資料的尺寸及類型。
BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;BitmapFactory.decodeResource(getResources(), R.id.myimage, options);int imageHeight = options.outHeight;int imageWidth = options.outWidth;String imageType = options.outMimeType;
為了避免java.lang.OutOfMemory異常,需要在解碼圖片之前檢查圖片的尺寸,除非你對這些映像資料的尺寸絕對的信任,並且該尺寸對可用記憶體非常適用。
載入等比縮小的版本到記憶體
那麼現在圖片的尺寸是知道了,這尺寸可以被用來決定:是否全尺寸的映像應該被載入到記憶體中還是應該有個二次採樣的版本載入到記憶體中。這裡有一些因素需要考慮:
- 往記憶體中載入全尺寸的映像應該估算要使用的記憶體大小。
- 要載入的圖片所需要的記憶體數量需要給應用預留一定的記憶體空間,不要消耗完全。
- ImageView或者UI組件的尺寸是映像將要載入的尺寸。
- 當前裝置的螢幕尺寸與密度。
舉個例子,載入一個1024*768像素的圖片到記憶體中是沒有價值的,如果這個圖片最終被顯示為一個128x96像素的縮圖的話。
為了告訴解碼器需要進行二次採樣,以便載入一個小版本的映像到記憶體中,需要設定BitmapFactory.Options對象的inSampleSize屬性為true。舉個例子,一張圖片的解析度為2048x1536,需要通過inSampleSize解碼為4分之一大小的位元影像,大概是512x384。載入這樣的映像只需要花費0.75MB記憶體,而全尺寸的映像則需要花費12MB的記憶體(假設位元影像的配置為ARGB_8888)。這裡有一個方法可以來計算一個樣本容量值,這個值是2的冪次方值並基於原映像的高度值與寬度值進行計算。
public static int calculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; // Calculate the largest inSampleSize value that is a power of 2 and keeps both // height and width larger than the requested height and width. while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { inSampleSize *= 2; } } return inSampleSize;}
Note: 最終計算後的值是一個2的冪次方值是因為解碼器需要通過舍入來獲得一個最終值,這個值與2的冪次方最為接近,依據inSampleSize文檔。
為了使用這個方法,第一步需要將inJustDecodeBounds設定為true,然後將options交給BitmapFactory使用,然後再次使用一個新的inSampleSize和inJustDecodeBounds設定為false來再次使用:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options);}
這個方法可以很輕易的載入任何大尺寸的位元影像給ImageView,這個ImageView展示了一個100*100像素的縮圖,就像下面的代碼所展示的這樣:
mImageView.setImageBitmap( decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
你可以遵循類似的過程來對其它資源進行解碼,如果需要的話,可以替代使用合適的BitmapFactory.decode*方法。
Android官方開發文檔Training系列課程中文版:高效顯示位元影像之載入大位元影像