Android中的Bitmap

夏碧筆發表於2019-03-04

Bitmap點陣圖簡介

點陣圖檔案(Bitmap),副檔名可以是.bmp或者.dib。點陣圖是Windows標準格式圖形檔案,它將影像定義為由點(畫素)組成,每個點可以由多種色彩表示,包括2、4、8、16、24和32位色彩。點陣圖檔案是非壓縮格式的,需要佔用較大儲存空間。

例如,一幅1920X1080解析度的32點陣圖片,其所佔儲存位元組數為:1920×1080×32/(8*1024)=8100KB=7.91M

在安卓系統中bitmap圖片一般是以ARGB_8888(ARGB分別代表的是透明度,紅色,綠色,藍色,每個值分別用8bit來記錄,也就是一個畫素會佔用4byte,共32bit。)來進行儲存的。

Android中圖片有四種顏色格式

顏色格式 每個畫素佔用記憶體(單位byte) 每個畫素佔用記憶體(單位bit)
ALPHA_8 1 8
ARGB_8888(預設) 4 32
ARGB_4444 2 16
RGB_565 2 16

ARGB_8888佔位計算: 8+8+8+8 =32

1 bit 0/1 ;最小單位

1 byte = 8bit 10101010 (0-255)00000000 — 11111111

說明:

ARGB_8888:ARGB分別代表的是透明度,紅色,綠色,藍色,每個值分別用8bit來記錄,也就是一個畫素會佔用4byte,共32bit.

ARGB_4444:ARGB的是每個值分別用4bit來記錄,一個畫素會佔用2byte,共16bit.

RGB_565:R=5bit,G=6bit,B=5bit,不存在透明度,每個畫素會佔用2byte,共16bit.

ALPHA_8:該畫素只儲存透明度,會佔用1byte,共8bit.

在實際應用中而言,建議使用ARGB_8888以及RGB_565。
如果你不需要透明度,選擇RGB_565,可以減少一半的記憶體佔用.

Bitmap的回收

在Android3.0以前Bitmap是存放在記憶體中的,我們需要回收native層和Java層的記憶體

在Android3.0以後Bitmap是存放在堆中的,我們只要回收堆記憶體即可

官方建議我們3.0以後使用recycle()方法進行回收,該方法可以不主動呼叫,因為垃圾回收器會自動收集不可用的Bitmap物件進行回收

recycle()官方說明:

釋放與此點陣圖關聯的本地物件,並清除對畫素資料的引用。這不會同步釋放畫素資料;它只是允許它被垃圾收集,如果沒有其他的參考。點陣圖被標記為“dead”,意味著如果getPixels()或setPixels()被呼叫,它將丟擲異常,並且不會畫任何東西。這個操作是不能逆轉的,所以只有在你確定沒有進一步的點陣圖使用時才能呼叫它。這是一個高階呼叫,通常不需要呼叫,因為當沒有更多的引用到這個點陣圖時,正常的GC程式將釋放這個記憶體。

LruCache介紹

LruCache是個泛型類,內部採用LinkedHashMap來實現快取機制,它提供get方法和put方法來獲取快取和新增快取,其最重要的方法trimToSize是用來移除最少使用的快取和使用最久的快取,並新增最新的快取到佇列中

計算inSampleSize

通過期望的寬高來獲取壓縮比

    public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        final int width = options.outWidth;
        final int height = options.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;
    }
複製程式碼

縮圖

獲取指定的寬高的bitmap的縮圖

   public static Bitmap thumbnail(String  path, int maxWidth, int maxHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        Bitmap bitmap = BitmapFactory.decodeFile(path,options);
        int sampleSize = calculateInSampleSize(options,maxWidth,maxHeight);
        options.inJustDecodeBounds = false;
        options.inSampleSize =sampleSize;
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        options.inPurgeable =true;
        options.inInputShareable = true;
        if(bitmap !=null&&!bitmap.isRecycled()){
            bitmap.recycle();
        }
        bitmap = BitmapFactory.decodeFile(path,options);
        return bitmap;
    }
複製程式碼

儲存Bitmap

將Bitmap儲存到指定檔案中,並設定圖片質量(下文會有介紹)

