android處理拍照旋轉問題及帶來的對內存占用的思考

想必大家對android處理拍照并保存照片的應用場景已經再熟悉不過了,其中比較頭疼的問題是像部分三星手機拍完照片后保存的圖片是旋轉90度后的圖片(當然,如果橫向拍照是沒有問題的)。

本篇文章目的不是簡單解決旋轉問題,而是通過這樣的問題討論下android內存占用(主要是圖片)的問題。通過文章大家可以掌握如下知識:

  1. 如何解決上面提到的三星拍照問題
  2. 如何計算bitmap占用的內存大小
  3. 如何盡量避免OOM的出現

解決旋轉問題的方案可以是:當拍照完成并保存圖片到某個路徑(比如用file記錄了圖片路徑)下以后,加載原始圖片得到bitmap,再通過對其進行旋轉生成新的bitmap,最后保存bitmap到file路徑下來覆蓋原始路徑下的內容。
顯示代碼如下(至于如果開啟攝像機并拍照保存不在討論范圍):
AlbumAcitivity.java 核心代碼如下:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode == RESULT_OK && requestCode == CAMERA_RESULT) {
        //掃描制定位置的文件
        String scanPath = file.getAbsolutePath();
        doRotateImageAndSave(scanPath);
    }
}
//通過img得到旋轉rotate角度后的bitmap
public static Bitmap rotateImage(Bitmap img,int rotate){
    Matrix matrix = new Matrix();
    matrix.postRotate(rotate);
    int width = img.getWidth();
    int height =img.getHeight();
    img = Bitmap.createBitmap(img, 0, 0, width, height, matrix, false);
    return img;
}
//加載filePath下的圖片,旋轉并保存到filePath
private void doRotateImageAndSaveStrategy1(String filePath){
    int rotate = readPictureDegree(filePath);//獲取需要加載圖片的旋轉角度
    if(rotate == 0){
        return;
    }
    FileInputStream inputStream = null;
    try {
        inputStream = new FileInputStream(filePath);
        Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
        Bitmap destBitmap = rotateImage(bitmap, rotate);
        bitmap.recycle();

        //save to file
        FileUtil.saveImageToSDCard(destBitmap, 100, filePath);
        destBitmap.recycle();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
            }
        }
    }
}
private void doRotateImageAndSave(String filePath){
    doRotateImageAndSaveStrategy1(filePath);
}
/**
 * 讀取照片exif信息中的旋轉角度
 * @param path 照片路徑
 * @return角度
 */
public static int readPictureDegree(String path) {
    int degree  = 0;
    ExifInterface exifInterface = null;
    try {
        exifInterface = new ExifInterface(path);
    } catch (IOException ex) {
    }
    if(exifInterface == null)
        return degree;
    int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
    switch (orientation) {
        case ExifInterface.ORIENTATION_ROTATE_90:
            degree = 90;
            break;
        case ExifInterface.ORIENTATION_ROTATE_180:
            degree = 180;
            break;
        case ExifInterface.ORIENTATION_ROTATE_270:
            degree = 270;
            break;
    }
    return degree;
}

由于代碼量不是很大,而且邏輯并不復在所以在這就一一解釋每行代碼的含義了。經過測試發現在調用到doRotateImageAndSave方法時內存占用直線上升,大概一下子增加了60~70MB。如果再加上整個應用之前占用的內存恐怕瞬時的內存壓力還是很大的。
至于出現占用這么大內存的原因,稍有經驗的coder應該知道直接加載原圖而不做任何處理顯然是很危險的,如果攝像頭的分辨率越高拍出的照片越大,在內存中解碼出來后占用的內存越高。

經過(三星S6 edge+)測試發現:拍出的照片分辨率是5312*2988的,那么這張圖片解碼后會占用多大內存呢?如下公式可以表示:
占用內存大小 size = width * height * 顏色深度(depth)
至于何為顏色深度大家自己百度或google吧,簡單講就是圖片中一個像素點占的字節個數。
我們假設拍出的照片的depth等于4,那么5312 * 2988大小的圖片總共占用的內存為5312 * 2988 * 4 = 63489024(byte)。將其轉為MB為63489024/1024/1024 = 60.55MB。

android為bitmap提供的depth在Bitmap類中的Config枚舉中有定義:ALPHA_8,RGB_565,ARGB_4444,ARGB_8888.如何理解這幾個值呢?
一個顏色值都是通過ARGB四個分量來描述(其中每個分量占一個字節),其中A:透明度(ALPHA) RGB分別表示紅,綠,藍三種顏色的值。RGB三種顏色值占用的比重不同就能描述出不同的顏色。

  • ALPHA_8:只存儲顏色的ALPHA,不存儲任何RGB分量,所以1byte就夠了
  • RGB_565:只存儲顏色的RGB,不存儲ALPHA,總共占5+6+5/8=2個字節。其中5位存儲R分量,接下來的6位存儲G分量,最后的5位存儲B分量
    剩下的幾個大家應該自己能分析了,所以顏色深度(depth)最大的是ARGB_8888占了4個字節。

綜上,影響圖片在內存中的大小主要取決于:

  1. 圖片的大小
  2. 圖片的顏色深度

