Bitmap是引發OOM的罪魁禍首之一,當咱們從網絡上下載圖片的時候沒法知道網絡圖片的準確大小,因此爲了節約內存,通常會在服務器上緩存一個縮略圖,提高下載速度。除此以外,咱們還能夠在本地顯示圖片前將圖片進行壓縮,使其徹底符合imageview的大小,這樣就不會浪費內存了。html
1、思路java
思路:計算出要顯示bitmap的imageview大小,根據imageview的大小壓縮bitmap,最終讓bitmap和imageview同樣大。android
2、得到ImageView的寬高web
經過這兩個方法我就能獲得imageview實際的大小了,單位是pix。這個方法請在imageview加載完畢後再調用,不然一致返回空。若是你不知道何時會加載完畢,你能夠將其放入view.post方法中。算法
view.post(new Runnable() { @Override public void run() { // TODO 自動生成的方法存根 } })
3、經過BitmapFactory獲得壓縮後的bitmap數組
3.1 BitmapFactory.Options緩存
BitmapFactory這個類提供了多個解析方法(decodeByteArray, decodeFile, decodeResource等)用於建立Bitmap對象,咱們應該根據圖片的來源選擇合適的方法。安全
這些方法都會爲一個bitmap分配內存,若是你的圖片太大就很容易形成bitmap。爲此,這裏面均可以傳入一個BitmapFactory.Options對象,用來進行配置。服務器
BitmapFactory.Options options = new BitmapFactory.Options();
BitmapFactory.Options中有個inJustDecodeBounds屬性,這個屬性爲true時,調用上面三個方法返回的就不是一個完整的bitmap對象,而是null。這是由於它禁止這些方法爲bitmap分配內存。那麼它有什麼用呢?設置inJustDecodeBounds=true後,BitmapFactory.Options的outWidth、outHeight和outMimeType屬性都會被賦值。這個技巧讓咱們能夠在加載圖片以前就獲取到圖片的長寬值和MIME類型,從而根據狀況對圖片進行壓縮。就等於不讀取圖片,但得到圖片的各類參數,大大節約內存。網絡
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;
3.2 初步計算壓縮比率
BitmapFactory.Options中有個inSampleSize屬性,能夠理解爲壓縮比率。設定好壓縮比率後,調用上面的decodexxxx()就能獲得一個縮略圖了。好比inSampleSize=4,載入的縮略圖是原圖大小的1/4。
爲了不OOM異常,最好在解析每張圖片的時候都先檢查一下圖片的大小,如下幾個因素是咱們須要考慮的:
預估一下加載整張圖片所需佔用的內存
爲了加載這一張圖片你所願意提供多少內存
用於展現這張圖片的控件的實際大小
當前設備的屏幕尺寸和分辨率
好比,你的ImageView只有128*96像素的大小,只是爲了顯示一張縮略圖,這時候把一張1024*768像素的圖片徹底加載到內存中顯然是不值得的。好比咱們有一張2048*1536像素的圖片,將inSampleSize的值設置爲4,就能夠把這張圖片壓縮成512*384像素。本來加載這張圖片須要佔用13M的內存,壓縮後就只須要佔用0.75M了(假設圖片是ARGB_8888類型,即每一個像素點佔用4個字節)。
同理,假設原圖是1500x700的,咱們給縮略圖留出的空間是100x100的。那麼inSampleSize=min(1500/100, 700/100)=7。咱們能夠獲得的縮略圖是原圖的1/7。
下面的代碼能夠用來計算這個inSampleSize的值:
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // 源圖片的高度和寬度 final int height = options.outHeight; final 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的註釋中有一個須要注意的一點。
If set to a value > 1, requests the decoder to subsample the original image, returning a smaller image to save memory. The sample size is the number of pixels in either dimension that correspond to a single pixel in the decoded bitmap. For example, inSampleSize == 4 returns an image that is 1/4 the width/height of the original, and 1/16 the number of pixels. Any value <= 1 is treated the same as 1.
Note: the decoder uses a final value based on powers of 2, any other value will be rounded down to the nearest power of 2.
即便設置了inSampleSize=7,可是獲得的縮略圖倒是原圖的1/4,緣由是inSampleSize只能是2的整數次冪,若是不是的話,向下取得最大的2的整數次冪,7向下尋找2的整數次冪,就是4。這樣設計的緣由極可能是爲了漸變bitmap壓縮,畢竟按照2的次方進行壓縮會比較高效和方便。
3.3 再次計算壓縮比率
經過上面的分析咱們知道,bitmap是能夠被壓縮的,咱們能夠根據須要來壓縮bitmap,但壓縮獲得的bitmap可能會比我須要的大。所以,還得改進方法!而後咱們發現了Bitmap中的這個方法:
Bitmap android.graphics.Bitmap.createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)
createScaledBitmap()能夠給咱們一個按照要求拉伸/縮小後的bitmap,咱們能夠經過這個方法把咱們以前獲得的較大的縮略圖進行縮小,讓其徹底符合實際顯示的大小。好了,如今咱們開始寫代碼:
/** * @description 計算圖片的壓縮比率 * * @param options 參數 * @param reqWidth 目標的寬度 * @param reqHeight 目標的高度 * @return */ private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // 源圖片的高度和寬度 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; while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { inSampleSize *= 2; } } return inSampleSize; }
/** * @description 經過傳入的bitmap,進行壓縮,獲得符合標準的bitmap * * @param src * @param dstWidth * @param dstHeight * @return */ private static Bitmap createScaleBitmap(Bitmap src, int dstWidth, int dstHeight, int inSampleSize) {
// 若是是放大圖片,filter決定是否平滑,若是是縮小圖片,filter無影響,咱們這裏是縮小圖片,因此直接設置爲false Bitmap dst = Bitmap.createScaledBitmap(src, dstWidth, dstHeight, false); if (src != dst) { // 若是沒有縮放,那麼不回收 src.recycle(); // 釋放Bitmap的native像素數組 } return dst; }
如今咱們就完成了以下的四個過程。
1. 使用inJustDecodeBounds,僅僅讀bitmap的長和寬。
2. 根據bitmap的長款和目標縮略圖的長和寬,計算出inSampleSize的大小。
3. 使用inSampleSize,載入一個比imageview大一點的縮略圖A
4. 使用createScaseBitmap再次壓縮A,將縮略圖A生成咱們須要的縮略圖B。
5. 回收縮略圖A(若是A和B的比率同樣,就不回收A)。
有朋友問,爲啥要先從inSampleSize產生一個縮略圖A,而不是直接把原始的bitmap經過createScaseBitmap()縮放爲目標圖片呢?
由於若是要從原始的bitmap直接進行縮放的話,就須要將原始圖片放入內存中,十分危險!!!如今經過計算獲得一個縮略圖A,這個縮略圖A比原圖能夠小了不少,徹底能夠直接加載到內存中,這樣再進行拉伸就比較安全了。
4、產生工具類
如今已經把全部的技術難點攻克了,徹底能夠寫一個工具類來產生縮略圖。工具類中應該包含生成縮略圖的重要方法:
/** * @description 從Resources中加載圖片 * * @param res * @param resId * @param reqWidth * @param reqHeight * @return */ public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; // 設置成了true,不佔用內存,只獲取bitmap寬高 BitmapFactory.decodeResource(res, resId, options); // 第一次解碼,目的是:讀取圖片長寬 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 調用上面定義的方法計算inSampleSize值 // 使用獲取到的inSampleSize值再次解析圖片 options.inJustDecodeBounds = false; Bitmap src = BitmapFactory.decodeResource(res, resId, options); // 產生一個稍大的縮略圖 return createScaleBitmap(src, reqWidth, reqHeight, options.inSampleSize); // 經過獲得的bitmap進一步產生目標大小的縮略圖 } /** * @description 從SD卡上加載圖片 * * @param pathName * @param reqWidth * @param reqHeight * @return */ public static Bitmap decodeSampledBitmapFromFile(String pathName, int reqWidth, int reqHeight) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(pathName, options); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); options.inJustDecodeBounds = false; Bitmap src = BitmapFactory.decodeFile(pathName, options); return createScaleBitmap(src, reqWidth, reqHeight, options.inSampleSize); }
這兩個方法的過程徹底一致,產生一個option對象,設置inJustDecodeBounds參數,開始第一次解析bitmap,得到bitmap的寬高數據,而後經過寬高數據計算縮略圖的比率。獲得縮略圖比率後,咱們把inJustDecodeBounds設置爲false,開始正式解析bitmap,最終獲得的是一個可能比想要的縮略圖略大的bitmap。最後一部是檢查bitmap是不是咱們想要的bitmap,若是是就返回,不是的話就開始拉伸,總之最終會返回一個徹底符合imageview大小的bitmap,不浪費一點點內存。
完整的工具類代碼以下:
package com.kale.bitmaptest; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; /** * @author:Jack Tony * @description : * @web : * http://developer.android.com/training/displaying-bitmaps/load-bitmap.html * http://www.cnblogs.com/kobe8/p/3877125.html * * @date :2015年1月27日 */ public class BitmapUtils { /** * @description 計算圖片的壓縮比率 * * @param options 參數 * @param reqWidth 目標的寬度 * @param reqHeight 目標的高度 * @return */ private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // 源圖片的高度和寬度 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; } /** * @description 經過傳入的bitmap,進行壓縮,獲得符合標準的bitmap * * @param src * @param dstWidth * @param dstHeight * @return */ private static Bitmap createScaleBitmap(Bitmap src, int dstWidth, int dstHeight, int inSampleSize) {
// 若是是放大圖片,filter決定是否平滑,若是是縮小圖片,filter無影響,咱們這裏是縮小圖片,因此直接設置爲false Bitmap dst = Bitmap.createScaledBitmap(src, dstWidth, dstHeight, false); if (src != dst) { // 若是沒有縮放,那麼不回收 src.recycle(); // 釋放Bitmap的native像素數組 } return dst; } /** * @description 從Resources中加載圖片 * * @param res * @param resId * @param reqWidth * @param reqHeight * @return */ public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; // 設置成了true,不佔用內存,只獲取bitmap寬高 BitmapFactory.decodeResource(res, resId, options); // 讀取圖片長寬,目的是獲得圖片的寬高 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 調用上面定義的方法計算inSampleSize值 // 使用獲取到的inSampleSize值再次解析圖片 options.inJustDecodeBounds = false; Bitmap src = BitmapFactory.decodeResource(res, resId, options); // 載入一個稍大的縮略圖 return createScaleBitmap(src, reqWidth, reqHeight, options.inSampleSize); // 經過獲得的bitmap,進一步獲得目標大小的縮略圖 } /** * @description 從SD卡上加載圖片 * * @param pathName * @param reqWidth * @param reqHeight * @return */ public static Bitmap decodeSampledBitmapFromFile(String pathName, int reqWidth, int reqHeight) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(pathName, options); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); options.inJustDecodeBounds = false; Bitmap src = BitmapFactory.decodeFile(pathName, options); return createScaleBitmap(src, reqWidth, reqHeight, options.inSampleSize); } }
5、測試
我在佈局中創建一個100x100dp的imageview,而後在res中放一張圖片。
5.1 佈局文件
放入兩個按鈕,一個按鈕啓動的是用常規的方法加載bitmap,不壓縮;另外一個啓動壓縮算法。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" tools:context="${relativePackage}.${activityClass}" > <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Bitmap Test" /> <ImageView android:id="@+id/imageView" android:layout_width="100dp" android:layout_height="100dp" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:scaleType="fitXY" android:src="@drawable/ic_launcher" /> <Button android:id="@+id/original_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:onClick="butonListener" android:text="加載原圖" /> <Button android:id="@+id/clip_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBaseline="@+id/original_button" android:layout_alignBottom="@+id/original_button" android:layout_alignParentRight="true" android:onClick="butonListener" android:text="加載縮略圖" /> </RelativeLayout>
5.2 java代碼
public void butonListener(View v) { switch (v.getId()) { case R.id.original_button: loadBitmap(false); // 加載原圖 break; case R.id.clip_button: loadBitmap(true); // 加載縮略圖 break; } } public void loadBitmap(boolean exactable) { int bmSize = 0; Bitmap bm = null; if (exactable) { // 經過工具類來產生一個符合ImageView的縮略圖,由於ImageView的大小是50x50,因此這裏獲得的縮略圖也應該是同樣大小的 bm = BitmapUtils.decodeSampledBitmapFromResource(getResources(), R.drawable.saber, iv.getWidth(), iv.getHeight()); } else { // 直接加載原圖 bm = BitmapFactory.decodeResource(getResources(), R.drawable.saber); } iv.setImageBitmap(bm); bmSize += bm.getByteCount(); // 獲得bitmap的大小 int kb = bmSize / 1024; int mb = kb / 1024; Toast.makeText(this, "bitmap size = " + mb + "MB" + kb + "KB", Toast.LENGTH_LONG).show(); }
根據點擊不一樣的按鈕,觸發不一樣的方法,最終把獲得的bitmap放入imageview,而且顯示當前的bitmap大小。運行後能夠發現,通過壓縮算法獲得的bitmap要小不少,更加節約內存。
結果:原圖:1M+;縮略圖:156kb。
注意:當你的imageview遠遠小於bitmap原圖大小的時候這種壓縮算法十分有效,可是若是的bitmap和imageview大小差很少,你會發現這個算法的做用就不那麼明顯了,並且不要認爲用了壓縮就永遠不會出現OOM了。
PS:實際使用中咱們的bitmap常常是大於imageview的,因此推薦採用此方法。
源碼下載:http://download.csdn.net/detail/shark0017/8402227
參考自:
http://www.cnblogs.com/kobe8/p/3877125.html
https://developer.android.com/training/displaying-bitmaps/load-bitmap.html
http://stormzhang.com/android/2013/11/20/android-display-bitmaps-efficiently/