public static String save(Bitmap  bitmap, Bitmap.CompressFormat format, int quality,File desFile) {
        try{
            FileOutputStream out = new FileOutputStream(desFile);
            if(bitmap.compress(format,quality,out)){
                out.flush();
                out.close();
            }
            if(bitmap!=null&&!bitmap.isRecycled()){
                bitmap.recycle();
            }
            return  desFile.getAbsolutePath();
        }catch (FileNotFoundException e){
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
複製程式碼

儲存到SD卡

public static String save(Bitmap  bitmap, Bitmap.CompressFormat format, int quality,Context context) {
        if(!Environment.getExternalStorageState()
                .equals(Environment.MEDIA_MOUNTED)){
            return null;
        }
        File dir = new File(Environment.getExternalStorageDirectory()+
        "/"+context.getPackageName());
        if(!dir.exists()){
            dir.mkdir();
        }
        File desFile = new File(dir, UUID.randomUUID().toString());
        return save(bitmap,format,quality,desFile);
    }
複製程式碼

壓縮

一、質量壓縮

用到的方法是Bitmap 的compress();

質量壓縮方法:在保持畫素的前提下改變圖片的位深及透明度等,來達到壓縮圖片的目的:

1、bitmap圖片的大小不會改變

2、bytes.length是隨著quality變小而變小的。

這樣適合去傳遞二進位制的圖片資料,比如分享圖片,要傳入二進位制資料過去,限制500kb之內。

public static Bitmap compressImage(Bitmap image) {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    // 把ByteArrayInputStream資料生成圖片
    Bitmap bitmap = null;
    image.compress(Bitmap.CompressFormat.JPEG, 5, baos);
    byte[] bytes = baos.toByteArray();
    // 把壓縮後的資料baos存放到bytes中
    bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
    if (bitmap != null) {
        loga(bitmap, baos.toByteArray());
    }
    return bitmap;
}

/**
 * @param image    目標原圖
 * @param maxSize 最大的圖片大小,
 * @return
 */
public static Bitmap compressImage(Bitmap image,long maxSize) {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    // 把ByteArrayInputStream資料生成圖片
    Bitmap bitmap = null;
    // 質量壓縮方法,options的值是0-100,這裡100表示原來圖片的質量,不壓縮,把壓縮後的資料存放到baos中
    image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
    int options = 90;
    // 迴圈判斷如果壓縮後圖片是否大於maxSize,大於繼續壓縮
    while (baos.toByteArray().length  > maxSize) {
        // 重置baos即清空baos
        baos.reset();
        // 這裡壓縮options%,把壓縮後的資料存放到baos中
        image.compress(Bitmap.CompressFormat.JPEG, options, baos);
        // 每次都減少10,當為1的時候停止,options<10的時候,遞減1
        if(options == 1){
            break;
        }else if (options <= 10) {
            options -= 1;
        } else {
            options -= 10;
        }
    }
    byte[] bytes = baos.toByteArray();
    // 把壓縮後的資料baos存放到bytes中
    bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
    if (bitmap != null) {
        loga(bitmap, baos.toByteArray());
    }
    return bitmap;
}
複製程式碼

二、取樣率壓縮

設定inSampleSize的值(int型別)後,假如設為n,則寬和高都為原來的1/n,寬高都減少,記憶體降低。上面的程式碼沒用過options.inJustDecodeBounds = true; 因為我是固定來取樣的資料,為什麼這個壓縮方法叫取樣率壓縮?

是因為配合inJustDecodeBounds,先獲取圖片的寬、高(這個過程就是取樣)。

然後通過獲取的寬高,動態的設定inSampleSize的值。 當inJustDecodeBounds設定為true的時候, BitmapFactory通過decodeResource或者decodeFile解碼圖片時, 將會返回空(null)的Bitmap物件,這樣可以避免Bitmap的記憶體分配, 但是它可以返回Bitmap的寬度、高度以及MimeType。

用法

int inSampleSize = getScaling(bitmap); bitmap = samplingRateCompression(path,inSampleSize);

private Bitmap samplingRateCompression(String path, int scaling) {

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = scaling;

Bitmap bitmap = BitmapFactory.decodeFile(path, options);

int size = (bitmap.getByteCount() / 1024 / 1024);

Log.i("wechat", "壓縮後圖片的大小" + (bitmap.getByteCount() / 1024 / 1024)
        + "M寬度為" + bitmap.getWidth() + "高度為" + bitmap.getHeight());
return bitmap;
}

/**
* 獲取縮放比例
* @param bitmap
* @return
*/
private int getScaling(Bitmap bitmap) {
    //設定目標尺寸(以畫素的寬度為標準)
    int Targetsize = 1500;
    int width = bitmap.getWidth();
    int height = bitmap.getHeight();
    //選擇最大值作為比較值(保證圖片的壓縮大小)
    int handleValue = width > height ? width : height;
    //迴圈計算壓縮比
    int i = 1;
    while (handleValue / i > Targetsize) {
        i++;
    }
}
複製程式碼

三、縮放法壓縮, 效果和方法2一樣

Android中使用Matrix對影像進行縮放、旋轉、平移、斜切等變換的。 Matrix是一個3*3的矩陣,其值對應如下:

|scaleX, skewX, translateX|

|skewY, scaleY, translateY|

|0 , 0 , scale |

Matrix提供了一些方法來控制圖片變換:

  • setTranslate(float dx,float dy):控制Matrix進行位移。
  • setSkew(float kx,float ky):控制Matrix進行傾斜,kx、ky為X、Y方向上的比例。
  • setSkew(float kx,float ky,float px,float py):控制Matrix以px、py為軸心進行傾斜,kx、ky為X、Y方向上的傾斜比例。
  • setRotate(float degrees):控制Matrix進行depress角度的旋轉,軸心為(0,0)。
  • setRotate(float degrees,float px,float py):控制Matrix進行depress角度的旋轉,軸心為(px,py)。
  • setScale(float sx,float sy):設定Matrix進行縮放,sx、sy為X、Y方向上的縮放比例。
  • setScale(float sx,float sy,float px,float py):設定Matrix以(px,py)為軸心進行縮放,sx、sy為X、Y方向上的縮放比例。

注意:以上的set方法,均有對應的post和pre方法,
Matrix呼叫一系列set,pre,post方法時,可視為將這些方法插入到一個佇列. 當然,按照佇列中從頭至尾的順序呼叫執行. 其中pre表示在隊頭插入一個方法,post表示在隊尾插入一個方法. 而set表示把當前佇列清空,並且總是位於佇列的最中間位置. 當執行了一次set後: pre方法總是插入到set前部的佇列的最前面,post方法總是插入到set後部的佇列的最後面

private Bitmap ScalingCompression(Bitmap bitmap) {
    Matrix matrix = new Matrix();
    matrix.setScale(0.25f, 0.25f);//縮放效果類似於方法2
    Bitmap bm = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
        bitmap.getHeight(), matrix, true);
    Log.i("wechat", "壓縮後圖片的大小" + (bm.getByteCount() / 1024 / 1024)
        + "M寬度為" + bm.getWidth() + "高度為" + bm.getHeight());
    return bm;
}
複製程式碼

四、Bitmap.Config

原圖大小:4M—-轉化為File—Bitmap大小

  • ALPHA_8———-6.77M ——-45M
  • ARGB_4444——-9.37M ——-22M
  • ARGB_8888——-6.77M ——-45M
  • RGB_565———–8.13M ——-22M

一般情況下預設使用的是ARGB8888,由此可知它是最佔記憶體的,因為一個畫素佔32位,8位=1位元組,所以一個畫素佔4位元組的記憶體。假設有一張480×800的圖片,如果格式為ARGB8888,那麼將會佔用1500KB的記憶體。

private Bitmap bitmapConfig(String path) {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;

    Bitmap bm = BitmapFactory.decodeFile(path, options);
    Log.i("wechat", "壓縮後圖片的大小" + (bm.getByteCount() / 1024f / 1024f)
    + "M寬度為" + bm.getWidth() + "高度為" + bm.getHeight());
    return bm;
} 
複製程式碼

五、Bitmap提供的:createScaledBitmap

方法

Bitmap.createScaledBitmap(src, dstWidth, dstHeight, filter);

引數說明:

  • src 用來構建子集的源點陣圖
  • dstWidth 新點陣圖期望的寬度
  • dstHeight 新點陣圖期望的高度
  • filter 為true則選擇抗鋸齒

補充抗鋸齒的知識點

在Android中,目前,我知道有兩種出現鋸齒的情況。

  • 1、當我們用Canvas繪製點陣圖的時候,如果對點陣圖進行了選擇,則點陣圖會出現鋸齒。
  • 2、在用View的RotateAnimation做動畫時候, 如果View當中包含有大量的圖形,也會出現鋸齒。

我們分別以這兩種情況加以考慮。 用Canvas繪製點陣圖的的情況。 在用Canvas繪製點陣圖時,一般地,我們使用drawBitmap函式家族, 在這些函式中,都有一個Paint引數, 要做到防止鋸齒,我們就要使用到這個引數。如下:

首先在你的建構函式中,需要建立一個Paint。 Paint mPaint = new Paint();
然後,您需要設定兩個引數:

  • 1)mPaint.setAntiAlias(Boolean aa);
  • 2)mPaint.setBitmapFilter(true)。