所以如何優化上面加載圖片,旋轉并保存圖片想必大家就有思路了。

  1. 可以通過計算需要輸出的圖片的大小和原始圖片的大小進行適當的壓縮并計算sampleSize。
  2. 是酌情更改加載圖片時指定使用顏色深度位RGB_565(主要針對沒有透明度的JPG圖片)
    演示代碼如下:
    在AlbumAcitivity.java添加代碼
private int outWidth = 0;//輸出bitmap的寬
private int outHeight = 0;//輸出bitmap的高
//計算sampleSize
private int caculateSampleSize(String imgFilePath,int rotate){
    outWidth = 0;
    outHeight = 0;
    int imgWidth = 0;//原始圖片的寬
    int imgHeight = 0;//原始圖片的高
    int sampleSize = 1;
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    FileInputStream inputStream = null;
    try {
        inputStream = new FileInputStream(imgFilePath);
        BitmapFactory.decodeStream(inputStream,null,options);//由于options.inJustDecodeBounds位true,所以這里并沒有在內存中解碼圖片,只是為了得到原始圖片的大小
        imgWidth = options.outWidth;
        imgHeight = options.outHeight;
        //初始化
        outWidth = imgWidth;
        outHeight = imgHeight;
        //如果旋轉的角度是90的奇數倍,則輸出的寬和高和原始寬高調換
        if((rotate / 90) % 2 != 0){
            outWidth = imgHeight;
            outHeight = imgWidth;
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        if(inputStream != null){
            try {
                inputStream.close();
            } catch (IOException e) {
            }
        }
    }
    //計算輸出bitmap的sampleSize
    while (imgWidth / sampleSize > outWidth || imgHeight / sampleSize > outHeight) {
        sampleSize = sampleSize << 1;
    }
    return sampleSize;
}
private void doRotateImageAndSaveStrategy2(String filePath){
    int rotate = readPictureDegree(filePath);
    if(rotate == 0)
        return;
    //得到sampleSize
    int sampleSize = caculateSampleSize(filePath, rotate);
    if (outWidth == 0 || outHeight == 0)
        return;

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inSampleSize = sampleSize;
    //適當調整顏色深度
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    options.inJustDecodeBounds = false;
    FileInputStream inputStream = null;
    try {
        inputStream = new FileInputStream(filePath);
        Bitmap srcBitmap = BitmapFactory.decodeStream(inputStream, null, options);//加載原圖
        //test
        Bitmap.Config srcConfig = srcBitmap.getConfig();
        int srcMem = srcBitmap.getRowBytes() * srcBitmap.getHeight();//計算bitmap占用的內存大小

        Bitmap destBitmap = rotateImage(srcBitmap, rotate);
        int destMem = srcBitmap.getRowBytes() * srcBitmap.getHeight();
        srcBitmap.recycle();

        //保存bitmap到文件(覆蓋原始圖片)
        FileUtil.saveImageToSDCard(destBitmap, 100, filePath);
        destBitmap.recycle();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (OutOfMemoryError error) {
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
            }
        }
    }
}

最后在doRotateImageAndSave方法中調用doRotateImageAndSaveStrategy2。

還是以拍照后圖片大小為5312*2988為例,通過caculateSampleSize可以計算出outWidth和outHeight分別為2988和5312,sampleSize為2。核心代碼是:

if((rotate / 90) % 2 != 0){
      outWidth = imgHeight;
      outHeight = imgWidth;
}
while (imgWidth / sampleSize > outWidth || imgHeight / sampleSize > outHeight) {
       sampleSize = sampleSize << 1;
  }

計算好了sampleSize為2后,再通過BitmapFactory.decodeStream并設置好對應的options,得到的bitmap的大小為:
width=5312 / sampleSize 即:2656
height=2988 / sampleSize 即:1494
可以看出如果顏色深度保持和原圖一樣為4,經過處理后的bitmap占用的內存為2656*1494*4 = 15872256 轉為MB為15.2MB, 是原始圖片占用內存的1/4。如果我們再設置options的顏色深度為options.inPreferredConfig = Bitmap.Config.RGB_565 如果原始圖片顏色深度為Bitmap.Config.ARGB_8888,則最終bitmap占用的內存為:2656*1494*2 = 7936128 轉為MB為7.56MB。

OK,最后通過rotateImage方法對2656*1494的圖片進行旋轉(90度)操作后并保存到原始路徑,所以最后保存的圖片大小為1494*2656。以上解決方案雖然損失了些圖片的清新度,但是卻能極大的節省內存讓OOM的概率將到最低。希望這篇文章能讓大家明白計算sampleSize,如何計算bitmap占用的內存大小。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 2021期待與你一起共事,點擊查看崗位[http://www.lxweimin.com/p/6f4d67fa406...
    閑庭閱讀 16,706評論 0 75
  • 一直以來Bitmap都是開發中很棘手的問題,這個問題就是傳說中的OOM(java.lang.OutofMemory...
    M悇芐冋憶閱讀 4,883評論 0 11
  • 參考資料 目錄 Bitmap BitmapFactory Bitmap加載方法 Bitmap | Drawable...
    玄策閱讀 2,793評論 0 7
  • BitmapFactory.options BitmapFactory.Options類是BitmapFactor...
    Showdy閱讀 12,662評論 1 24
  • 文 | 望之 如果你希望1小時速成,那么我建議你可以去看看別的文章。 你有試過矢量模式了么,下面我們來學習下矢量模...
    秦瑞麒閱讀 507評論 0 0