原文首發於微信公衆號:jzman-blog,歡迎關注交流!
Android 開發中常常考慮的一個問題就是 OOM(Out Of Memory),也就是內存溢出,一方面大量加載圖片時有可能出現 OOM, 經過採樣壓縮圖片可避免 OOM,另外一方面,如一張 1024 x 768 像素的圖像被縮略顯示在 128 x 96 的 ImageView 中,這種作法顯然是不值得的,可經過採樣加載一個合適的縮小版本到內存中,以減少內存的消耗,Bitmap 的優化主要有兩個方面以下:java
這篇文章主要側重於如何有效的處理較大的位圖。android
此外,在 Android 中按照位圖採樣的方法加載一個縮小版本到內存中應該考慮因素?canvas
圖像有不一樣的形狀的和大小,讀取較大的圖片時會耗費內存。讀取一個位圖的尺寸和類型,爲了從多種資源建立一個位圖,BitmapFactory 類提供了許多解碼的方法,根據圖像數據資源選擇最合適的解碼方法,這些方法試圖請求分配內存來構造位圖,所以很容易致使 OOM 異常。每種類型的解碼方法都有額外的特徵可讓你經過 BitMapFactory.Options 類指定解碼選項。當解碼時設置 inJustDecodeBounds 爲true,可在不分配內存以前讀取圖像的尺寸和類型,下面的代碼實現了簡單的位圖採樣:緩存
/** * 位圖採樣 * @param res * @param resId * @return */ public Bitmap decodeSampleFromResource(Resources res, int resId){ //BitmapFactory建立設置選項 BitmapFactory.Options options = new BitmapFactory.Options(); //設置採樣比例 options.inSampleSize = 200; Bitmap bitmap = BitmapFactory.decodeResource(res,resId,options); return bitmap; }
注意:其餘 decode... 方法與 decodeResource 相似,這裏都以 decodeRedource 爲例。微信
實際使用時,必須根據具體的寬高要求計算合適的 inSampleSize 來進行位圖的採樣,好比,將一個分辨率爲 2048 x 1536 的圖像使用 inSampleSize 值爲 4 去編碼產生一個 512 x 384 的圖像,這裏假設位圖配置爲 ARGB_8888,加載到內存中僅僅是 0.75M 而不是原來的 12M,關於圖像所佔內存的計算將在下文中介紹,下面是根據所需寬高進行計算採樣比例的計算方法:學習
/** * 1.計算位圖採樣比例 * * @param option * @param reqWidth * @param reqHeight * @return */ public int calculateSampleSize(BitmapFactory.Options option, int reqWidth, int reqHeight) { //得到圖片的原寬高 int width = option.outWidth; int height = option.outHeight; int inSampleSize = 1; if (width > reqWidth || height > reqHeight) { if (width > height) { inSampleSize = Math.round((float) height / (float) reqHeight); } else { inSampleSize = Math.round((float) width / (float) reqWidth); } } return inSampleSize; } /** * 2.計算位圖採樣比例 * @param options * @param reqWidth * @param reqHeight * @return */ public int calculateSampleSize1(BitmapFactory.Options options, int reqWidth, int reqHeight) { //得到圖片的原寬高 int height = options.outHeight; int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { // 計算出實際寬高和目標寬高的比率 final int heightRatio = Math.round((float) height / (float) reqHeight); final int widthRatio = Math.round((float) width / (float) reqWidth); /** * 選擇寬和高中最小的比率做爲inSampleSize的值,這樣能夠保證最終圖片的寬和高 * 必定都會大於等於目標的寬和高。 */ inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; } return inSampleSize; }
得到採樣比例以後就能夠根據所需寬高處理較大的圖片了,下面是根據所需寬高計算出來的 inSampleSize 對較大位圖進行採樣:測試
/** * 位圖採樣 * @param resources * @param resId * @param reqWidth * @param reqHeight * @return */ public Bitmap decodeSampleFromBitmap(Resources resources, int resId, int reqWidth, int reqHeight) { //建立一個位圖工廠的設置選項 BitmapFactory.Options options = new BitmapFactory.Options(); //設置該屬性爲true,解碼時只能獲取width、height、mimeType options.inJustDecodeBounds = true; //解碼 BitmapFactory.decodeResource(resources, resId, options); //計算採樣比例 int inSampleSize = options.inSampleSize = calculateSampleSize(options, reqWidth, reqHeight); //設置該屬性爲false,實現真正解碼 options.inJustDecodeBounds = false; //解碼 Bitmap bitmap = BitmapFactory.decodeResource(resources, resId, options); return bitmap; }
在解碼過程當中使用了 BitmapFactory.decodeResource() 方法,具體以下:優化
/** * 解碼指定id的資源文件 */ public static Bitmap decodeResource(Resources res, int id, BitmapFactory.Options opts) { ... /** * 根據指定的id打開數據流讀取資源,同時爲TypeValue進行復制獲取原始資源的density等信息 * 若是圖片在drawable-xxhdpi,那麼density爲480dpi */ is = res.openRawResource(id, value); //從輸入流解碼出一個Bitmap對象,以便根據opts縮放相應的位圖 bm = decodeResourceStream(res, value, is, null, opts); ... }
顯然真正解碼的方法應該是 decodeResourceStream() 方法,具體以下:編碼
/** * 從輸入流中解碼出一個Bitmap,並對該Bitmap進行相應的縮放 */ public static Bitmap decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, BitmapFactory.Options opts) { if (opts == null) { //建立一個默認的Option對象 opts = new BitmapFactory.Options(); } /** * 若是設置了inDensity的值,則按照設置的inDensity來計算 * 不然將資源文件夾所表示的density設置inDensity */ if (opts.inDensity == 0 && value != null) { final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density != TypedValue.DENSITY_NONE) { opts.inDensity = density; } } /** * 同理,也能夠經過BitmapFactory.Option對象設置inTargetDensity * inTargetDensity 表示densityDpi,也就是手機的density * 使用DisplayMetrics對象.densityDpi得到 */ if (opts.inTargetDensity == 0 && res != null) { opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } //decodeStream()方法中調用了native方法 return decodeStream(is, pad, opts); }
設置完 inDensity 和 inTargetDensity 以後調用了 decodeStream() 方法,該方法返回徹底解碼後的 Bitmap 對象,具體以下:spa
/** * 返回解碼後的Bitmap, */ public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) { ... bm = nativeDecodeAsset(asset, outPadding, opts); //調用了native方法:nativeDecodeStream(is, tempStorage, outPadding, opts); bm = decodeStreamInternal(is, outPadding, opts); Set the newly decoded bitmap's density based on the Options //根據Options設置最新解碼的Bitmap setDensityFromOptions(bm, opts); ... return bm; }
顯然,decodeStream() 方法主要調用了本地方法完成 Bitmap 的解碼,跟蹤源碼發現 nativeDecodeAsset() 和 nativeDecodeStream() 方法都調用了 dodecode() 方法,doDecode 方法關鍵代碼以下:
/** * BitmapFactory.cpp 源碼 */ static jobject doDecode(JNIEnv*env, SkStreamRewindable*stream, jobject padding, jobject options) { ... if (env -> GetBooleanField(options, gOptions_scaledFieldID)) { const int density = env -> GetIntField(options, gOptions_densityFieldID); const int targetDensity = env -> GetIntField(options, gOptions_targetDensityFieldID); const int screenDensity = env -> GetIntField(options, gOptions_screenDensityFieldID); if (density != 0 && targetDensity != 0 && density != screenDensity) { //計算縮放比例 scale = (float) targetDensity / density; } } ... //原始Bitmap SkBitmap decodingBitmap; ... //原始位圖的寬高 int scaledWidth = decodingBitmap.width(); int scaledHeight = decodingBitmap.height(); //綜合density和targetDensity計算最終寬高 if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) { scaledWidth = int(scaledWidth * scale + 0.5f); scaledHeight = int(scaledHeight * scale + 0.5f); } ... //x、y方向上的縮放比例,大概與scale相等 const float sx = scaledWidth / float(decodingBitmap.width()); const float sy = scaledHeight / float(decodingBitmap.height()); ... //將canvas放大scale,而後繪製Bitmap SkCanvas canvas (outputBitmap); canvas.scale(sx, sy); canvas.drawARGB(0x00, 0x00, 0x00, 0x00); canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, & paint); }
上面代碼能看到縮放比例的計算,以及 density 與 targetDensity 對 Bitmap 寬高的影響,實際上間接影響了 Bitmap 在所佔內存的大小,這個問題會在下文中舉例說明,注意 density 與當前 Bitmap 所對應資源文件(圖片)的目錄有關,若有一張圖片位於 drawable-xxhdpi 目錄中,其對應的 Bitmap 的 density 爲 480dpi,而 targetDensity 就是 DisPlayMetric 的 densityDpi,也就是手機屏幕表明的 density。那麼怎麼查看 Android 中本地的 native 方法的實現呢,連接以下:
BitmapFactory.cpp,直接搜索 native 方法的方法名便可,能夠試一下咯。
首先貢獻一張大圖 6000 x 4000 ,圖片接近 12M,【可在公衆號零點小築索要】 當直接加載這張圖片到內存中確定會發生 OOM,固然經過適當的位圖採樣縮小圖片可避免 OOM,那麼 Bitmap 所佔內存又如何計算呢,通常狀況下這樣計算:
Bitmap Memory = widthPix * heightPix * 4
可以使用 bitmap.getConfig() 獲取 Bitmap 的格式,這裏是 ARGB_8888 ,這種 Bitmap 格式下一個像素點佔 4 個字節,因此要 x 4,若是將圖片放置在 Android 的資源文件夾中,計算方式以下:
scale = targetDensity / density widthPix = originalWidth * scale heightPix = orignalHeight * scale Bitmap Memory = widthPix * scale * heightPix * scale * 4
上述簡單總結了一下 Bitmap 所佔內存的計算方式,驗證時可以使用以下方法獲取 Bitmap 所佔內存大小:
BitmapMemory = bitmap.getByteCount()
因爲選擇的這張圖片直接加載會致使 OOM,因此下文的事例中都是先採樣壓縮,而後在進行 Bitmap 所佔內存的計算。
這種方式就是直接指定採樣比例 inSampleSize 的值,而後先採樣而後計算採樣後的內存,這裏指定 inSampleSize 爲200。
inSampleSize = 200 scale = targetDensity / density} = 480 / 480 = 1 widthPix = orignalScale * scale = 6000 / 200 * 1 = 30 heightPix = orignalHeight * scale = 4000 / 200 * 1 = 20 Bitmap Memory = widthPix * heightPix * 4 = 30 * 20 * 4 = 2400(Byte)
inSampleSize = 200 scale = targetDensity / density = 480 / 320 widthPix = orignalWidth * scale = 6000 / 200 * scale = 45 heightPix = orignalHeight * scale = 4000 / 200 * 480 / 320 = 30 Bitmap Memory = widthPix * scale * heightPix * scale * 4 = 45 * 30 * 4 = 5400(Byte)
這種方式就是根據請求的寬高計算合適的 inSampleSize,而不是隨意指定 inSampleSize,實際開發中這種方式最經常使用,這裏請求寬高爲100x100,具體 inSampleSize 計算在上文中已經說明。
inSampleSize = 4000 / 100 = 40 scale = targetDensity / density = 480 / 480 = 1 widthPix = orignalWidth * scale = 6000 / 40 * 1 = 150 heightPix = orignalHeight * scale = 4000 / 40 * 1 = 100 BitmapMemory = widthPix * scale * heightPix * scale * 4 = 60000(Byte)
inSampleSize = 4000 / 100 = 40 scale = targetDensity / density = 480 / 320 widthPix = orignalWidth * scale = 6000 / 40 * scale = 225 heightPix = orignalHeight * scale = 4000 / 40 * scale = 150 BitmapMemory = widthPix * heightPix * 4 = 225 * 150 * 4 = 135000(Byte)
位圖採樣及 Bitmap 在不一樣狀況下所佔內存的計算大概過程如上所述。
測試效果圖參考以下:
drawable-xhdpi | drawable-xxhdpi |
---|---|
![]() |
![]() |
若是感興趣,能夠關注公衆號:jzman-blog,一塊兒交流學習。