第一個函式是用來防止邊緣的鋸齒, (true時影像邊緣相對清晰一點,鋸齒痕跡不那麼明顯, false時,寫上去的字不飽滿,不美觀,看地不太清楚)。

第二個函式是用來對點陣圖進行濾波處理。

最後,在畫圖的時候,呼叫drawBitmap函式,只需要將整個Paint傳入即可。

有時候,當你做RotateAnimation時, 你會發現,鋸齒又出現了。 這個時候,由於你不能控制點陣圖的繪製, 只能用其他方法來實現防止鋸齒。 另外,如果你畫的點陣圖很多。 不想每個點陣圖的繪製都傳入一個Paint。 還有的時候,你不可能控制每個視窗的繪製的時候, 您就需要用下面的方法來處理——對整個Canvas進行處理。

  • 1)在您的建構函式中,建立一個Paint濾波器。 PaintFlagsDrawFilter mSetfil = new PaintFlagsDrawFilter(0, Paint.FILTERBITMAPFLAG); 第一個引數是你要清除的標誌位, 第二個引數是你要設定的標誌位。此處設定為對點陣圖進行濾波。
  • 2)當你在畫圖的時候, 如果是View則在onDraw當中,如果是ViewGroup則在dispatchDraw中呼叫如下函式。 canvas.setDrawFilter( mSetfil );

另外,在Drawable類及其子類中, 也有函式setFilterBitmap可以用來對Bitmap進行濾波處理, 這樣,當你選擇Drawable時,會有抗鋸齒的效果。

private Bitmap createScaledBitmap(Bitmap bitmap) {
    Bitmap bm = Bitmap.createScaledBitmap(bitmap, 200, 200, true);
    Log.i("wechat", "壓縮後圖片的大小" + (bm.getByteCount() / 1024) + "KB寬度為"
        + bm.getWidth() + "高度為" + bm.getHeight());
    return bm;
}
複製程式碼

六、輔助方法(上述方法的):

通過路徑獲取bitmap的方法

  • 1、利用BitmapFactory解析檔案,轉換為Bitmap
    bitmap = BitmapFactory.decodeFile(path);
  • 2、自己寫解碼,轉換為Bitmap過程, 同樣需使用BitmapFactory.decodeByteArray(buf, 0, len);程式碼如下:
private Bitmap getBitmapByPath(String path) {
if (!new File(path).exists()) {
    System.err.println("getBitmapByPath: 檔案不存在");
    return null;
}
byte[] buf = new byte[1024 * 1024];// 1M
Bitmap bitmap = null;
try {
    FileInputStream fis = new FileInputStream(path);
    int len = fis.read(buf, 0, buf.length);
    bitmap = BitmapFactory.decodeByteArray(buf, 0, len);
    //當bitmap為空的時候,說明解析失敗
    if (bitmap == null) {
        System.out.println("檔案長度:" + len);
        System.err.println("path: " + path + "  無法解析!!!");
    }
} catch (Exception e) {
    e.printStackTrace();
}
return bitmap;
}
複製程式碼

儲存圖片

private void savaPictrue(Bitmap bitmap) {
    File file = new File("storage/emulated/0/DCIM/Camera/test.jpg");
    FileOutputStream stream = null;
    try {
        stream = new FileOutputStream(file);
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
        Log.e("圖片大小:", file.length() / 1024 / 1024 + "M");
    } catch (Exception e) {
        e.printStackTrace();
    }
}
複製程式碼

剪下圖片(這裡只是裁剪圖片,但是對圖片的大小並不影響)

private void crop(Uri uri) {
    // 裁剪圖片意圖
    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.setDataAndType(uri, "image/*");
    intent.putExtra("crop", "true");
    // 裁剪框的比例,1:1
    intent.putExtra("aspectX", 1);
    intent.putExtra("aspectY", 1);
    // 裁剪後輸出圖片的尺寸大小
    intent.putExtra("outputX", 300);
    intent.putExtra("outputY", 300);

    intent.putExtra("outputFormat", "JPEG");// 圖片格式
    intent.putExtra("noFaceDetection", true);// 取消人臉識別
    intent.putExtra("return-data", true);
    // 開啟一個帶有返回值的Activity,請求碼為PHOTO_REQUEST_CUT
    startActivityForResult(intent, 200);
}

private void logp(Bitmap bitmap) {
Log.i("wechat", "壓縮前圖片的大小" + (bitmap.getByteCount() / 1024 / 1024)
        + "M寬度為" + bitmap.getWidth() + "高度為" + bitmap.getHeight());
}

private static void loga(Bitmap bitmap, byte[] bytes) {
    Log.i("wechat", "壓縮後圖片的大小" + (bitmap.getByteCount() / 1024 / 1024)
        + "M寬度為" + bitmap.getWidth() + "高度為" + bitmap.getHeight()
        + "bytes.length=  " + (bytes.length / 1024) + "KB"
    );
}
複製程式碼
Android中的Bitmap

相關文章